VK_KHR_dedicated_allocation unofficial manual

Introduction

This is an unofficial manual for a Vulkan extension "VK_KHR_dedicated_allocation". I've written it because I couldn't find anything like this on the Internet, while understanding all the logic involved in its usage based on Vulkan specification only (available for download from Khronos Vulkan Registry) may be hard. Here I provide general description of the extension and snippets of example code.

I think that difficulty level of this article is advanced. To understand it, you need to know basics of programming in C or C++ (e.g. pointers), as well as already have some experience in graphics programming using Vulkan.

Problem

Resources in Vulkan

In previous generation graphics APIs, like DirectX 9, 10, 11, or OpenGL, you could create a texture, vertex buffer, index buffer, and other types of resources with just one call. In Vulkan it is more difficult. Here you need to separately create a resource (there are only two types - buffers and images), allocate a block of device memory (VkDeviceMemory object), and finally bind them together. In between you also need to query the resource for "memory requirements" - like required size and alignment. Typical code looks like this:

// 1. Create buffer.
VkBufferCreateInfo bufferCreateInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
// Fill members of bufferCreateInfo...

VkBuffer buffer;
vkCreateBuffer(device, &bufferCreateInfo, nullptr, &buffer);

// 2. Query for memory requirements of this buffer.
VkMemoryRequirements memReq;
vkGetBufferMemoryRequirements(device, buffer, &memReq);

// 3. Allocate device memory.
VkMemoryAllocateInfo allocateInfo = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
allocateInfo.allocationSize = memReq.size;
allocateInfo.memoryTypeIndex = /* Some magic to find suitable memory type using
    memReq.memoryTypeBits. */;
    
VkDeviceMemory memory;
vkAllocateMemory(device, &allocateInfo, nullptr, &memory);

// 4. Bind buffer with memory at offset 0.
vkBindBufferMemory(device, buffer, memory, 0);

Note that I omit any checking of errors (VkResult value returned from Vulkan functions) just for clarity. In real code you should never do this. You also mustn't forget to destroy all your Vulkan object when no longer needed using appropriate functions: vkDestroyBuffer, vkFreeMemory. I show all examples using buffers, but everything in this article applies to images as well - just replace any "buffer" with "image".

Suballocation

In real applications you shouldn't allocate separate VkDeviceMemory for each buffer or image. First, it's inefficient - memory allocation can be a slow operation. Second, there is a limit on maximum number of such allocations that you can make (VkPhysicalDeviceLimits::maxMemoryAllocationCount), which may be as small as 4096.

It is thus recommended to allocate bigger blocks of device memory and manually bind (we could say "suballocate") parts of them to your resources, using offset. There is a good article about this topic: "Vulkan Memory Management". To do this you have to implement some form of memory allocator (or use Vulkan Memory Allocator library, which I developed as part of my work at AMD).

Dedicated allocation

The usage pattern described above seems to be initial intention of the creators of Vulkan, but it turned out that some kinds of buffers and images (probably bigger ones) on some GPUs could benefit from having their own, dedicated allocation - a VkDeviceMemory object created to fit just that single resource. Then graphics driver could perform some additional optimizations, invisible to the developer.

Fortunately, Vulkan, just like OpenGL, supports extensions. I'm not going to explain here how do extensions in Vulkan work. I hope you already know that. Probably the most interesting and difficult concept is the existence of members sType and pNext in almost every Vulkan structure. Thanks to them you can augment your data passed to or returned from Vulkan functions with new structures, non-existent in original specification but defined by some extension. You identify them using sType enum and chain them using pNext pointer, pointing from one structure to the next.

VK_KHR_dedicated_allocation

Introduction

To support such dedicated allocations, first an NVIDIA specific extension appeared: "VK_NV_dedicated_allocation". Obviously, it's supported by GeForce cards only. A new extension came up recently: "VK_KHR_dedicated_allocation", which this article is about. Its "KHR" prefix suggests that it is more general, not any vendor-specific. Actually, as you can see in Vulkan Hardware Database, it is supported by NVIDIA and Intel GPUs with latest driver installed. AMD doesn't support it because it doesn't need it.

VK_KHR_dedicated_allocation relies on another extension: VK_KHR_get_memory_requirements2. Why is this? I will explain it in a moment. For now, you must know that both extensions must be supported by a device if you want to use the features described here.

Enabling extensions

First thing you need to do is to check whether both extensions: VK_KHR_get_memory_requirements2 and VK_KHR_dedicated_allocation are supported by the device and if so, enable them when creating the device.

bool getMemoryRequirements2Enabled = false;
bool dedicatedAllocationEnabled = false;

// Here I will collect list of requested extensions.
std::vector<const char*> enabledExtensions;

// This extension is mandatory for my application.
enabledExtensions.push_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME);

// Enumerate supported extensions from physical device.
uint32_t propertyCount;
vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &propertyCount, nullptr);
std::vector<VkExtensionProperties> properties(propertyCount);
vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &propertyCount, properties.data());

// Check if extensions of our interest are supported.
for(uint32_t i = 0; i < propertyCount; ++i)
{
    if(strcmp(properties[i].extensionName,
        VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME) == 0)
    {
        enabledExtensions.push_back(VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME);
        getMemoryRequirements2Enabled = true;
    }
    else if(strcmp(properties[i].extensionName,
        VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME) == 0)
    {
        enabledExtensions.push_back(VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME);
        dedicatedAllocationEnabled = true;
    }
}

// Create Vulkan device.
VkDeviceCreateInfo deviceCreateInfo = { VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO };
deviceCreateInfo.enabledExtensionCount = (uint32_t)enabledExtensions.size();
deviceCreateInfo.ppEnabledExtensionNames = enabledExtensions.data();
// Fill other members of deviceCreateInfo...

VkDevice device;
vkCreateDevice(physicalDevice, &deviceCreateInfo, nullptr, &device);

Fetching function pointers

VK_KHR_get_memory_requirements2 extension adds 3 functions: vkGetImageMemoryRequirements2KHR, vkGetBufferMemoryRequirements2KHR, vkGetImageSparseMemoryRequirements2KHR, as well as 5 structures: VkBufferMemoryRequirementsInfo2KHR, VkImageMemoryRequirementsInfo2KHR, VkImageSparseMemoryRequirementsInfo2KHR, VkMemoryRequirements2KHR, VkSparseImageMemoryRequirements2KHR. VK_KHR_dedicated_allocation extension adds 2 structures: VkMemoryDedicatedRequirementsKHR and VkMemoryDedicatedAllocateInfoKHR.

Using these structures is simple - they are just defined in "vulkan.h" header like standard ones. Functions added by an extension cannot be linked statically, like normal Vulkan functions. You must fetch pointers to these functions using function vkGetDeviceProcAddr and use them via these pointers.

PFN_vkGetBufferMemoryRequirements2KHR pVkGetBufferMemoryRequirements2KHR = nullptr;
PFN_vkGetImageMemoryRequirements2KHR pVkGetImageMemoryRequirements2KHR = nullptr;

if(getMemoryRequirements2Enabled && dedicatedAllocationEnabled)
{
    pVkGetBufferMemoryRequirements2KHR = (PFN_vkGetBufferMemoryRequirements2KHR)
        vkGetDeviceProcAddr(device, "vkGetBufferMemoryRequirements2KHR");
    pVkGetImageMemoryRequirements2KHR = (PFN_vkGetImageMemoryRequirements2KHR)
        vkGetDeviceProcAddr(device, "vkGetImageMemoryRequirements2KHR");
}

By the way: Article "Architecture of the Vulkan Loader Interfaces" recommends to use vkGetInstanceProcAddr and vkGetDeviceProcAddr to fetch pointers to regular Vulkan functions as well, as more efficient way.

Querying memory requirements

First thing that the "dedicated allocation" extension adds is a possibility to query driver whether it would like to have a resource in a separate, dedicated memory block. It can be queried together with old "memory requirements" parameters, like size, alignment, and memoryTypeBits.

There is one problem though. vkGetBufferMemoryRequirements function takes just handle to a buffer and returns structure VkMemoryRequirements. None of these have sType or pNext, which would allow to attach additional structures. This oversight is fixed by VK_KHR_get_memory_requirements2 that defines a new function: vkGetBufferMemoryRequirements2KHR. This function takes a new structure VkBufferMemoryRequirementsInfo2KHR as input, which contains handle to buffer, as well as sType and pNext fields. It fills new structure VkMemoryRequirements2KHR as output, which contains old VkMemoryRequirements as its member, but also has sType and pNext. This way, it offers the same functionality, but it is also extensible.

VK_KHR_dedicated_allocation builds on top of that. It adds structure VkMemoryDedicatedRequirementsKHR, to be attached to VkMemoryRequirements2KHR. When present, it will be filled by the function with an information about whether graphics driver prefers or requires to have this buffer in a dedicated allocation.

// 2. Query for memory requirements of this buffer.
VkMemoryRequirements memReq = {};
bool dedicatedAllocation = false;
if(getMemoryRequirements2Enabled && dedicatedAllocationEnabled)
{
    VkBufferMemoryRequirementsInfo2KHR memReqInfo2 = {
        VK_STRUCTURE_TYPE_BUFFER_MEMORY_REQUIREMENTS_INFO_2_KHR };
    memReqInfo2.buffer = buffer;
    
    VkMemoryRequirements2KHR memReq2 = { VK_STRUCTURE_TYPE_MEMORY_REQUIREMENTS_2_KHR };

    VkMemoryDedicatedRequirementsKHR memDedicatedReq = {
        VK_STRUCTURE_TYPE_MEMORY_DEDICATED_REQUIREMENTS_KHR };
    memReq2.pNext = &memDedicatedReq;

    pVkGetBufferMemoryRequirements2KHR(device, &memReqInfo2, &memReq2);

    memReq = memReq2.memoryRequirements;
    dedicatedAllocation =
        (memDedicatedReq.requiresDedicatedAllocation != VK_FALSE) ||
        (memDedicatedReq.prefersDedicatedAllocation != VK_FALSE);
}
else
{
    vkGetBufferMemoryRequirements(device, buffer, &memReq);
}

Allocating dedicated memory

Finally, if we are going to respect the request for dedicated memory, we must not only create separate VkDeviceMemory object for that buffer, but we must also inform the driver that this really is a dedicated allocation. Our extension adds another structure for that: VkMemoryDedicatedAllocateInfoKHR. It should be attached to VkMemoryAllocateInfo. With it you can pass handle to a buffer or an image that the memory block you are about to allocate will be exclusively bound to.

// 3. Allocate device memory.
VkDeviceMemory memory;
VkDeviceSize offset = 0;
if(dedicatedAllocation)
{
    VkMemoryAllocateInfo allocateInfo = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
    allocateInfo.allocationSize = memReq.size;
    allocateInfo.memoryTypeIndex = /* Some magic to find suitable memory type using memReq.memoryTypeBits. */

    VkMemoryDedicatedAllocateInfoKHR dedicatedAllocateInfo = {
        VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO_KHR };
    dedicatedAllocateInfo.buffer = buffer;
    allocateInfo.pNext = &dedicatedAllocateInfo;

    vkAllocateMemory(device, &allocateInfo, nullptr, &memory);
}
else
{
    // Try to find free space in an existing, bigger block of VkDeviceMemory or allocate new one.
    // Respect parameters from memReq.
    memory = /* ... */;
    offset = /* ... */;
}

// 4. Bind buffer with memory.
vkBindBufferMemory(device, buffer, memory, offset);

Additional notes

When using the extension described in this article together with Vulkan Validation Layer, you will receive warnings like this:

vkBindBufferMemory(): Binding memory to buffer 0x33 but vkGetBufferMemoryRequirements() has not been called on that buffer.

It is OK, you should just ignore it. It happens because you use function vkGetBufferMemoryRequirements2KHR instead of standard vkGetBufferMemoryRequirements, while the validation layer seems to be unaware of it.

Can we create dedicated allocation for a buffer or an image and use VkMemoryDedicatedAllocateInfoKHR with it even if it wasn't requested by the driver as requiresDedicatedAllocation or prefersDedicatedAllocation? I think yes. It seems to work, although I doubt it could provide any performance benefit in such case.

If you decide to use Vulkan Memory Allocator library to handle allocation of Vulkan resources for you, usage of VK_KHR_dedicated_allocation extension will also happen automatically, because the library internally supports it. See library documentation to learn how to enable it.

Adam Sawicki
2017-10-16
STAT NO AD
[Stat] [STAT NO AD] [Download] [Dropbox] [pub] [Mirror]
Copyright © 2004-2017