Metal 4 and object lifetime

I have a metal kit view and drain the draw method of its delegate like shown below. Let's say I have one or more MTLBuffers with vertex resources bound via the argument table.

When is it ok to drop these buffers?

As far as I know one cannot schedule a completion handler In Metal 4 and I haven't been able to find any documentation about the lifetime requirements here.

Any pointers/ideas appreciated.

class RenderCoordinator: NSObject, MTKViewDelegate {
  public func draw(in view: MTKView) {
    let commandAllocator: any MTL4CommandAllocator = ...
    let commandBuffer: any MTL4CommandBuffer = ...
    let commandQueue: any MTL4CommandQueue = ...
    guard let drawable = view.currentDrawable else { return }

    commandBuffer.beginCommandBuffer(allocator: commandAllocator)

    let state: any MTLRenderPipelineState = ...
    let encoder: any MTL4RenderCommandEncoder = ...
    let argTable: any MTL4ArgumentTable = ...
    encoder.setRenderPipelineState(state)
    encoder.setArgumentTable(argTable, stages: .vertex)

    commandBuffer.endCommandBuffer()
    commandQueue.waitForDrawable(drawable)
    commandQueue.commit([commandBuffer])
    commandQueue.signalDrawable(drawable)
    drawable.present()
  }
}
Answered by DTS Engineer in 904562022

There is no addCompletedHandler equivalent on MTL4CommandBuffer, but the completion signal still exists. It moved to the queue: signalEvent(_:value:) on MTL4CommandQueue, paired with an MTLSharedEvent your renderer waits on.

Understanding the Metal 4 core API (https://developer.apple.com/documentation/metal/understanding-the-metal-4-core-api) states the change:

Unlike the default behavior of MTLCommandBuffer, you may need to consider a resource's retain count because each MTL4CommandBuffer instance doesn't create strong references to resources.

The argument table doesn't hold a reference either, because it binds addresses rather than objects:

- (void)setAddress:(MTLGPUAddress)gpuAddress atIndex:(NSUInteger)bindingIndex;

So nothing in the submission path keeps your vertex buffers alive. Something in your own code needs to hold them until the GPU finishes the work that reads them.

Drawing a triangle with Metal 4 (https://developer.apple.com/documentation/metal/drawing-a-triangle-with-metal-4) shows the pattern, running three frames in flight:

  • A separate MTLBuffer per in-flight frame, held in an array for the renderer's lifetime
  • Bindings by vertexBuffer.gpuAddress
  • A signal at the end of each frame: commandQueue.signalEvent(sharedEvent, value: frameNumber)
  • A wait before reusing a frame's resources: sharedEvent.wait(untilSignaledValue: frameNumber - kMaxFramesInFlight, timeoutMS: 10)

That wait is what tells you a buffer can be dropped or overwritten. The event fires when the frame that referenced it has finished.

Your submit ordering, waitForDrawable then commit then signalDrawable then present, matches the sample. What isn't there yet is the signalEvent call, and a reference to the buffers that outlives draw(in:).

A residency set covers residency rather than lifetime. The sample keeps its own strong references to the buffers in addition to adding them to one.

MTL4CommitOptions does have an addFeedbackHandler: method, and it does fire after completion. It carries error, GPUStartTime, and GPUEndTime, and is described as debug information, so it suits timing and error reporting more than resource management.

The sample relates to WWDC25 session 205, Discover Metal 4 (https://developer.apple.com/wwdc25/205).

the code should, of course have said that

    encoder.setArgumentTable(argTable, stages: .vertex)
    // encode draw commands
    commandBuffer.endCommandBuffer()
Accepted Answer

There is no addCompletedHandler equivalent on MTL4CommandBuffer, but the completion signal still exists. It moved to the queue: signalEvent(_:value:) on MTL4CommandQueue, paired with an MTLSharedEvent your renderer waits on.

Understanding the Metal 4 core API (https://developer.apple.com/documentation/metal/understanding-the-metal-4-core-api) states the change:

Unlike the default behavior of MTLCommandBuffer, you may need to consider a resource's retain count because each MTL4CommandBuffer instance doesn't create strong references to resources.

The argument table doesn't hold a reference either, because it binds addresses rather than objects:

- (void)setAddress:(MTLGPUAddress)gpuAddress atIndex:(NSUInteger)bindingIndex;

So nothing in the submission path keeps your vertex buffers alive. Something in your own code needs to hold them until the GPU finishes the work that reads them.

Drawing a triangle with Metal 4 (https://developer.apple.com/documentation/metal/drawing-a-triangle-with-metal-4) shows the pattern, running three frames in flight:

  • A separate MTLBuffer per in-flight frame, held in an array for the renderer's lifetime
  • Bindings by vertexBuffer.gpuAddress
  • A signal at the end of each frame: commandQueue.signalEvent(sharedEvent, value: frameNumber)
  • A wait before reusing a frame's resources: sharedEvent.wait(untilSignaledValue: frameNumber - kMaxFramesInFlight, timeoutMS: 10)

That wait is what tells you a buffer can be dropped or overwritten. The event fires when the frame that referenced it has finished.

Your submit ordering, waitForDrawable then commit then signalDrawable then present, matches the sample. What isn't there yet is the signalEvent call, and a reference to the buffers that outlives draw(in:).

A residency set covers residency rather than lifetime. The sample keeps its own strong references to the buffers in addition to adding them to one.

MTL4CommitOptions does have an addFeedbackHandler: method, and it does fire after completion. It carries error, GPUStartTime, and GPUEndTime, and is described as debug information, so it suits timing and error reporting more than resource management.

The sample relates to WWDC25 session 205, Discover Metal 4 (https://developer.apple.com/wwdc25/205).

Metal 4 and object lifetime
 
 
Q