Bindless Rendering: HLSL Unbounded Arrays in DX12

Modern graphics architectures rely on bindless rendering to eliminate CPU-side binding bottlenecks and achieve massive draw-call scalability. In DirectX 12 and High-Level Shader Language (HLSL), this paradigm is largely enabled by unbounded resource array syntax, which allows shaders to reference an arbitrary number of descriptors without knowing the array size at compile time. This article explains how declaring unbounded resource arrays in HLSL interacts with DirectX 12 descriptor heaps and dynamic indexing to remove traditional descriptor limits and facilitate efficient, fully bindless rendering pipelines.

The Traditional Binding Limit

In traditional rendering models, shaders bind resources to explicit, hardware-limited slots (such as t0 to t127 in Direct3D 11). This architecture forces the CPU to constantly update and rebind descriptor sets or tables between draw calls when materials, textures, or buffers change. The resulting driver overhead severely limits the number of unique assets a scene can render simultaneously.

DirectX 12 introduced descriptor heaps to store massive numbers of resource descriptors in GPU memory, but standard HLSL syntax initially required explicit array boundaries for resources.

HLSL Unbounded Resource Array Syntax

Shader Model 5.1 introduced unbounded resource arrays in HLSL. By omitting the array size within the brackets, developers can declare an array of indefinite size that aliases a descriptor table or an entire descriptor heap.

Texture2D g_Textures[] : register(t0, space0);
SamplerState g_Samplers[] : register(s0, space0);

The empty brackets ([]) signify that the actual range is undefined at compile time and will be established dynamically at runtime through application code and root signatures. The space keyword defines distinct register spaces, allowing separate arrays of different resource types to exist without overlapping their binding registers.

Bridging HLSL to DirectX 12 Descriptor Heaps

In DirectX 12, an unbounded array in HLSL maps directly to a descriptor table containing a descriptor range set to unbounded (UINT_MAX or a dynamically resolved size).

Instead of binding individual textures for each draw call:

  1. The application creates a single continuous, shader-visible descriptor heap containing all texture or buffer descriptors for the entire frame or scene.
  2. The root signature defines a descriptor table that covers this unbounded range.
  3. At draw time, the CPU binds this single descriptor table once (or uses modern dynamic resource heap indexing).

Dynamic Resource Indexing

Unbounded arrays alone do not complete the bindless design; the shader must also be able to index into them dynamically.

struct MaterialData
{
    uint DiffuseTextureIndex;
    uint NormalTextureIndex;
};

ConstantBuffer<MaterialData> g_Material : register(b0);

float4 PSMain(float2 uv : TEXCOORD) : SV_Target
{
    // Dynamically retrieve the texture index from root constants or a buffer
    Texture2D diffuseTex = g_Textures[g_Material.DiffuseTextureIndex];
    return diffuseTex.Sample(g_Samplers[0], uv);
}

Through Shader Model 5.1 and 6.0, direct scalar or non-uniform expressions can index the array. When dynamic branch conditions or primitive attributes (like a material ID passed via vertex data or meshlets) supply the index, HLSL provides the NonUniformResourceIndex intrinsic to inform the compiler and hardware that different threads within the same wave may read different descriptors:

uint textureID = GetPerPrimitiveTextureID();
float4 color = g_Textures[NonUniformResourceIndex(textureID)].Sample(g_Samplers[0], uv);

Shader Model 6.6 Direct Heap Access

DirectX 12 Agility SDK and Shader Model 6.6 advanced this concept further by formalizing native heap access through ResourceDescriptorHeap and SamplerDescriptorHeap. These built-in system arrays act as globally accessible unbounded arrays representing the entire active descriptor heap:

Texture2D myTexture = ResourceDescriptorHeap[textureIndex];
float4 color = myTexture.Sample(SamplerDescriptorHeap[samplerIndex], uv);

This syntax bypasses root signature descriptor table declarations entirely, relying on direct descriptor heap indexing everywhere in the shader.

Architectural Benefits of the Bindless Approach

Using unbounded resource arrays fundamentally changes how GPU workloads are structured:

  • Elimination of State Changes: The GPU no longer processes command-list interruptions to swap descriptor tables between draw calls.
  • Aggressive Draw Merging: Distinct objects with different materials, textures, and properties can be merged into a single multi-draw or indirect dispatch call, as their resource indices can be fetched from per-instance or per-primitive data.
  • GPU-Driven Rendering: Compute shaders and mesh shaders can cull geometry, select materials, and submit draw arguments directly on the GPU without needing CPU intervention to bind corresponding textures.