vma-logo

In my post A Simple Vulkan Compute Example in C++ I described the hello world of Vulkan Compute. A very simple application that squares a vector of integers using a HLSL compute shader. It is quite an involved process, requiring many steps to be completed before getting to the actual compute shader execution. You need to:

  1. Create a Vulkan Instance, Physical Device, Logical Device
  2. Find the flags required to create a compute Queue
  3. Create the buffers that the shader will operate on:
    1. Query the memory requirements for a particular buffer
    2. Find the index of the memory type to create the buffer from
    3. Allocate Memory for the buffers
    4. Map the memory and fill it with the data you want
    5. Bind the buffers to the memory
  4. Create a Descriptor Set, Shader Module, Pipeline
  5. Create a Command Pool, Command Buffer, Fences
  6. Dispatch the shader
  7. Wait for completion
  8. Map the buffers and read the results back

It is a rather complex process just to run some program on your GPU.

I deliberately split the buffer creation steps into multiple sub-steps to highlight how difficult it can be to manage memory in a Vulkan application. Note that the history repeats for DirectX 12. You need to be aware of the different types of memory and you need to be clever when allocating memory and binding buffers to it. Ideally you allocate a large chunk of memory with several buffers bound to it, when buffers get destroyed you just free the memory in the the pool leaving it available for another allocation. Allocations are quite expensive, so avoiding allocating memory is a good idea.

Fortunately there is a super easy-to-use, open-source, MIT licensed library developed by AMD as part of their GPU-Open initiative that helps you to manage memory in your Vulkan application. The Vulkan Memory Allocator, or VMA for short

The Vulkan Memory Allocator (VMA) Library

Some useful links:

Basic usage

This is a single-header C++ library with a C interface. You can just put the header in your repository and done, installed. You just need to include the following:

#define VMA_IMPLEMENTATION
#include "vk_mem_alloc.h"

The VMA_IMPLEMENTATION macro needs to be defined in a single source file or you will get linker errors, so it is recommended that you create a dedicated translation unit just for that.

You start by creating an allocator:

VmaAllocatorCreateInfo AllocatorInfo = {};
AllocatorInfo.vulkanApiVersion = DeviceProps.apiVersion;
AllocatorInfo.physicalDevice = PhysicalDevice;
AllocatorInfo.device = Device;
AllocatorInfo.instance = Instance;

VmaAllocator Allocator;
vmaCreateAllocator(&AllocatorInfo, &Allocator);

Then you can start creating buffers:

VkBuffer InBufferRaw;
VkBuffer OutBufferRaw;

VmaAllocationCreateInfo AllocationInfo = {};
AllocationInfo.usage = VMA_MEMORY_USAGE_CPU_TO_GPU;

VmaAllocation InBufferAllocation;
vmaCreateBuffer(Allocator,
                &static_cast<VkBufferCreateInfo>(BufferCreateInfo),
                &AllocationInfo,
                &InBufferRaw,
                &InBufferAllocation,
                nullptr);

AllocationInfo.usage = VMA_MEMORY_USAGE_GPU_TO_CPU;
VmaAllocation OutBufferAllocation;
vmaCreateBuffer(Allocator,
                &static_cast<VkBufferCreateInfo>(BufferCreateInfo),
                &AllocationInfo,
                &OutBufferRaw,
                &OutBufferAllocation,
                nullptr);

vk::Buffer InBuffer = InBufferRaw;
vk::Buffer OutBuffer = OutBufferRaw;

int32_t* InBufferPtr = nullptr;
vmaMapMemory(Allocator, InBufferAllocation, reinterpret_cast<void**>(&InBufferPtr));
for (int32_t I = 0; I < NumElements; ++I)
{
    InBufferPtr[I] = I;
}
vmaUnmapMemory(Allocator, InBufferAllocation);

Note the reduction of lines of code compared to the raw Vulkan way of allocating memory, the library handles the details for you, and what’s best, it does it very efficiently.

Visualising Memory Allocations

One very neat feature of this library is its capability to display your memory allocations in a visual way.

At any point in your program you can build a stats string, which is a json file containing the internals of the library. This json can then be fed into a tool that comes with the library to generate an image that shows you how your application is using the memory, you can see more information on the VmaDumpVis on Github, but you can basically do this:

char* StatsString = nullptr;
vmaBuildStatsString(Allocator, &StatsString, true);
{
    std::ofstream OutStats{ "VmaStats.json" };
    OutStats << StatsString;
}
vmaFreeStatsString(Allocator, StatsString);

And then run the VmaDumpVis.py script with this file to get an image with a snapshot of your memory allocations. Pretty cool!

To conclude, this library is super useful, efficient and easy to integrate and you should really use it in your Vulkan application.

I have updated my Vulkan Compute Sample on Github to include an example of how to integrate VMA in an application. You can disable VMA by simply commenting the line

// Comment this to disable VMA support
#define WITH_VMA

This shows how easy it is to replace memory allocations in Vulkan with VMA