Buffer Reference and 64-Bit Integer Casting in GLSL

The GL_EXT_buffer_reference extension introduces C-style raw pointer semantics to GLSL, allowing shaders to access memory through GPU virtual addresses. A fundamental mechanism of this extension is the bidirectional casting between buffer reference types and 64-bit unsigned integers (uint64_t). This article explains the syntax for casting between addresses and buffer references, how to implement pointer arithmetic using integer conversions, and the alignment constraints necessary to prevent undefined behavior.

Prerequisites and Type Declarations

To use buffer references and 64-bit integer types, shaders must enable both GL_EXT_buffer_reference and an extension that provides 64-bit integer support, such as GL_EXT_gpu_shader_int64 or GL_ARB_gpu_shader_int64:

#extension GL_EXT_buffer_reference : require
#extension GL_EXT_gpu_shader_int64 : require

A buffer reference is declared similarly to a standard uniform or shader storage buffer object (SSBO), but with the buffer_reference layout qualifier:

layout(buffer_reference, std430) buffer Node {
    int data;
    uint64_t nextNodeAddress;
};

Under the hood, any variable of type Node occupies 64 bits of storage, representing a memory address pointing to an instance of that block layout.

Casting Mechanisms

Casting between buffer references and 64-bit integers relies on GLSL constructor-style explicit type conversions.

Reference to 64-Bit Integer

To retrieve the raw 64-bit memory address from a buffer reference variable, cast the reference directly to uint64_t:

Node myNode = ...;
uint64_t address = uint64_t(myNode);

This operation yields the physical GPU virtual address where the referenced structure resides. It allows shader code to serialize references into standard data fields or transmit addresses between shaders.

64-Bit Integer to Reference

Conversely, a raw 64-bit unsigned integer can be cast back into a typed buffer reference using the reference type's name as a constructor:

uint64_t rawAddress = ...;
Node reconstitutedNode = Node(rawAddress);

// Accessing fields through the reconstituted reference
int value = reconstitutedNode.data;

Once cast, the variable behaves as a pointer to memory formatted according to the layout specified in the reference block definition.

Pointer Arithmetic Emulation

GLSL does not support native pointer arithmetic operators (such as +, -, ++, or --) directly on buffer reference types. To traverse contiguous arrays or offset memory manually, operations must be performed using integer arithmetic:

  1. Cast the buffer reference to uint64_t.
  2. Add or subtract the offset measured strictly in bytes.
  3. Cast the calculated address back to the target buffer reference type.
layout(buffer_reference, std430) buffer Element {
    float value;
};

Element getElementAtIndex(Element basePtr, uint index) {
    uint64_t baseAddr = uint64_t(basePtr);
    uint64_t byteOffset = uint64_t(index) * sizeof(float); // 4 bytes per float
    return Element(baseAddr + byteOffset);
}

Alignment and Dereferencing Constraints

While casting between arbitrary 64-bit integers and buffer references is syntactically valid, dereferencing the resulting pointer imposes strict requirements:

  • Natural Alignment: By default, buffer references assume an alignment of 16 bytes. If an address converted from a uint64_t does not meet this alignment, dereferencing it causes undefined behavior.
  • Explicit Alignment (buffer_reference_align): Shaders can specify custom alignments using the buffer_reference_align layout qualifier:
layout(buffer_reference, std430, buffer_reference_align = 4) buffer CompactData {
    int value;
};

When converting a uint64_t to CompactData, the address must be a multiple of 4 bytes before any read or write operation takes place.

  • Null Pointers: An address value of 0 is treated as a null pointer. Casting 0 to a buffer reference is legal, but attempting to read from or write to a null reference results in undefined behavior or GPU faults.