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:
- The CPU enqueues a GPU signal command with a target fence value.
- The CPU associates a standard Win32 event handle with that value via
ID3D12Fence::SetEventOnCompletion. - The CPU calls a blocking OS primitive, such as
WaitForSingleObject, on the event handle. - 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.