Role of uvec2 Handles in GLSL Bindless Textures

Bindless texturing in OpenGL eliminates the CPU overhead of binding state to fixed texture units by representing textures directly through unique 64-bit integer handles. In GLSL, the uvec2 type serves as a fundamental, cross-platform container used to store, pass, and unpack these 64-bit handles, especially when native 64-bit integer support is absent or restricted. This article breaks down why the two-component 32-bit unsigned integer vector is utilized, how it interfaces with buffer layouts, and the methods used to cast it back into usable texture samplers.

The Need for 64-Bit Handles

Standard OpenGL texturing requires binding a texture to an active texture image unit before a draw call. The GL_ARB_bindless_texture extension bypasses this bottleneck. Instead of binding to a slot, the driver generates a globally unique 64-bit handle using glGetTextureHandleARB(). This handle contains all internal metadata required by the GPU to sample the texture, allowing applications to store arrays of thousands of texture pointers in buffers.

Because these handles are 64 bits wide, the shader needs a representation that accommodates this memory footprint.

Why uvec2 Represents 64-Bit Handles

In standard GLSL, native 64-bit integers (uint64_t) are not part of the core language and require the GL_ARB_gpu_shader_int64 extension. Not all hardware or driver implementations support 64-bit integer arithmetic in shaders, even if they support bindless texturing.

The uvec2 type (a two-component vector of standard 32-bit unsigned integers) provides a universal solution:

  • Bit-Width Matching: A uvec2 consists of two 32-bit integers, totaling exactly 64 bits (8 bytes), which matches the width of a GLuint64 handle.
  • Buffer Alignment: Under standard GLSL layout rules (such as std140 and std430), a uvec2 is 8-byte aligned. This matches the native 8-byte alignment of 64-bit pointers on modern GPU architectures, preventing padding and alignment mismatches in Uniform Buffer Objects (UBOs) and Shader Storage Buffer Objects (SSBOs).
  • Portability: Shaders can ingest handles through buffers as uvec2 arrays across virtually all modern hardware without declaring optional 64-bit integer extensions.

Unpacking and Sampler Reconstruction

The GL_ARB_bindless_texture extension overloads GLSL sampler constructors to accept uvec2 representations directly, as well as uint64_t values.

When a handle is stored in a buffer as a uvec2, the lower 32 bits typically reside in the .x component and the upper 32 bits in the .y component (depending on system endianness). The GLSL extension provides explicit constructor functions that interpret this layout:

#version 450
#extension GL_ARB_bindless_texture : require

layout(std430, binding = 0) readonly buffer MaterialData {
    uvec2 textureHandles[];
};

layout(location = 0) in vec2 v_TexCoord;
layout(location = 0) out vec4 o_Color;

void main() {
    // Retrieve the 64-bit handle as a uvec2
    uvec2 handle = textureHandles[gl_DrawID];

    // Reconstruct the sampler directly from the uvec2 handle
    sampler2D mySampler = sampler2D(handle);

    // Sample the texture normally
    o_Color = texture(mySampler, v_TexCoord);
}

If GL_ARB_gpu_shader_int64 is also enabled, functions such as packUint2x32(uvec2) and unpackUint2x32(uint64_t) can convert between a single uint64_t and uvec2, but direct construction of sampler types via sampler2D(uvec2) removes the need for explicit arithmetic packing.

Summary

The uvec2 type acts as a universal 64-bit payload in GLSL. It guarantees correct 8-byte memory layout in memory buffers, circumvents the hardware requirement for native 64-bit integer math pipelines, and directly feeds the sampler conversion constructors defined by bindless texturing extensions.