Understanding Split Barriers in DirectX 12
In DirectX 12, split barriers optimize GPU synchronization by dividing a standard resource state transition into two distinct phases: a beginning and an end. Instead of halting execution while waiting for memory caches to flush and pipeline stages to clear, split barriers allow the GPU to overlap unrelated workloads with the transition overhead. This article explains the mechanism behind split barriers, why they eliminate costly GPU pipeline bubbles, and how to effectively utilize them in graphics engine architectures.
The Cost of Traditional Barriers
To understand split barriers, consider the standard resource barrier
in DirectX 12. A typical transition barrier
(D3D12_RESOURCE_BARRIER_TYPE_TRANSITION) acts as a blocking
operation:
- The GPU finishes writing to a resource (e.g., a render target).
- The pipeline stalls while caches are flushed, memory is synchronized, and compression states (such as depth or render target compression) are resolved.
- Subsequent operations that read the resource (e.g., a shader resource view in a compute or pixel shader) wait until the entire transition completes.
This sequence frequently generates "pipeline bubbles"—periods where GPU compute units sit idle waiting for memory synchronization tasks to finalize.
How Split Barriers Work
A split barrier breaks the transition process into two explicit API calls using flags:
D3D12_RESOURCE_BARRIER_FLAG_BEGIN_ONLY: Signals to the GPU driver that the resource is done being modified in its source state and that any necessary background processing (such as decompression or cache flushing) can commence.D3D12_RESOURCE_BARRIER_FLAG_END_ONLY: Placed immediately before the resource is read by a dependent operation, signaling that the resource must now be fully transitioned to the target state.
Between the begin and end calls, the application can issue independent draw calls, compute dispatches, or state changes that do not rely on the transitioning resource.
Traditional Barrier:
[Work A] -> [Stall: Flush & Transition] -> [Work B (depends on A)]
Split Barrier:
[Work A]
-> [Barrier BEGIN]
-> [Independent Work C] (GPU flushes & transitions A in parallel)
-> [Barrier END]
-> [Work B (depends on A)]
Primary Purposes and Benefits
1. Latency Hiding
The fundamental purpose of a split barrier is latency hiding. Heavy state transitions—such as converting an MSAA render target or a high-resolution depth buffer into a shader-readable texture—involve hardware-level decompression algorithms and significant cache eviction. By placing the "begin" barrier immediately after the writing pass and delaying the "end" barrier until right before the reading pass, this latency runs concurrently with other rendering tasks.
2. Eliminating Execution Bubbles
Modern GPUs achieve high performance through massive parallelism. When a command processor encounters a monolithic barrier, it must drain execution pipes before proceeding. Split barriers provide the driver with a broader execution window, allowing it to schedule wave launches and memory transactions more fluidly, keeping arithmetic logic units (ALUs) saturated.
3. Asynchronous Cache Management
Hardware vendors implement cache management differently. Split barriers give the GPU hardware scheduler maximum flexibility to perform partial or opportunistic cache invalidation across multiple clock cycles, rather than forcing an abrupt, full-system pipeline flush.
Best Practices for Using Split Barriers
- Maximize the Distance: A split barrier provides no
performance benefit if the
BEGIN_ONLYandEND_ONLYflags are submitted back-to-back. Space them out by inserting independent rendering passes between them. - Target Expensive Transitions: Use split barriers
primarily for operations that trigger decompression or heavy cache
manipulation, such as transitioning depth buffers
(
D3D12_RESOURCE_STATE_DEPTH_WRITEtoD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE) or render targets to copy sources. - Avoid Over-Splitting Lightweight States: Transitioning buffers between states that only require minor flag adjustments or simple cache invalidations (like constant buffers or uniform read states) rarely benefits from split barriers and may introduce unnecessary driver overhead.