DirectX 12 Fences: CPU and GPU Synchronization

DirectX 12 fences are synchronization primitives designed to coordinate asynchronous operations between the host CPU and the graphics hardware, as well as between distinct GPU command queues. Because DirectX 12 is an explicit low-level API, it eliminates the automatic driver-level synchronization present in older APIs, requiring developers to manage the execution timeline manually. This article examines the core mechanisms provided by ID3D12Fence, explaining how signal, wait, event notification, and polling operations manage resource hazards and prevent race conditions.

The Anatomy of an ID3D12Fence

At its core, an ID3D12Fence encapsulates an atomically updated, monotonically increasing 64-bit unsigned integer value. This value represents a specific progress milestone along an execution timeline. Fences are typically initialized with a starting value (often zero), and as work progresses through the rendering pipeline, subsequent values denote the completion of specific sets of commands or frames.

GPU-Side Signaling (ID3D12CommandQueue::Signal)

The primary mechanism for recording progress on the GPU is the ID3D12CommandQueue::Signal method. When called, this function appends a command directly into the GPU command queue. The GPU executes this instruction in sequence after all preceding commands in that specific queue have fully completed execution. Upon reaching this instruction, the GPU updates the fence's internal value to the specified target integer.

GPU-to-GPU Synchronization (ID3D12CommandQueue::Wait)

DirectX 12 supports multiple distinct hardware queues, such as Direct (graphics), Compute, and Copy (transfer) queues. To coordinate dependencies across these disparate engines without CPU intervention, DirectX 12 provides ID3D12CommandQueue::Wait.

When a queue encounters a Wait instruction, it halts the execution of any subsequent commands in that queue until the target fence reaches or exceeds the specified value. Because this stall occurs entirely on the GPU, the CPU remains free to continue recording commands or handling game logic.

GPU-to-CPU Synchronization via Events (SetEventOnCompletion)

When the CPU must pause until the GPU completes a specific task—such as when recycling ring-buffer dynamic memory, resizing a swap chain, or managing frame latency—it uses event-based synchronization:

  1. The CPU enqueues a GPU signal command with a target fence value.
  2. The CPU associates a standard Win32 event handle with that value via ID3D12Fence::SetEventOnCompletion.
  3. The CPU calls a blocking OS primitive, such as WaitForSingleObject, on the event handle.
  4. Once the GPU writes the specified value to the fence, it fires an interrupt that signals the Win32 event, immediately waking the sleeping CPU thread.

Non-Blocking CPU Polling (ID3D12Fence::GetCompletedValue)

In many rendering architectures, completely stalling the CPU is undesirable. DirectX 12 allows the CPU to query the current progress of the GPU without blocking by calling ID3D12Fence::GetCompletedValue.

This method performs a thread-safe read of the current fence value recorded in shared memory. Developers leverage this for deferred resource destruction (garbage collection) and tracking available space in linear allocation buffers, freeing or re-allocating memory only after confirming that the GPU has advanced past the milestone where that memory was referenced.

CPU-to-GPU Signaling (ID3D12Fence::Signal)

While less common than GPU-side signaling, the CPU can directly update the value of a fence using the member function ID3D12Fence::Signal. This immediately changes the fence's current value from the host side. Any GPU queues that were stalled waiting on that fence value via ID3D12CommandQueue::Wait will unblock as soon as the memory write is visible, allowing coordinated handover of data prepared entirely on the CPU side.