DirectX 12 / PRIMARY BACKEND

Synchronization without guesswork

Understand queue execution, state barriers, fence values, allocator reuse, frame contexts, and multi-queue retirement.

Three different responsibilities#

Execution ordering determines when work happens. Memory access/state transitions determine how work reads and writes resources. Lifetime tracking determines how long resources and command storage remain valid. A fence wait does not replace every barrier, and a barrier does not tell the CPU that a queue is finished. The complete sample uses one direct queue, two frame contexts, one command list, and a monotonically increasing fence. FrameContext retains its allocator and the completion value of its last submitted work. The list may be reset for new recording while older submissions execute, provided its newly selected allocator is safe.

Follow the real fence calls#

cpp · REFERENCE EXCERPT
UINT64 Signal() {
    UINT64 value = nextFence++;
    Check(queue->Signal(fence.Get(), value));
    return value;
}
void Wait(UINT64 value) {
    if (value && fence->GetCompletedValue() < value) {
        Check(fence->SetEventOnCompletion(value, event));
        WaitForSingleObject(event, INFINITE);
    }
}

These are the sample’s real API operations, shortened only by omitting event-wait error handling. queue, fence, event, and nextFence are Sample members created during Init. A completion value is recorded only after the queue submission it must cover. Before resetting an allocator, ensure every submitted list that used its storage has completed. Before rewinding mapped upload bytes or overwriting descriptors, ensure their last GPU readers have completed too. They may share a frame fence if ownership is deliberately confined to that frame context.

INTERACTIVE ILLUSTRATIONSIMULATION / BROWSER

CPU and GPU, in parallel This illustration uses canvas. The explanation below describes the same process.

Change a control to inspect the result. Values describe the simulation, not native engine benchmarks. Open full lab ↗

Avoid a flush after every frame#

The sample waits for the selected back-buffer context’s prior completion, not unconditionally for the newest submission. This permits bounded overlap. Resize and initial texture upload intentionally Flush because they replace targets or publish a one-time resource in a simple teaching path. More frame contexts retain more resources and can increase latency. More swap-chain buffers do not automatically prove every independent upload or compute allocation is safe. Associate each piece of storage with the submission that last used it.

State transitions on the direct queue#

Back buffers use PRESENT → RENDER_TARGET → PRESENT. The texture uses COPY_DEST → PIXEL_SHADER_RESOURCE. Upload buffers remain GENERIC_READ. State history belongs to the resource or render-graph plan; guessing the previous state independently in each pass creates contradictory barriers. Legacy transition barriers are used in the teaching sample for broad clarity. Enhanced barriers are a separate feature/API path with their own support and semantics. Choose one documented implementation per backend instead of mixing terminology without a translation layer.

Several queues require several completion facts#

A copy queue can fill a texture while graphics executes unrelated work. The graphics queue waits on the copy completion before sampling it. Required final-state transitions occur on queues that support those usages. Queue waits are GPU scheduling operations; a CPU event wait is host synchronization. A resource used by multiple queues is retired after all relevant fence points pass. One graphics fence cannot prove unrelated compute work completed unless a dependency chain deliberately joins them. Upload storage can retire after its copy; the destination texture may remain for many more frames.

Reference retirement record
cpp · REFERENCE EXCERPT
struct FencePoint {
    QueueId queue;
    uint64_t value;
};
struct RetiredResource {
    ResourceHandle resource;
    std::vector<FencePoint> lastUses;
};

QueueId names a backend queue; ResourceHandle refers to a backend-owned allocation record. Retirement polls completion without blocking the main loop. Once every point completes, the backend can release storage and recycle its descriptor slots. Failed/device-lost submissions need an explicit teardown policy rather than waiting forever for an impossible value.

Debug symptoms#

Allocator reset errors mean storage reuse is premature. A texture that flickers every other frame may have a shared dynamic region being overwritten. A correct-looking image can still contain an unsafe lifetime bug that only appears under heavier GPU load. Enable the debug layer, inspect completion histories, and avoid adding arbitrary sleeps as a fix. Official references: allocator reset and fence-based management.

Search titles and full article text.