DirectX 12 Compute Queue Resource Transition Overhead

Using asynchronous compute alongside a direct (graphics) queue in DirectX 12 is a primary technique for maximizing GPU saturation, but sharing resources between these queues introduces significant transition overhead. When a resource switches context between a direct queue and a compute queue, the driver and hardware must execute cross-queue resource barriers, enforce cache-flushing operations, and coordinate GPU-level synchronization. If managed inefficiently, this transition overhead can introduce pipeline bubbles, stall hardware engines, and completely eliminate the performance benefits of asynchronous compute.

The Mechanics of Cross-Queue Transitions

In DirectX 12, transferring a resource from one command queue to another requires an explicit ownership transfer. This is not a standard state transition; it involves two distinct barriers across the command lists:

  1. Release Barrier: Recorded on the releasing queue (e.g., transitioning from D3D12_RESOURCE_STATE_UNORDERED_ACCESS to D3D12_RESOURCE_STATE_COMMON).
  2. Acquire Barrier: Recorded on the acquiring queue (e.g., transitioning from D3D12_RESOURCE_STATE_COMMON to D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE).

DirectX 12 does not permit implicit subresource ownership across different queues. The hardware requires the resource to hit the COMMON state to mediate the transfer safely, forcing explicit synchronization via GPU fences between the two queues.

Cache Flushes and Invalidation

The most substantial performance cost of queue transitions stems from cache maintenance operations. Direct and compute engines often use distinct hardware pipelines that may not share coherent L1 caches, and write operations must be made visible globally before the other queue can read the data.

  • Flushing: When the compute queue finishes writing to an Unordered Access View (UAV), the releasing barrier forces the GPU to flush dirty data from the local compute unit (CU) or streaming multiprocessor (SM) caches out to the L2 cache or main VRAM.
  • Invalidation: When the direct queue acquires this resource, its graphics caches (such as texture or L1 caches) must be invalidated to ensure subsequent draw calls do not read stale data.

These flushes introduce latency because memory pipelines must wait for write-backs to complete before proceeding. If a render pipeline relies on frequent transitions back and forth within a single frame, the memory subsystem spends critical cycles performing memory fence operations rather than processing geometry or compute workloads.

Pipeline Stalls and Engine Synchronization

The fundamental goal of asynchronous compute is overlapping compute workloads with graphics workloads, such as running physics, culling, or depth-prepass compute alongside non-saturating rendering tasks. Resource transitions disrupt this concurrency:

  • Wait Fences: The acquiring queue must execute a Wait on a fence signaled by the releasing queue to ensure data integrity. If the compute work takes longer than expected, the direct queue stalls.
  • Pipeline Bubbles: If transitions occur frequently, the GPU hardware schedulers cannot keep execution units saturated. The time spent draining pipelines for a clean transition creates "bubbles"—periods where hardware units sit idle waiting for cache flushes and queue fence resolutions.

Best Practices to Minimize Overhead

To prevent transition overhead from degrading frame rates:

  • Batch Queue Transitions: Avoid fine-grained, alternating dependencies between queues. Aggregate compute work so that a batch of resources transitions once per frame rather than continuously across multiple passes.
  • Use Split Barriers: Initiate the transition process as early as possible using D3D12_RESOURCE_BARRIER_FLAG_BEGIN_ONLY and end it just before use with D3D12_RESOURCE_BARRIER_FLAG_END_ONLY to hide flush latencies behind independent workloads.
  • Buffer Duplication: For small, read-heavy resources, maintaining duplicate buffers (one per queue) can completely remove the need for cross-queue synchronization at the cost of a slight memory footprint increase.
  • Keep Common States Static: Resources that only serve as read-only inputs across both queues should remain persistently in read-compatible states (like NON_PIXEL_SHADER_RESOURCE) to avoid queue-ownership transfers entirely.