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:
- The application creates a single continuous, shader-visible descriptor heap containing all texture or buffer descriptors for the entire frame or scene.
- The root signature defines a descriptor table that covers this unbounded range.
- 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.