DirectX Occlusion Queries and CPU Visibility Systems

Occlusion queries are specialized GPU commands that determine whether geometry is visible on screen by counting how many rasterized samples pass the depth test. In DirectX-driven engines, these queries test lightweight bounding proxies against the depth buffer and asynchronously stream the visibility results back to the CPU. By reading these results without stalling the rendering pipeline, CPU-side visibility systems can intelligently cull entire scene-graph nodes, suppress draw calls, and bypass expensive state changes for hidden objects in subsequent frames.

What Are Occlusion Queries?

An occlusion query is a GPU-side testing mechanism exposed by modern graphics APIs, including Direct3D 11 and Direct3D 12. Instead of evaluating full geometric meshes with complex pixel shaders, the engine renders a coarse bounding volume—typically an axis-aligned bounding box (AABB) or an oriented bounding box (OBB)—wrapped inside an occlusion query scope.

During rasterization, color writes and depth writes are disabled while the depth test remains enabled. The GPU’s rasterizer and depth/stencil hardware count the number of fragments or samples that pass the depth test.

In Direct3D 11, this is represented by an ID3D11Query created with D3D11_QUERY_OCCLUSION (which returns the exact number of visible samples) or D3D11_QUERY_OCCLUSION_PREDICATE (which returns a boolean indicating whether any samples passed). In Direct3D 12, queries are allocated from an ID3D12QueryHeap using the D3D12_QUERY_HEAP_TYPE_OCCLUSION descriptor.

If zero samples pass the depth test, the proxy volume is completely concealed behind previously rendered geometry (such as terrain or room walls), meaning the actual high-detail object inside that volume is also occluded.

The CPU-GPU Asynchrony Problem

The primary challenge in utilizing occlusion queries for CPU-side culling is pipeline latency. The CPU typically generates draw calls several frames ahead of the GPU's execution.

If the CPU demands the result of an occlusion query immediately after issuing it, the graphics driver must flush the command queue and force the CPU to wait until the GPU finishes rendering all prior work up to the query point. This CPU stall eliminates pipeline parallelism and severely degrades frame rates.

To solve this, visibility systems treat occlusion queries as asynchronous readback operations, trading absolute frame-accurate precision for high throughput.

Feeding Query Data Back to the CPU

DirectX applications feed occlusion data back to the CPU through decoupled readback pipelines, differing slightly across API generations:

Direct3D 11 Polling

In Direct3D 11, the CPU checks the status of an active query using ID3D11DeviceContext::GetData. To prevent stalls, the engine passes the D3D11_ASYNC_GETDATA_DONOTFLUSH flag:

  • If the GPU has completed the query, GetData returns S_OK along with a 64-bit integer representing the visible sample count.
  • If the GPU is still processing earlier commands, GetData returns S_FALSE, instructing the CPU to leave the query open and retain the object’s prior visibility state for the current frame.

Direct3D 12 Asynchronous Readback Heaps

Direct3D 12 provides explicit, lower-overhead control:

  1. Query Resolution: Occlusion counts inside the ID3D12QueryHeap are copied to a GPU buffer resource using ID3D12GraphicsCommandList::ResolveQueryData.
  2. Buffer Copy: The resolved data is copied from default GPU memory to a readback buffer (D3D12_HEAP_TYPE_READBACK).
  3. Fence Tracking: The engine signals an ID3D12Fence once the copy command finishes on the GPU queue.
  4. CPU Access: The CPU checks if the fence value has reached the target threshold. Once signaled, the CPU calls Map on the readback buffer to directly read the array of visibility results.

Because reads occur asynchronously across an N-frame buffer ring (often with a 1- to 2-frame delay), the CPU never blocks waiting on the GPU.

Integrating Query Results into CPU Visibility Systems

Once the sample counts reach CPU memory, the visibility management system applies the data to optimize the scene graph and render loop:

Exploiting Temporal Coherence

Camera and object movements are continuous from frame to frame. Because of this high temporal coherence, an object occluded in frame \(N-1\) is extremely likely to remain occluded in frame \(N\).

The CPU maintains a persistent visibility record for each entity:

  • Visible Objects: The engine issues full draw calls and simultaneously issues an occlusion query with their bounding proxy.
  • Occluded Objects: The engine skips the complex draw calls, avoiding animation updates, material binds, and pipeline state changes. Only their bounding proxies are submitted to the query queue to verify if they have moved into view.

Latency and Popping Mitigation

When an occluded object suddenly becomes visible (e.g., an entity emerging from behind a wall), a latency-delayed query might keep it culled for an extra frame, leading to visual "popping." Robust CPU visibility systems counter this by:

  • Inflating bounding boxes along the camera’s velocity vector to trigger "visible" states before objects cross occlusion edges.
  • Retesting previously occluded items across hierarchical spatial trees (e.g., BVH or Octrees). If an entire tree node's query passes zero samples, all child nodes are culled simultaneously on the CPU without issuing separate individual queries.