Unbounded Descriptors and DX12 Heap Exhaustion
This article explains how unbounded descriptor ranges resolve descriptor heap exhaustion in DirectX 12. In traditional DirectX 12 rendering pipelines, managing dynamic resource bindings per draw call requires constantly copying descriptors into GPU-visible descriptor heaps, frequently leading to fragmentation and heap exhaustion. By leveraging unbounded descriptor ranges alongside a bindless rendering paradigm, developers can allocate resources persistently and index them directly from shaders, removing the continuous churn that depletes heap space.
The Problem of Descriptor Heap Exhaustion
In standard DirectX 12 implementations, a shader specifies static arrays or single slots for resources such as Shader Resource Views (SRVs) and Unordered Access Views (UAVs). To bind these to the pipeline, the CPU must allocate and populate descriptor tables inside a shader-visible descriptor heap.
Because draw calls often require different sets of resources, engines typically use dynamic descriptor sub-allocation. This involves allocating temporary descriptor ranges out of a ring buffer in the descriptor heap for every draw call or frame. Under heavy draw-call counts or complex material systems, this constant churning creates severe drawbacks:
- Ring Buffer Saturation: Rapidly copying descriptors every frame can consume the available descriptor heap before the GPU finishes executing previous frames.
- Heap Fragmentation: Managing varying table sizes dynamically introduces allocation overhead and fragmentation.
- API Limits: Hardware limits the maximum number of descriptors in a single shader-visible heap (often 1,000,000 or fewer depending on the hardware tier), which dynamic schemes can rapidly deplete.
How Unbounded Descriptor Ranges Work
An unbounded descriptor range allows a shader to declare a resource array without specifying a fixed size at compile time. In HLSL, this is expressed using empty brackets:
Texture2D g_Textures[] : register(t0, space0);
In the root signature, the descriptor range size is set to
UINT_MAX. This indicates to the driver that the range can
encompass an arbitrary number of descriptors up to the total capacity of
the descriptor heap, rather than a fixed slice bounded to a specific
draw call.
Eliminating Heap Exhaustion
Unbounded descriptor ranges eliminate heap exhaustion by fundamentally changing how resources are bound and managed:
- Persistent Resource Lifetime: Instead of writing descriptors dynamically per draw call, descriptors are written to the shader-visible heap once—typically when the resource is loaded or created. They remain in their designated slot for their entire lifecycle.
- True Bindless Indexing: Because the entire heap can be referenced by the unbounded array, shaders access specific resources using an index (such as an integer passed via a root constant, instance buffer, or material ID). The pipeline binds the entire heap once per frame or pass, eliminating the need to allocate new descriptor tables during rendering.
- Zero Dynamic Descriptor Allocation: Because descriptors do not need to be duplicated or repackaged into smaller contiguous tables for individual draw calls, runtime heap allocations are reduced to zero during the render loop.
- Fragmentation Removal: Descriptors occupy stable slots, meaning the memory layout within the heap remains predictable. Unused slots can be managed with a simple free-list allocator rather than complex ring buffers or defragmentation schemes.
By replacing dynamic table generation with static global residency and runtime indexing, unbounded descriptor ranges allow an application to scale to hundreds of thousands of resources while staying well within DirectX 12 descriptor heap limits.