Vulkan Memory and Resources
- Host Memory Management
- Resources
- Device Memory Management
- Summary
Vulkan architects introduce the memory system, perhaps the most fundamental part of the interface. They show how to allocate memory used by the Vulkan device and by Vulkan drivers and system components running inside your application.
Save 35% off the list price* of the related book or multi-format eBook (EPUB + MOBI + PDF) with discount code ARTICLE.
* See informit.com/terms
Memory is fundamental to the operation of virtually all computing systems, including Vulkan. In Vulkan, there are two fundamental types of memory: host memory and device memory. All resources upon which Vulkan operates must be backed by device memory, and it is the application’s responsibility to manage this memory. Further, memory is used to store data structures on the host. Vulkan provides the opportunity for your application to manage this memory too. In this chapter, you’ll learn about the mechanisms through which you can manage memory used by Vulkan.
Host Memory Management
Whenever Vulkan creates new objects, it might need memory to store data related to them. For this, it uses host memory, which is regular memory accessible to the CPU that might be returned from a call to malloc or new, for example. However, beyond a normal allocator, Vulkan has particular requirements for some allocations. Most notably, it expects allocations to be aligned correctly. This is because some high-performance CPU instructions work best (or only) on aligned memory addresses. By assuming that allocations storing CPU-side data structures are aligned, Vulkan can use these high-performance instructions unconditionally, providing substantial performance advantages.
Because of these requirements, Vulkan implementations will use advanced allocators to satisfy them. However, it also provides the opportunity for your application to replace the allocators for certain, or even all, operations. This is performed through the pAllocator parameter available in most device creation functions. For example, let’s revisit the vkCreateInstance() function, which is one of the first that your application might call. Its prototype is
VkResult vkCreateInstance ( const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkInstance* pInstance);
The pAllocator parameter is a pointer to a VkAllocationCallbacks structure. Until now, we’ve been setting pAllocator to nullptr, which tells Vulkan to use its own internal allocator rather than rely on our application. The VkAllocationCallbacks structure encapsulates a custom memory allocator that we can provide. The definition of the structure is
typedef struct VkAllocationCallbacks { void* pUserData; PFN_vkAllocationFunction pfnAllocation; PFN_vkReallocationFunction pfnReallocation; PFN_vkFreeFunction pfnFree; PFN_vkInternalAllocationNotification pfnInternalAllocation; PFN_vkInternalFreeNotification pfnInternalFree; } VkAllocationCallbacks;
You can see from the definition of VkAllocationCallbacks that the structure is essentially a set of function pointers and an additional void pointer, pUserData. The pointer is for your application’s use. It can point anywhere; Vulkan will not dereference it. In fact, it doesn’t even need to be a pointer. You can put anything in there, so long as it fits into a pointer-size blob. The only thing that Vulkan will do with pUserData is pass it back to the callback functions to which the remaining members of VkAllocationCallbacks point.
pfnAllocation, pfnReallocation, and pfnFree are used for normal, object-level memory management. They are defined as pointers to functions that match the following declarations:
void* VKAPI_CALL Allocation( void* pUserData, size_t size, size_t alignment, VkSystemAllocationScope allocationScope); void* VKAPI_CALL Reallocation( void* pUserData, void* pOriginal size_t size, size_t alignment, VkSystemAllocationScope allocationScope); void VKAPI_CALL Free( void* pUserData, void* pMemory);
Notice that all three functions take a pUserData parameter as their first argument. This is the same pUserData pointer that’s part of the VkAllocationCallbacks structure. If your application uses data structures to manage memory, this is a good place to put their addresses. One logical thing to do with this is to implement your memory allocator as a C++ class (assuming you’re writing in C++) and then put the class’s this pointer in pUserData.
The Allocation function is responsible for making new allocations. The size parameter gives the size of the allocation, in bytes. The alignment parameter gives the required alignment of the allocation, also in bytes. This is an often-overlooked parameter. It is very tempting to simply hook this function up to a naïve allocator such as malloc. If you do this, you will find that it works for a while but that certain functions might mysteriously crash later. If you provide your own allocator, it must honor the alignment parameter.
The final parameter, allocationScope, tells your application what the scope, or lifetime, of the allocation is going to be. It is one of the VkSystemAllocationScope values, which have the following meanings:
VK_SYSTEM_ALLOCATION_SCOPE_COMMAND means that the allocation will be live only for the duration of the command that provoked the allocation. Vulkan will likely use this for very short-lived temporary allocations, as it works on a single command.
VK_SYSTEM_ALLOCATION_SCOPE_OBJECT means that the allocation is directly associated with a particular Vulkan object. This allocation will live at least until the object is destroyed. This type of allocation will only ever be made as part of executing a creation command (one beginning with vkCreate).
VK_SYSTEM_ALLOCATION_SCOPE_CACHE means that the allocation is associated with some form of internal cache or a VkPipelineCache object.
VK_SYSTEM_ALLOCATION_SCOPE_DEVICE means that the allocation is scoped to the device. This type of allocation is made when the Vulkan implementation needs memory associated with the device that is not tied to a single object. For example, if the implementation allocates objects in blocks, this type of allocation might be made in response to a request to create a new object, but because many objects might live in the same block, the allocation can’t be tied directly to any specific object.
VK_SYSTEM_ALLOCATION_SCOPE_INSTANCE means that the allocation is scoped to the instance. This is similar to VK_SYSTEM_ALLOCATION_SCOPE_DEVICE. This type of allocation is typically made by layers or during early parts of Vulkan startup, such as by vkCreateInstance() and vkEnumeratePhysicalDevices().
The pfnInternalAllocation and pfnInternalFree function pointers point to alternate allocator functions that are used when Vulkan makes memory allocations using its own allocators. These callbacks have the same signatures as pfnAllocation and pfnFree, except that pfnInternalAllocation doesn’t return a value and pfnInternalFree shouldn’t actually free the memory. These functions are used only for notification so that your application can keep track of how much memory Vulkan is using. The prototypes of these functions should be
void VKAPI_CALL InternalAllocationNotification( void* pUserData, size_t size, VkInternalAllocationType allocationType, VkSystemAllocationScope allocationScope); void VKAPI_CALL InternalFreeNotification( void* pUserData, size_t size, VkInternalAllocationType allocationType, VkSystemAllocationScope allocationScope);
There’s not much you can do with the information provided through pfnInternalAllocation and pfnInternalFree besides log it and keep track of the total memory usage made by the application. Specifying these function pointers is optional, but if you supply one, you must supply both. If you don’t want to use them, set them both to nullptr.
Listing 2.1 shows an example of how to declare a C++ class that can be used as an allocator that maps the Vulkan allocation callback functions. Because the callback functions used by Vulkan are naked C function pointers, the callback functions themselves are declared as static member functions of the class, whereas the actual implementations of those functions are declared as regular nonstatic member functions.
Listing 2.1: Declaration of a Memory Allocator Class
class allocator { public: // Operator that allows an instance of this class to be used as a // VkAllocationCallbacks structure inline operator VkAllocationCallbacks() const { VkAllocationCallbacks result; result.pUserData = (void*)this; result.pfnAllocation = &Allocation; result.pfnReallocation = &Reallocation; result.pfnFree = &Free; result.pfnInternalAllocation = nullptr; result.pfnInternalFree = nullptr; return result; }; private: // Declare the allocator callbacks as static member functions. static void* VKAPI_CALL Allocation( void* pUserData, size_t size, size_t alignment, VkSystemAllocationScope allocationScope); static void* VKAPI_CALL Reallocation( void* pUserData, void* pOriginal, size_t size, size_t alignment, VkSystemAllocationScope allocationScope); static void VKAPI_CALL Free( void* pUserData, void* pMemory); // Now declare the nonstatic member functions that will actually perform // the allocations. void* Allocation( size_t size, size_t alignment, VkSystemAllocationScope allocationScope); void* Reallocation( void* pOriginal, size_t size, size_t alignment, VkSystemAllocationScope allocationScope); void Free( void* pMemory); };
An example implementation of this class is shown in Listing 2.2. It maps the Vulkan allocation functions to the POSIX aligned_malloc functions. Note that this allocator is almost certainly not better than what most Vulkan implementations use internally and serves only as an example of how to hook the callback functions up to your own code.
Listing 2.2: Implementation of a Memory Allocator Class
void* allocator::Allocation( size_t size, size_t alignment, VkSystemAllocationScope allocationScope) { return aligned_malloc(size, alignment); } void* VKAPI_CALL allocator::Allocation( void* pUserData, size_t size, size_t alignment, VkSystemAllocationScope allocationScope) { return static_cast<allocator*>(pUserData)->Allocation(size, alignment, allocationScope); } void* allocator::Reallocation( void* pOriginal, size_t size, size_t alignment, VkSystemAllocationScope allocationScope) { return aligned_realloc(pOriginal, size, alignment); } void* VKAPI_CALL allocator::Reallocation( void* pUserData, void* pOriginal, size_t size, size_t alignment, VkSystemAllocationScope allocationScope) { return static_cast<allocator*>(pUserData)->Reallocation(pOriginal, size, alignment, allocationScope); } void allocator::Free( void* pMemory) { aligned_free(pMemory); } void VKAPI_CALL allocator::Free( void* pUserData, void* pMemory) { return static_cast<allocator*>(pUserData)->Free(pMemory); }
As can be seen in Listing 2.2, the static member functions simply cast the pUserData parameters back to a class instance and call the corresponding nonstatic member function. Because the nonstatic and static member functions are located in the same compilation unit, the nonstatic member function is likely to be inlined into the static one, making the efficiency of this implementation quite high.