DirectX 12 Descriptor Heaps and Tables Explained

In DirectX 12, explicit GPU resource binding is achieved through descriptors, descriptor heaps, and descriptor tables, replacing the implicit driver-managed state mechanisms of earlier graphics APIs. This article explains how these three components work together to expose buffers, textures, and samplers to GPU shaders, how memory is allocated and partitioned within heaps, and how root signatures leverage descriptor tables to optimize pipeline binding and maximize rendering performance.

Descriptors: The Fundamental Units of Binding

A descriptor (historically referred to as a "resource view") is a relatively small, opaque data block—typically 32 to 64 bytes—that fully defines a resource to the GPU hardware. Rather than passing an entire resource object directly to the pipeline, the application passes a descriptor that contains:

  • The GPU virtual address of the resource.
  • The data format (e.g., DXGI_FORMAT_R8G8B8A8_UNORM).
  • Dimensionality and structural layout (e.g., 2D texture, buffer, mip levels, array slices).
  • Resource type specifications, such as Constant Buffer Views (CBV), Shader Resource Views (SRV), Unordered Access Views (UAV), Samplers, Render Target Views (RTV), and Depth Stencil Views (DSV).

Descriptors cannot reside in standard CPU system memory when read by shaders; they must be stored in contiguous GPU-visible allocations.

Descriptor Heaps: The Backing Memory

A descriptor heap is a dedicated, contiguous array of descriptors allocated directly in GPU memory. It functions similarly to a custom memory pool specifically designed for descriptor allocations.

Descriptor heaps are categorized by the types of descriptors they store:

  1. CBV/SRV/UAV Heaps: Store constant buffers, shader resources (textures and buffers), and read-write unordered access resources. These can be configured as shader-visible.
  2. Sampler Heaps: Store sampler states. Samplers are kept in separate heaps due to hardware differences in sampler units on many GPU architectures.
  3. RTV Heaps: Store render target views for output merger binding. These are non-shader-visible.
  4. DSV Heaps: Store depth/stencil views for the output merger. These are also non-shader-visible.

Descriptor heaps are either Shader-Visible or Non-Shader-Visible. Non-shader-visible heaps exist in system or GPU memory to stage descriptors on the CPU (such as RTVs or staging pools). Shader-visible heaps are bound to the command list using SetDescriptorHeaps, allowing shader units to fetch descriptors directly during execution. Only one CBV/SRV/UAV heap and one Sampler heap can be bound to the pipeline concurrently.

Descriptor Tables: Slicing the Heap

A descriptor table is not an allocation of memory; rather, it is a subrange, or view, into an existing descriptor heap.

Because a shader-visible heap can contain thousands of descriptors, binding the entire heap directly to individual shader stages is inefficient. Instead, descriptor tables designate contiguous slices of a heap by specifying an offset (base descriptor handle) and a length.

For example, an application may organize a single CBV/SRV/UAV heap containing:

  • Indices 0–99: Material textures (SRVs).
  • Indices 100–149: Dynamic per-frame constant buffers (CBVs).
  • Indices 150–199: Compute dispatch storage buffers (UAVs).

A descriptor table can then be defined to point specifically to index 0 with a length of 10 to expose 10 material textures to a pixel shader, while another descriptor table points to index 100 with a length of 2 to provide constant buffers to the vertex shader.

Integration via the Root Signature

The root signature acts as the parameter contract between the application code and the shader stages. It defines what data the shaders expect, arranged in root parameters:

  • Root Constants: Data pushed directly into root registers (fastest, smallest space).
  • Root Descriptors: Pointers bound directly to a single buffer (CBV, SRV, or UAV) without utilizing a descriptor heap.
  • Descriptor Tables: Pointers to ranges inside a bound descriptor heap.

When a root parameter is configured as a descriptor table, the root signature merely holds a 64-bit GPU descriptor handle representing the start of that table. During a draw or dispatch call:

  1. The application binds the master descriptor heap using ID3D12GraphicsCommandList::SetDescriptorHeaps.
  2. The application sets the root table parameter using SetGraphicsRootDescriptorTable (or compute equivalent), providing the GPU handle pointing to the desired offset within that heap.
  3. The GPU shader dereferences the table pointer to index into the heap and read the resource descriptors with minimal driver intervention.

Architectural Benefits

By decoupling descriptor allocation (heaps) from pipeline exposure (tables), DirectX 12 achieves:

  • Reduced Driver Overhead: Descriptor validation occurs at creation time on the CPU rather than dynamically per draw call.
  • Cache Efficiency: Grouping frequently used descriptors contiguously in GPU cache-friendly structures reduces cache misses during shader execution.
  • Bindless Rendering: Because descriptor tables allow dynamic indexing, shaders can index descriptors using variables (e.g., material IDs stored in a vertex buffer), enabling modern bindless rendering workflows where thousands of resources are accessed dynamically without CPU-side state changes.