Contents

Synchronization

Synchronization establishes the order in which operations access shared data and ensures that later operations see the results they need. In a write-to-read dependency, the writer is the producer and the reader is the consumer. Barriers and texture transitions coordinate GPU accesses; timelines connect submissions and let the CPU wait for GPU work to finish.

Access dependencies

For each resource, identify the accesses that touch the same data. If either access writes, preserve the required order between them. Two reads do not conflict, although texture layout and presentation requirements still apply. CPU updates and resource disposal also need to wait until the GPU has finished using the affected storage.

The appropriate mechanism depends on where the operations execute:

Situation Mechanism
Dependent GPU stages in a command stream CommandBuffer.Barrier with the producing and consuming stages.
A texture subresource changes how it is used CommandBuffer.Transition with its previous and next layouts.
One queue consumes data produced on another queue Pass the producer's TimelineValue to the consumer's Submit.
CPU access or resource release depends on GPU completion Call Wait() on the relevant completion value.

A workload may need more than one mechanism. For example, a texture produced on another queue needs a submission dependency and the appropriate layout for its next use. Neither operation requires blocking the CPU unless the CPU itself needs to access or release the data.

Pipeline barriers

Consider two compute dispatches recorded into the same command buffer, where the first writes simulation data and the second reads it. Place a barrier between them so that the second dispatch sees the completed writes. For a storage buffer, this orders the accesses without a texture layout transition:

commandBuffer.Barrier(BarrierStages.ComputeShading, BarrierStages.ComputeShading);

The first argument identifies the stages that produced the data; the second identifies the stages that will consume it. Together they describe the execution order and memory visibility required between the two dispatches.

The FluidTank simulation uses this pattern between simulation steps. Each barrier follows the dispatch that produces the data and precedes the dispatch that uses it.

Choose stages that include the relevant accesses and are supported by the queue. BarrierStages.All includes every supported stage; select a narrower set when the dependency only involves those stages.

Texture layouts

A texture's usage flags declare its permitted roles. TextureLayout describes how a particular subresource, identified by a mip level and array layer, is accessed at a given point in the command sequence. When that access changes, record a transition. For example, the compute sample writes an output texture in Storage layout, then prepares it for sampling in a graphics pass:

commandBuffer.Transition(outputTexture, default, TextureLayout.Storage, TextureLayout.Sampled);

Here, outputTexture was created with Sampled | Storage usage and the selected subresource is in Storage layout. default selects mip level 0 and array layer 0. Other mip levels and layers are unaffected by this transition.

For Transition, supply the layout from the subresource's previous use. The application tracks this state as it records commands and passes textures between rendering stages.

Use Undefined as the previous layout when the existing contents can be discarded, such as for a new image or an attachment that will be cleared completely. To preserve the contents for loading, sampling, or accumulation, use the layout from the preceding access.

Backend behavior

DirectX 12 and Vulkan encode texture transitions as layout and access changes. Metal has no equivalent operation in Zenith.NET's Transition implementation. The Metal backend instead groups commands into render and compute encoders and inserts visibility barriers when an encoder ends. Dependent operations within one encoder, such as successive compute dispatches, use explicit Barrier calls.

In the compute sample, starting the graphics render pass ends the compute encoder, making its output visible to the graphics work. Two successive compute dispatches remain in the same encoder, so their dependency needs a barrier even if a texture transition is recorded between them. This is why portable command sequences account for access dependencies as well as texture layouts.

Timelines and queue dependencies

A timeline identifies queue completion with increasing values. Submit() returns a TimelineValue containing the timeline and the submission's value. Numeric Value fields from different timelines cannot be compared directly.

Let producer and consumer be recorded command buffers from two queues in the same context, with the consumer using data written by the producer. Submit them with this dependency:

TimelineValue produced = producer.Submit();

TimelineValue consumed = consumer.Submit(produced);

The consuming queue waits for the supplied completion value before executing its commands. The CPU can continue without calling produced.Wait(). Keep the shared resources alive until consumed completes, and record the texture transitions required by the consumer's access.

If the CPU needs to read the result, overwrite the shared data, or release the resources after the consumer finishes, wait for that submission:

consumed.Wait();

The value returned by queue.Timeline.Signal() can likewise be waited on with Wait() or used as a dependency for another queue. Signal() places this completion point after previously submitted work; the GPU reaches it once that work has finished.

Readback completion

IsCompleted reports GPU completion. A recorded download has one further step: copying the result from internal readback storage into the application's destination memory.

CommandBuffer.Download records the GPU copy into that internal storage. After the submission finishes, the library performs the CPU copy while reclaiming the completed command buffer. Calling Wait() on the download's submission value waits for the GPU and processes that queue's completed buffers, completing both steps. Keep the destination allocated, and managed memory pinned, until the call returns.

Polling IsCompleted alone does not perform the final CPU copy. A wait on another queue does not reclaim the download queue's completed buffers either. Resource Management describes when uploads and downloads use direct memory access or staging storage.

Resource reuse

The triangle waits for each frame to finish before reusing its data. To overlap frames, give each in-flight frame its own mutable storage and track when that storage becomes available again. Before changing data, wait for GPU work that still accesses the affected bytes; before releasing a resource, wait for all of its outstanding uses.

Resizing and shutdown follow the same rule: stop new uses, wait for existing uses to finish, then replace or release the resources. Establish this order before calling SwapChain.Resize or disposing resources or the context. The triangle already waits for each frame, so it can resize between callbacks.

SwapChain.Present() signals and waits on the graphics queue after presentation. The triangle therefore waits at presentation even if its explicit submission wait is removed. Work submitted independently to other queues requires its own completion tracking.

Search documentation

Search tutorials, concepts, samples, and the API reference.