<!--
{
  "documentType" : "article",
  "framework" : "Metal",
  "identifier" : "/documentation/Metal/synchronizing-passes-with-a-fence",
  "metadataVersion" : "0.1.0",
  "role" : "article",
  "title" : "Synchronizing passes with a fence"
}
-->

# Synchronizing passes with a fence

Block GPU stages in a pass until another pass unblocks it by signaling a fence.

## Discussion

A fence resolves access conflicts between commands in different passes that you submit to the same command queue, including the passes you commit in other command buffers.

> Note:
> ``doc://com.apple.metal/documentation/Metal/MTLFence`` instances in Metal 3 work across multiple command queues that belong to the same device;
> to synchronize across multiple command queues in Metal 4,
> use ``doc://com.apple.metal/documentation/Metal/MTLEvent`` or ``doc://com.apple.metal/documentation/Metal/MTLSharedEvent`` instances.

When your app encodes commands that access a resource from different passes
— or different stages within a single pass —
it creates an access conflict when at least one command modifies that resource.
This conflict happens because the GPU can run multiple commands at the same time, including those from:

- Multiple passes
- Different stages of a pass, such as the [`blit`](/documentation/Metal/MTLStages/blit) and [`dispatch`](/documentation/Metal/MTLStages/dispatch) stages of a compute pass
- Multiple instances of a stage, such as two or more dispatch commands within a compute pass

For more information about resource access conflicts and GPU stages,
see [Resource synchronization](/documentation/Metal/resource-synchronization) and [`MTLStages`](/documentation/Metal/MTLStages), respectively.

> Important: To synchronize stages within the same pass,
> use an *intrapass barrier* instead of a fence
> because fences can only synchronize between stages of different passes.

For more information about synchronizing within a single pass, see
[Synchronizing stages within a pass](/documentation/Metal/synchronizing-stages-within-a-pass).

Start by identifying which memory operations from different passes introduce a conflict and resolve it with a fence:

1. Update a fence in the producing pass.
2. Wait for that fence in the consuming pass.

> Note:
> Create an ``doc://com.apple.metal/documentation/Metal/MTLFence`` by calling the ``doc://com.apple.metal/documentation/Metal/MTLDevice/makeFence()`` method of an ``doc://com.apple.metal/documentation/Metal/MTLDevice`` instance.

### Identify access conflicts between two or more passes

The following code example encodes two compute passes.
The first encoder creates a pass with a copy command and a dispatch command:

```swift
func encodeComputeWorkWithFence(fence: MTLFence,
                                commandBuffer: MTL4CommandBuffer,
                                argumentTable: MTL4ArgumentTable,
                                buffers: [MTLBuffer])
{
    // === Encode pass 1 ===

    // Create an encoder for the first compute pass.
    let computeEncoder1: MTL4ComputeCommandEncoder!
    computeEncoder1 = commandBuffer.makeComputeCommandEncoder()

    // Assign the argument table to the compute encoder.
    computeEncoder1.setArgumentTable(argumentTable)

    // Add the buffers to the argument table for the dispatch command.
    let bufferA = buffers[0]
    let bufferB = buffers[1]

    argumentTable.setAddress(bufferA.gpuAddress, index: 0)
    argumentTable.setAddress(bufferB.gpuAddress, index: 1)

    // Copy from `bufferA` to `bufferB`, which runs during the blit stage.
    computeEncoder1.copy(sourceBuffer: bufferA, sourceOffset: 0,
                        destinationBuffer: bufferB, destinationOffset: 0,
                        size: copySize)

    // Run a dispatch command that modifies `bufferC`,
    // which the GPU runs during the dispatch stage.
    let bufferC = buffers[2]
    argumentTable.setAddress(bufferC.gpuAddress, index: 2)
    computeEncoder1.setComputePipelineState(modifyBufferIndex2ComputePipeline)
    computeEncoder1.dispatchThreadgroups(threadgroupsPerGrid: threadgroupCount,
                                         threadsPerThreadgroup: threadsPerThreadgroup)

    // Pass 1 needs to update a fence here.

    // Finalize the first compute pass.
    computeEncoder1.endEncoding()
```

The second encoder also creates a pass with a copy command and a dispatch command:

```swift
    // === Encode pass 2 ===

    // Create an encoder for the second compute pass.
    let computeEncoder2: MTL4ComputeCommandEncoder!
    computeEncoder2 = commandBuffer.makeComputeCommandEncoder()

    // Assign the argument table to the compute encoder.
    computeEncoder2.setArgumentTable(argumentTable)

    // Pass 2 needs to wait for a fence here.

    // Copy from `bufferC` to `bufferD`, which runs during the blit stage.
    let bufferD = buffers[3]
    argumentTable.setAddress(bufferD.gpuAddress, index: 3)
    computeEncoder2.copy(sourceBuffer: bufferC, sourceOffset: 0,
                         destinationBuffer: bufferD, destinationOffset: 0,
                         size: copySize)

    // Run a dispatch command that works with `bufferE`.
    let bufferE = buffers[4]
    argumentTable.setAddress(bufferE.gpuAddress, index: 4)
    computeEncoder2.setComputePipelineState(modifyBufferIndex4ComputePipeline)
    computeEncoder2.dispatchThreadgroups(threadgroupsPerGrid: threadgroupCount,
                                         threadsPerThreadgroup: threadsPerThreadgroup)

    // Finalize the second compute pass.
    computeEncoder2.endEncoding()
}
```

The example has at least one access conflict because both passes access a common resource, `bufferC`:

- The dispatch command from the first pass stores to `bufferC`.
- The copy command from the second pass loads from `bufferC`.

![A diagram showing two compute passes accessing the same buffer C, with pass 1 storing to buffer C during its dispatch stage and pass 2 loading from buffer C during its blit stage.](images/com.apple.metal/synchronizing-passes-with-a-fence-1@2x.png)

Without synchronization, the GPU can run both passes and their stages in parallel, which can yield inconsistent results in resources with access conflicts.

![A diagram showing both passes and their stages running in parallel without synchronization, potentially causing inconsistent results.](images/com.apple.metal/synchronizing-passes-with-a-fence-2@2x.png)

### Resolve an access conflict between passes with a fence

Resolve access conflicts between passes from the same command queue with an [`MTLFence`](/documentation/Metal/MTLFence) instance by:

- Instructing the producing pass to signal a pass that’s waiting for a fence by calling the encoder’s [`updateFence(_:afterEncoderStages:)`](/documentation/Metal/MTL4CommandEncoder/updateFence(_:afterEncoderStages:)) method.
- Instructing the consuming pass to wait for the fence by calling the encoder’s [`waitForFence(_:beforeEncoderStages:)`](/documentation/Metal/MTL4CommandEncoder/waitForFence(_:beforeEncoderStages:)) method.

The GPU pauses before running the commands you encode in the consuming pass after the wait command until the GPU runs all update commands you encode for the same fence in the other relevant, producing passes.

> Tip: To get the best runtime performance in passes that update or wait for a fence, encode them as close as possible to the commands that introduce resource access conflicts.

The following code example modifies the code for the first pass by adding a call that updates the fence:

```swift
    // Run a dispatch command that modifies `bufferC`,
    // which the GPU runs during the dispatch stage.
    let bufferC = buffers[2]
    argumentTable.setAddress(bufferC.gpuAddress, index: 2)
    computeEncoder1.setComputePipelineState(modifyBufferIndex2ComputePipeline)
    computeEncoder1.dispatchThreadgroups(threadgroupsPerGrid: threadgroupCount,
                                         threadsPerThreadgroup: threadsPerThreadgroup)

    // Unblock the pass 2 that's waiting on the fence by
    // updating it when the dispatch stage (of pass 1) is done.
    computeEncoder1.updateFence(fence, afterEncoderStages: .dispatch)

    // Finalize the first compute pass.
    computeEncoder1.endEncoding()
```

The following code example modifies the code for the second pass by adding a call that waits for the fence.

```swift
    // Assign the argument table to the compute encoder.
    computeEncoder2.setArgumentTable(argumentTable)

    // Wait for pass 1 to update the fence.
    computeEncoder2.waitForFence(fence, beforeEncoderStages: .blit)

    // Copy from `bufferC` to `bufferD`, which runs during the blit stage.
    let bufferD = buffers[3]
    argumentTable.setAddress(bufferD.gpuAddress, index: 3)
    computeEncoder2.copy(sourceBuffer: bufferC, sourceOffset: 0,
                         destinationBuffer: bufferD, destinationOffset: 0,
                         size: copySize)
```

The fence forces the GPU to wait before it runs the blit stage of the second pass until the dispatch stage of the first pass finishes storing its modifications to the underlying memory for `bufferC`.

![A diagram showing the fence synchronization where the GPU waits for pass 1’s dispatch stage to complete before running pass 2’s blit stage.](images/com.apple.metal/synchronizing-passes-with-a-fence-3@2x.png)

You can reuse a fence instance to resolve resource access conflicts in subsequent commands after encoding a wait command for a pass.

> Important:
> To reuse a fence within the same pass, encode the wait command first, then encode the update command.

For more information about other synchronization mechanisms, see these articles in the series:

- [Synchronizing stages within a pass](/documentation/Metal/synchronizing-stages-within-a-pass)
- [Synchronizing passes with consumer barriers](/documentation/Metal/synchronizing-passes-with-consumer-barriers)
- [Synchronizing passes with producer barriers](/documentation/Metal/synchronizing-passes-with-producer-barriers)

---

Copyright &copy; 2026 Apple Inc. All rights reserved. | [Terms of Use](https://www.apple.com/legal/internet-services/terms/site.html) | [Privacy Policy](https://www.apple.com/privacy/privacy-policy)