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:
- Release Barrier: Recorded on the releasing queue
(e.g., transitioning from
D3D12_RESOURCE_STATE_UNORDERED_ACCESStoD3D12_RESOURCE_STATE_COMMON). - Acquire Barrier: Recorded on the acquiring queue
(e.g., transitioning from
D3D12_RESOURCE_STATE_COMMONtoD3D12_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
Waiton 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_ONLYand end it just before use withD3D12_RESOURCE_BARRIER_FLAG_END_ONLYto 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.