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:

  1. The GPU finishes writing to a resource (e.g., a render target).
  2. The pipeline stalls while caches are flushed, memory is synchronized, and compression states (such as depth or render target compression) are resolved.
  3. 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_ONLY and END_ONLY flags 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_WRITE to D3D12_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.