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 InstanceCount field of a batched DrawIndexedInstanced command 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.