How Conditional Rendering Works in DirectX 12
Conditional rendering in DirectX 12 enables the GPU to discard draw
or dispatch calls dynamically based on the outcome of a prior GPU query,
completely eliminating CPU-GPU synchronization bottlenecks. By using
hardware predication through the
ID3D12GraphicsCommandList::SetPredication API, the
rendering pipeline evaluates visibility or execution conditions directly
on the hardware. This approach allows developers to skip rendering
complex geometry—such as objects hidden behind other geometry—without
needing to stall the pipeline to read query results back to the CPU.
The Problem with Traditional Query-Based Culling
In standard occlusion culling pipelines, the CPU issues a simplified bounding-box draw call to test whether an object is visible via an occlusion query. To determine whether the full-detail mesh should be drawn, the CPU traditionally needs to read the query result from GPU memory. This round-trip latency introduces pipeline stalls, forcing the engine to either wait several frames to retrieve the data (introducing latency and visibility popping) or stall the CPU thread waiting for the GPU to finish execution.
The Mechanism of Predication
DirectX 12 solves this via predication, allowing commands recorded into a command list to be conditionally executed or bypassed by the GPU's Command Processor (CP) at runtime. The process functions in four distinct steps:
- Recording Queries: The application issues a
lightweight draw call (such as a bounding box) surrounded by query
markers using
ID3D12GraphicsCommandList::BeginQueryandEndQuerywith a query heap configured forD3D12_QUERY_TYPE_OCCLUSIONorD3D12_QUERY_TYPE_BINARY_OCCLUSION. Binary occlusion is typically preferred for conditional rendering because it only returns a 0 (not visible) or non-zero (visible) 64-bit value, optimizing performance. - Resolving Query Data: The raw query results inside
the query heap are converted into standard buffer data using
ID3D12GraphicsCommandList::ResolveQueryData. This writes the 64-bit visibility results to a designatedID3D12Resourcebuffer on the GPU. - Setting Predication: Before issuing the draw or
dispatch calls for the actual complex object, the CPU records
ID3D12GraphicsCommandList::SetPredication. This method accepts:- A pointer to the buffer resource containing the resolved query result.
- An aligned byte offset to the specific 64-bit query value.
- A predication operation flag
(
D3D12_PREDICATION_OP_EQUAL_ZEROorD3D12_PREDICATION_OP_NOT_EQUAL_ZERO).
- Hardware-Level Bypassing: All subsequent draw,
dispatch, or copy commands recorded in the command list will reference
this predication status until
SetPredicationis called again withnullptr.
GPU-Side Execution Flow
When the GPU reaches the predicated commands in the command buffer:
- The Command Processor fetches the 64-bit integer stored at the designated buffer offset.
- If
D3D12_PREDICATION_OP_EQUAL_ZEROis specified, the GPU executes the subsequent commands only if the query result is equal to zero (meaning no pixels passed the depth test). - If
D3D12_PREDICATION_OP_NOT_EQUAL_ZEROis specified, the GPU executes the commands only if the value is non-zero (meaning at least one pixel passed the depth test and the object is visible). - If the predication test fails, the Command Processor discards the draw command. No vertex shaders, pixel shaders, or rasterization states are scheduled or executed for that call, saving substantial vertex and fragment processing overhead.
Performance Considerations
Because predication operates at the Command Processor level, commands
are skipped with minimal overhead. However, the command packets still
travel through the GPU's front-end pipeline. While conditional rendering
effectively prevents expensive geometry and pixel shading workloads on
hidden objects, drawing millions of tiny, empty predicated packets can
still create a minor bottleneck in front-end command processing. For
massive scene hierarchies, conditional rendering is best paired with
coarse CPU-side frustum culling or compute-based GPU-driven pipelines
(via ExecuteIndirect) to maximize throughput.