D3D12 Compute Queue Execution with Graphics Queues
In DirectX 12, D3D12_COMMAND_LIST_TYPE_COMPUTE enables
asynchronous compute execution by processing compute workloads on a
dedicated queue alongside the main direct (graphics) queue. This article
details the underlying multi-engine hardware architecture, how tasks
execute concurrently, the mechanics of explicit synchronization via
fences, and the resource barrier patterns required to avoid race
conditions while maximizing GPU utilization.
Multi-Queue Architecture
DirectX 12 exposes the physical hardware engines of the GPU through separate command queue types:
D3D12_COMMAND_LIST_TYPE_DIRECT(Graphics Queue): Can process draw commands, compute shaders, and copy operations. It interfaces with both fixed-function hardware (rasterizers, render-target outputs) and unified programmable shader cores.D3D12_COMMAND_LIST_TYPE_COMPUTE(Compute Queue): Bypasses graphics-specific stages entirely, submitting work directly to the GPU's compute engines and programmable cores.D3D12_COMMAND_LIST_TYPE_COPY(Copy Queue): Interfaces with DMA engines for memory transfers, operating independently of compute and graphics pipelines.
By segregating these pipelines, applications can prevent long-running compute workloads from stalling critical rendering passes.
Hardware Concurrency (Async Compute)
Modern GPUs contain multiple hardware schedulers feeding unified arrays of compute units or streaming multiprocessors. During traditional rendering, graphics commands often fail to saturate all available execution units, creating pipeline bubbles during memory-bound operations, low-geometry draw calls, or fixed-function stages like rasterization.
When a command list of type
D3D12_COMMAND_LIST_TYPE_COMPUTE is executed on a compute
queue, the hardware scheduler interleaves its thread groups
(wavefronts/warps) into idle execution slots alongside active graphics
work. This simultaneous execution maximizes arithmetic unit occupancy,
memory bandwidth usage, and overall throughput without increasing total
frame time.
Cross-Queue Synchronization Using Fences
Because graphics and compute queues execute asynchronously without
implicit ordering, developers must manage dependencies explicitly using
ID3D12Fence.
- Signaling Work Completion: The producer queue
submits work and signals a fence value via
ID3D12CommandQueue::Signal(pFence, Value). - Waiting for Execution: The consumer queue stalls
its execution pipeline until that value is reached using
ID3D12CommandQueue::Wait(pFence, Value).
Crucially, ID3D12CommandQueue::Wait executes on the GPU.
It does not block the CPU thread submitting the commands, allowing CPU
record-and-submit loops to continue uninterrupted.
Resource State Transitions Across Queues
Resources shared between compute and graphics queues require cross-queue synchronization and state transitions:
- Queue Ownership Transfers: To modify a resource on
a compute queue that was previously bound to a graphics queue, the
resource must transition to
D3D12_RESOURCE_STATE_COMMON. The sending queue issues a transition barrier ending inCOMMON, and the receiving queue issues a transition barrier starting fromCOMMONto the desired state (e.g.,D3D12_RESOURCE_STATE_UNORDERED_ACCESS). - Simultaneous Read Access: If a resource is used
purely for read access (such as
D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE) across both queues, and the resource was created with shared access flags, both engines can read from the resource concurrently without requiring explicit ownership transfer barriers, provided no writes occur simultaneously.
Optimal Workload Pairings
To achieve measurable speedups, paired workloads should utilize complementary hardware resources:
- Compute on Compute Queue: Depth downsampling, screen-space ambient occlusion (SSAO), clustered light culling, physics simulation, or skinning.
- Graphics on Direct Queue: Shadow map generation, early Z pre-passes, or base geometry rasterization.
Running two compute-heavy, cache-thrashing passes simultaneously will degrade performance due to cache eviction and register starvation. Effective utilization relies on pairing memory-latency-heavy tasks with compute-bound workloads.