DirectX 12 ExecuteIndirect for GPU-Driven Culling
In modern real-time rendering, shifting visibility determination and
draw call generation from the CPU to the GPU drastically minimizes
synchronization stalls and API overhead. This article explains how the
DirectX 12 method
ID3D12GraphicsCommandList::ExecuteIndirect serves as the
critical bridge for GPU-driven rendering pipelines, enabling
compute-based frustum and occlusion culling, dynamic draw generation,
and massive draw call compaction entirely within GPU memory.
The Limitation of CPU-Bound Culling
Traditional rendering architectures perform scene traversal, bounding
volume intersection tests, and visibility culling on the CPU. Once the
visible set of meshes or instances is determined, the CPU records
individual draw calls—such as DrawIndexedInstanced—into a
command list.
This approach introduces severe performance bottlenecks:
- CPU Overhead: Processing hundreds of thousands of individual instances exhausts CPU cycles.
- Latency and Stalls: Efficient occlusion culling requires depth buffer data from the GPU. Reading depth data back to the CPU introduces multi-frame latency or costly pipeline stalls.
- Static Allocation: The CPU must issue draw calls based on conservative estimates or outdated visibility information to keep the GPU fed.
How ExecuteIndirect Solves the Problem
ID3D12GraphicsCommandList::ExecuteIndirect allows the
GPU to read its own generated data to execute draw, dispatch, or root
parameter state changes. Instead of receiving explicit rendering
parameters from the CPU, the GPU reads command arguments directly from
an application-defined buffer (ID3D12Resource) located in
VRAM.
By utilizing D3D12_COMMAND_SIGNATURE_DESC, developers
can define indirect commands containing draw arguments
(D3D12_DRAW_INDEXED_ARGUMENTS), dispatch parameters, or
dynamic resource bindings (root constants, descriptor tables, and
vertex/index buffer views).
The GPU-Driven Culling Pipeline
Using ExecuteIndirect to power culling transforms the
rendering loop into a self-contained GPU workflow executed in three main
phases:
1. Compute-Based Visibility Testing
Before rendering, a compute shader processes scene instances in parallel. Each instance's bounding box is tested against the current camera view:
- Frustum Culling: Testing oriented bounding boxes (OBBs) or axis-aligned bounding boxes (AABBs) against the six camera view planes.
- Occlusion Culling (Hi-Z): Projecting bounding volumes onto a Hierarchical Z-Buffer (depth pyramid) generated from the previous or current frame to discard fully hidden geometry.
- Dynamic LOD Selection: Selecting the appropriate mesh detail level based on distance and screen-space size within the same pass.
2. Argument Buffer Construction
Instances that survive the visibility tests are written directly into an indirect argument buffer. A common approach involves:
- Maintaining a global atomic counter in an unordered access view (UAV) buffer to track the number of visible instances.
- Storing per-instance draw arguments sequentially, compacting the data so only visible geometry consumes draw commands.
- Alternatively, updating the
InstanceCountfield of a batchedDrawIndexedInstancedcommand structure directly.
3. Execution via the Command List
Once the compute shader completes and a resource barrier transitions
the argument buffer to
D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT, the application
records ExecuteIndirect:
m_commandList->ExecuteIndirect(
m_commandSignature.Get(),
maxCommandCount,
m_argumentBuffer.Get(),
argumentBufferOffset,
m_countBuffer.Get(),
countBufferOffset
);By providing an optional m_countBuffer, the application
instructs the hardware to read the exact number of commands to execute
directly from a GPU counter. The CPU specifies only the maximum possible
command count (maxCommandCount), completely eliminating the
need for the CPU to know how many meshes survived the culling
process.
Core Advantages
- Zero CPU-to-GPU Feedback Latency: Occlusion culling operates on the most up-to-date GPU depth representations without requiring CPU readbacks.
- Massive Draw Call Reduction: Geometry processing scales with visible complexity rather than total scene complexity. Hidden objects are culled before entering the input assembler and vertex processing stages.
- Pipeline Cohesion: Geometry generation, LOD selection, visibility culling, and final drawing execution reside entirely within GPU execution queues, freeing CPU threads for physics, AI, and game logic.