Understanding GL_EXT_buffer_reference in GLSL

The GL_EXT_buffer_reference extension introduces true raw pointer semantics to the OpenGL Shading Language (GLSL), allowing shaders to access memory via 64-bit GPU virtual addresses rather than traditional descriptor set bindings. This article explores the architecture of GL_EXT_buffer_reference, explains how it maps directly to physical storage buffers in SPIR-V, demonstrates how to define and dereference typed pointers in GLSL, and highlights the performance benefits it brings to modern graphics pipelines.

What is GL_EXT_buffer_reference?

Historically, graphics APIs required shaders to access GPU buffers (such as uniform buffers or shader storage buffer objects) through explicit binding slots and descriptor sets. While functional, this binding model incurs CPU overhead, limits the number of accessible buffers simultaneously, and prevents shaders from utilizing complex dynamic data structures.

GL_EXT_buffer_reference addresses this limitation by introducing the ability to pass, store, and dereference 64-bit GPU memory addresses (device addresses) directly within shader code. In Vulkan, this extension pairs with the VK_KHR_buffer_device_address API extension and the SPV_KHR_physical_storage_buffer SPIR-V capability. Instead of relying on a descriptor to index into a buffer, the shader receives a raw memory pointer and reads or writes memory directly.

How Pointer Semantics Work in GLSL

The extension implements a typed, C-like pointer model while retaining memory alignment safety.

1. Declaring Buffer Reference Types

To use pointers, you define a block layout marked with the buffer_reference qualifier. This acts like a struct definition in C, establishing both the memory layout and the type:

#extension GL_EXT_buffer_reference : require
#extension GL_ARB_gpu_shader_int64 : require

// Define a typed buffer reference layout
layout(buffer_reference, std430) buffer Node {
    vec4 position;
    uint64_t nextNodeAddress;
};

You can optionally specify memory alignments explicitly using buffer_reference_align = N to satisfy hardware alignment requirements.

2. Casting and Storing Pointers

Addresses can be stored as standard 64-bit integers (uint64_t). To treat an integer address as an accessible data structure, GLSL allows explicit casting between integer types and buffer reference types:

// Cast a 64-bit integer address into a Node pointer
uint64_t rawAddress = ...; // passed via push constant or UBO
Node nodePtr = Node(rawAddress);

You can also store pointers directly within other buffer references, enabling native pointer nesting without manual address arithmetic.

3. Dereferencing

Once a variable is typed as a buffer reference, accessing its members uses standard dot syntax, behaving identically to dereferencing a struct pointer in C++:

// Read data directly from memory
vec4 currentPos = nodePtr.position;

// Follow a pointer to traverse a linked list
if (nodePtr.nextNodeAddress != 0ul) {
    Node nextNode = Node(nodePtr.nextNodeAddress);
    vec4 nextPos = nextNode.position;
}

Practical Benefits in Modern Rendering

By decoupling shader memory access from the descriptor model, GL_EXT_buffer_reference unlocks several critical architectural paradigms:

  • Bindless Architecture: Applications can pack entire scene hierarchies, materials, and mesh representations into memory buffers and traverse them using pointers, drastically reducing CPU-to-GPU draw call overhead.
  • Complex Data Structures: Shaders can natively build and traverse complex dynamic structures such as linked lists, bounding volume hierarchies (BVH), and scene graphs directly on the GPU.
  • Reduced Descriptor Set Churn: Instead of binding hundreds of descriptors across multiple draw calls, a single push constant containing a 64-bit base pointer is often all that is needed to supply a shader with its entire operational dataset.