Skip to content

Buffer Device Address (GPU Pointers)

This example shows how to use buffer device addresses (also called "GPU pointers") to access buffers directly in shaders without descriptor sets. Traditional Vulkan requires binding buffers through descriptor sets. Buffer device address allows querying a 64-bit GPU virtual address and passing it to shaders via push constants or other buffers. This enables advanced techniques like GPU-driven rendering, dynamic resource selection, and raytracing acceleration structures.

The example uses the KDGpuExample helper API for simplified setup.

Overview

What this example demonstrates:

  • Enabling VK_KHR_buffer_device_address feature
  • Querying 64-bit GPU addresses for buffers
  • Passing addresses to shaders via push constants
  • Using GLSL buffer reference syntax for pointer dereferencing
  • Accessing buffer data without descriptor sets

Use cases:

  • GPU-driven rendering (indirect draws with GPU-selected resources)
  • Ray tracing (acceleration structure references)
  • Mesh shading (direct vertex/index buffer access)
  • Sparse data structures (linked lists, trees on GPU)
  • Dynamic resource selection without descriptor indexing

Vulkan Requirements

  • Vulkan Version: 1.2+ (buffer device address promoted to core)
  • Extensions: VK_KHR_buffer_device_address (core in 1.2)
  • Features: bufferDeviceAddress
  • Shader: SPIR-V 1.5+ or GLSL 460+ with GL_EXT_buffer_reference

Key Concepts

Traditional Buffer Access:

1
2
3
// CPU: Bind buffer via descriptor set
bindDescriptorSet(descriptorSetContainingBuffer);
draw();
1
2
3
4
5
6
// Shader: Access via descriptor binding
layout(set = 0, binding = 0) buffer Data {
    vec4 values[];
} data;

vec4 value = data.values[index];

Limitations:

  • Fixed number of bindings per set
  • Descriptor set updates required
  • Cannot dynamically select buffers at runtime

Buffer Device Address:

1
2
3
4
5
6
// CPU: Query GPU address
uint64_t address = buffer.getAddress();

// Pass to shader via push constant
pushConstant(address);
draw();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Shader: Use buffer reference (pointer)
layout(buffer_reference, scalar) buffer Data {
    vec4 values[];
};

layout(push_constant) uniform PushData {
    Data dataPtr;  // 64-bit GPU pointer
};

vec4 value = dataPtr.values[index];  // Dereference pointer

Benefits:

  • No descriptor set needed
  • Unlimited buffer references
  • Runtime buffer selection
  • Required for ray tracing

Spec: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_KHR_buffer_device_address.html

Buffer Reference Syntax:

GLSL buffer references are similar to C pointers:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#extension GL_EXT_buffer_reference : require

layout(buffer_reference, scalar, buffer_reference_align = 16) buffer MyData {
    vec4 color;
    mat4 transform;
};

layout(push_constant) uniform Push {
    MyData dataPtr;
} push;

void main() {
    vec4 color = push.dataPtr.color;  // Dereference
}
  • buffer_reference: Declares a buffer reference type (pointer type)
  • scalar: Use scalar layout (C-like, no padding)
  • buffer_reference_align: Minimum alignment (typically 16 bytes)

Safety Considerations:

Buffer device addresses are raw pointers:

  • No bounds checking - accessing out of bounds is undefined behavior
  • No lifetime tracking - using freed buffer address crashes
  • No validation - driver cannot catch errors easily
  • Programmer responsible for correctness

Implementation

Feature Check:

1
2
3
4
5
6
    // Check that our device actually supports the Vulkan Descriptor Indexing features
    const AdapterFeatures &features = m_device.adapter()->features();
    if (!features.bufferDeviceAddress) {
        SPDLOG_CRITICAL("Buffer Device Address is not supported, can't run this example");
        exit(0);
    }

Filename: buffer_reference/buffer_reference.cpp

Always verify feature support before use. Missing feature causes validation errors or crashes.

Creating Addressable Buffer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    // Create a buffer that can be referenced by its address that will hold vertex colors
    {
        const DeviceSize dataByteSize = 3 * sizeof(glm::vec4);
        const BufferOptions bufferOptions = {
            .size = dataByteSize,
            .usage = BufferUsageFlagBits::StorageBufferBit | BufferUsageFlagBits::ShaderDeviceAddressBit,
            .memoryUsage = MemoryUsage::CpuToGpu
        };
        m_vertexColorsBuffer = m_device.createBuffer(bufferOptions);
    }

Filename: buffer_reference/buffer_reference.cpp

The ShaderDeviceAddressBit flag enables address queries. Without this flag, getDeviceAddress() returns error.

Querying GPU Address:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    // Create Push Constant that will hold the address of our vertexColorBuffer
    m_pushConstants = PushConstantRange{
        .offset = 0,
        .size = sizeof(BufferDeviceAddress),
        .shaderStages = ShaderStageFlagBits::VertexBit,
    };

    // Create a pipeline layout (array of bind group layouts)
    const PipelineLayoutOptions pipelineLayoutOptions = {
        .bindGroupLayouts = {},
        .pushConstantRanges = { m_pushConstants }
    };
    m_pipelineLayout = m_device.createPipelineLayout(pipelineLayoutOptions);

Filename: buffer_reference/buffer_reference.cpp

Returns a 64-bit virtual GPU address. This address is valid for the buffer's lifetime.

Push Constants with Address:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
    auto opaquePass = commandRecorder.beginRenderPass(RenderPassCommandRecorderOptions{
            .colorAttachments = {
                    {
                            .view = m_swapchainViews.at(m_currentSwapchainImageIndex),
                            .clearValue = { 0.3f, 0.3f, 0.3f, 1.0f },
                            .finalLayout = TextureLayout::PresentSrc,
                    },
            },
            .depthStencilAttachment = {
                    .view = m_depthTextureView,
            },
    });
    opaquePass.setPipeline(m_pipeline);
    opaquePass.setVertexBuffer(0, m_vertexBuffer);
    // Push Constant
    const BufferDeviceAddress vertexColorBufAddress = m_vertexColorsBuffer.bufferDeviceAddress();
    opaquePass.pushConstant(m_pushConstants, &vertexColorBufAddress);
    // Draw
    opaquePass.draw(DrawCommand{ .vertexCount = 3 });
    opaquePass.end();

Filename: buffer_reference/buffer_reference.cpp

Push constants are ideal for passing addresses (8 bytes). The render function queries the buffer device address and passes it to the shader via this push constant. No descriptor set binding is needed - the address is passed directly.

Performance Notes

Benefits:

  • CPU: Eliminates descriptor set creation/updates for dynamic buffers
  • CPU: Reduces driver overhead for frequent buffer changes
  • Memory: No descriptor pool memory needed

Costs:

  • GPU: Pointer dereferencing may have small overhead vs. descriptors
  • Cache: Random access patterns may reduce cache efficiency

Best Practices:

  • Align buffer data to 16 bytes for best performance
  • Keep hot data tightly packed (cache-friendly)
  • Validate addresses in debug builds
  • Use for dynamic selection; descriptors still good for static bindings

Ray Tracing Requirement:

  • Acceleration structures require buffer device address
  • Shader binding table uses device addresses
  • Essential feature for ray tracing

See Also

Further Reading


Updated on 2026-03-31 at 00:02:07 +0000