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.

  1. Signaling Work Completion: The producer queue submits work and signals a fence value via ID3D12CommandQueue::Signal(pFence, Value).
  2. 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 in COMMON, and the receiving queue issues a transition barrier starting from COMMON to 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.