Buffer device addresses in Vulkan and VMA
Published: 2023-05-15 17:00:42

A while ago, I posted about [managing bindless descriptors in Vulkan](https://dev.to/gasim/implementing-bindless-design-in-vulkan-34no) with heavy focus on setting up a system for accessing buffers of different structures using descriptor indexing.

As I recently learned about buffer device addresses, I want to talk about device buffer addresses and how they simplify managing and accessing buffers by getting rid of descriptors and making buffer access more intuitiveş

## What are buffer device addresses?

Buffer device address ([VK_KHR_buffer_device_address](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_KHR_buffer_device_address.html)) is a Vulkan extension that enables to fetching memory address of a buffer in GPU and using `GLSL_EXT_buffer_reference` in shaders.

## GLSL buffer reference

GLSL buffer reference ([GLSL_EXT_buffer_reference](https://github.com/KhronosGroup/GLSL/blob/master/extensions/ext/GLSL_EXT_buffer_reference.txt)) shader extension enables defining structures `buffer_reference` layout and using those structures in other storages blocks such as uniforms, push constants, and buffers:

```glsl
layout(buffer_reference, std430, buffer_reference_align=16) buffer MyBuffer {
  vec4 color;
};

layout(push_constant) uniform Data {
  // Maps device address stored in push constant
  // to MyBuffer structure.
  MyBuffer buffer;
} pcData;

void main() {
  // Using the buffer directly from push constant
  // as if it is an object.
  vec4 color = pcData.buffer.color;
} 
```

## Setup

Before we start with our application code, we need to enable creating buffers with .

### Enable device feature

Now, we need to enable the device feature. To use this feature we are going to utilize `VkPhysicalDeviceFeatures2`, which we can pass as `pNext` device create info structure:

```cpp
// Create the feature chain
VkPhysicalDeviceDescriptorIndexingFeatures descriptorIndexingFeatures{};
descriptorIndexingFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES;

// Pass your other features through this chain
descriptorIndexingFeatures.pNext = nullptr;

VkPhysicalDeviceBufferDeviceAddressFeatures bufferDeviceAddressFeatures{};
bufferDeviceAddressFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BUFFER_DEVICE_ADDRESS_FEATURES;
bufferDeviceAddressFeatures.pNext = &descriptorIndexingFeatures;

VkPhysicalDeviceFeatures2 deviceFeatures{};
deviceFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
deviceFeatures.pNext = &bufferDeviceAddressFeatures;

// Fetch all features
vkGetPhysicalDeviceFeatures2(physicalDevice, &deviceFeatures);

// Buffer device address feature is required to exist
// in the physical device
assert(bufferDeviceFeatures.bufferDeviceAddress);

createDeviceInfo.pNext = &deviceFeatures;
// create device
```

> **Note:**
>
> If you are using Vulkan < 1.2, you need to enable the extension explicitly:
> ```cpp
> std::vector<const char *> extensions;
> // other extensions
> // Add buffer device address extension
> extensions.push_back(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME);
> createDeviceInfo.ppEnabledExtensionNames = extensions.data();
> createDeviceInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
> ```

### Enable buffer device addresses in VMA

Now that the feature is enabled, we need to enable buffer device addresses in [Vulkan Memory Allocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator):

```cpp
VmaAllocatorCreateInfo createInfo{};
createInfo.instance = vulkanInstance;
createInfo.physicalDevice = vulkanPhysicalDevice;
createInfo.device = vulkanDevice;
// Pass the buffer device address flag
createInfo.flags = VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT;
vmaCreateAllocator(&createInfo, &mAllocator);
```

Adding `VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT` flags automatically adds `VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT` when new memory blocks are allocated by VMA.

### Enable buffer access from shaders

Let's add `VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT` usage flag during buffer creation to allow accessing these buffers from shaders.

```cpp
VkBufferUsageFlags bufferUsage = VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT;

VkBufferCreateInfo createBufferInfo{};
createBufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
createBufferInfo.pNext = nullptr;
createBufferInfo.flags = 0;
createBufferInfo.size = description.size;
createBufferInfo.usage = bufferUsage;

// create device here
```

That's it! Now we can query and access device addresses of buffers in our application and use the addresses to access buffers in shaders.

## Pass buffer device addresses to shaders

After enabling the buffer device address, let's fetch the device addresses of buffers and send them to shaders:

### Introducing new type in our application

Buffer device addresses have type uint64_t. In order to have compile time guarantees and between uint64_t and device addresses, we are going to introduce a new type:

```cpp
enum class DeviceAddress : uint64_t { Invalid = 0 };
```

This will provide minor assurance for us to not accidentally pass unintended values within our application.

### Fetch buffer device addresses

Let's introduce a function that fetches device address of a buffer:

```cpp
DeviceAddress getBufferDeviceAddress(VkBuffer buffer) {
  VkBufferDeviceAddressInfo addressInfo{};
  addressInfo.sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO;
  addressInfo.pNext = nullptr;
  addressInfo.buffer = buffer;
  return static_cast<DeviceAddress>(
      vkGetBufferDeviceAddress(mDevice, &addressInfo));
}
```

## Pass device addresses to pipelines

We are going to use the same `BindlessParams` system to pass draw parameters to shaders. Nothing inside the `BindlessParams` is going to change. Let's also use the same example in the previous post but with `DeviceAddress` instead of `BufferHandle`:

```cpp
struct PBRParams {
  DeviceAddress meshTransforms;
  DeviceAddress pointLights;
  DeviceAddress camera;
};

struct SkyboxParams {
  DeviceAddress camera;
  TextureHandle skybox;
  uint32_t pad0;
};

struct TextParams {
  DeviceAddress textTransforms;
  DeviceAddress camera;
  DeviceAddress glyphsBuffer;
};

BindlessParams bindlessParams(minUniformBufferOffsetAlignment);

auto rangePBR = bindlessParams.addRange(PBRParams({
  getBufferDeviceAddress(meshTransformsBuffer),
  getBufferDeviceAddress(pointLightsBuffer),
  getBufferDeviceAddress(cameraBuffer)
});

auto rangeSkybox = bindlessParams.addRange(SkyboxParams({
  getBufferDeviceAddress(cameraBuffer),
  getBufferDeviceAddress(skyboxTexture)
});

auto textParams = bindlessParams.addRange(TextParams({
  getBufferDeviceAddress(textTransformsBuffer),
  getBufferDeviceAddress(cameraBuffer),
  getBufferDeviceAddress(glyphsBuffer)
});

bindlessParams.build(mDevice, mAllocator, mDescriptorPool);
```

As you can see, we do not need any descriptor to store buffers like we did with descriptor indexing. We still use descriptor indexing for textures and use dynamic uniform buffer to pass draw parameters to the shaders but we do not need to manage any descriptors for buffers.

## Accessing buffers in shaders

We now pass buffer addresses to shaders, now it is time to utilize these addresses using GLSL `buffer_reference` extension.

### Macro to define buffers references with ease

To make our lives easier, we are going to create a tiny preprocessor macro to easily define buffer reference buffers in our shaders:

```glsl
#define Buffer(Alignment) \
  layout(buffer_reference, std430, buffer_reference_align = Alignment) buffer
```

### Define buffer references

Now, let's use the same buffer layouts that we defined in our previous post with this new approach:

```cpp
Buffer(64) Camera {
  mat4 viewProjection;
  mat4 view;
  mat4 projection;
};

struct TransformData {
  mat4 transform;
};

Buffer(64) Transforms {
  TransformData items[];
};
```

### Use buffer references in uniforms

After we defined our buffers with `buffer_reference` layout, we can use the buffers in our draw parameters uniform:

```
// Draw parameters
layout(set = 1, binding = 0) uniform DrawParameters {
  Transforms meshTransforms;
  PointLights pointLights;
  Camera camera;
  // Don't forget the padding
  uint pad0;
} uDrawParameters;

void main() {
  mat4 transform = uDrawParameters.meshTransforms[0].transform;
  mat4 view = uDrawParameters.camera.view;
}
```

## That's it!

As you can see, buffer device addresses significantly simplifies buffer access management by eliminating the need for descriptors while also providing a more intuitive API in shaders to access them.

This was my first attempt in buffer device addresses. As a next step I am going to try to use buffer device addresses for vertex buffers as well, which can eliminate the need for multiple pipeline layouts when combining it with descriptor indexing my vertex or descriptor layouts can be unified for all pipelines.