How DirectX 12 Split Barriers Hide GPU Latency
DirectX 12 split barriers optimize GPU pipeline performance by decoupling the initiation and completion of resource state transitions. Unlike traditional instant barriers that force the GPU to stall until a transition completes, split barriers divide the operation into two distinct phases using dedicated flags. This article explains the mechanics of split barriers, how they decouple resource availability, and how developers can interleave independent workloads between barrier phases to completely absorb transition latency and prevent pipeline bubbles.
The Problem with Traditional Resource Barriers
In modern graphics APIs like DirectX 12, resources regularly change
states—for example, transitioning a texture from a render target
(D3D12_RESOURCE_STATE_RENDER_TARGET) to a shader resource
view (D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE). When
using a standard barrier, the transition is instantaneous from the
command stream's perspective.
Under the hood, however, this transition requires physical hardware actions:
- Flushing and invalidating L1 and L2 caches.
- Decompressing compressed color, depth, or stencil data.
- Waiting for prior pipeline stages to drain completely.
When these operations execute within a single barrier command, the GPU frequently experiences a pipeline bubble. Execution units (ALUs) sit idle while memory controllers and fixed-function hardware perform synchronization and cache flushing, resulting in measurable performance overhead.
How Split Barriers Function
Split barriers address this latency by splitting a transition into two separate calls:
- The Begin Phase
(
D3D12_RESOURCE_BARRIER_FLAG_BEGIN_ONLY): This signals to the driver and GPU that the application is finished using the resource in its "before" state. The GPU can immediately start asynchronous background tasks, such as draining read/write queues, resolving metadata, or invalidating relevant caches. - The End Phase
(
D3D12_RESOURCE_BARRIER_FLAG_END_ONLY): This informs the GPU that the resource is now required in its "after" state. The pipeline will only halt at this point if the background operations initiated by the begin barrier have not yet concluded.
During the execution window between the begin and end calls, the resource is in an indeterminate transitional state. It cannot be read or written by the GPU during this interval, but the pipeline remains entirely free to execute unrelated work.
Hiding Latency with Independent Workloads
The primary advantage of split barriers is that they turn idle waiting time into productive compute cycles. To effectively hide transition latency:
- Issue the Begin Barrier Early: As soon as a pass
finishes writing to a resource (e.g., generating a shadow map or
downsampling a bloom buffer), emit the
BEGIN_ONLYbarrier. - Schedule Independent Work: Submit draw calls, compute dispatches, or state setup commands that do not depend on the transitioning resource. For instance, while a shadow map resolves its depth metadata, the GPU can run compute dispatches for particle updates or execute unrelated post-processing passes.
- Issue the End Barrier Late: Immediately before the
command that actually binds the resource in its new state (e.g., reading
the shadow map in the main forward-lighting pass), submit the
END_ONLYbarrier.
If the independent work takes longer to execute than the hardware
transition operations, the latency of the state change is effectively
zero from the perspective of the application frame time. The GPU reaches
the END_ONLY barrier, detects that all decompression, cache
syncs, and memory updates are complete, and executes the dependent pass
without stalling.
Implementation Considerations
- Workload Balance: The effectiveness of split barriers depends directly on having sufficient independent work to fill the gap. If the end barrier is placed too close to the begin barrier, the GPU will still stall waiting for completion.
- Enhanced Barriers: In newer iterations of the DirectX 12 Agility SDK, Enhanced Barriers formalize this concept even further via sync and access scopes, allowing fine-grained control over specific cache lines and pipeline stages rather than coarse legacy states. Regardless of the API variation used, the fundamental architectural principle remains the same: starting synchronization work as early as possible to completely overlap memory latency with independent ALU computation.