Metal addCompletedHandler causes crash with Swift 6 (iOS)

The following code runs fine when compiled with Swift 5, but crashes when compiled with Swift 6 (stack trace below). In the draw method, commenting out the addCompletedHandler line fixes the problem. I'm testing on iOS 18.0 and see the same behavior in both the simulator and on a device. What's going on here?

import Metal
import MetalKit
import UIKit

class ViewController: UIViewController {
  @IBOutlet var metalView: MTKView!

  private var commandQueue: MTLCommandQueue?

  override func viewDidLoad() {
    super.viewDidLoad()

    guard let device = MTLCreateSystemDefaultDevice() else {
      fatalError("expected a Metal device")
    }
    self.commandQueue = device.makeCommandQueue()

    metalView.device = device
    metalView.enableSetNeedsDisplay = true
    metalView.isPaused = true
    metalView.delegate = self
  }
}

extension ViewController: MTKViewDelegate {
  func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {}

  func draw(in view: MTKView) {
    guard let commandQueue,
          let commandBuffer = commandQueue.makeCommandBuffer()
    else { return }

    commandBuffer.addCompletedHandler { _ in }  // works with Swift 5, crashes with Swift 6

    commandBuffer.commit()
  }
}

Here's the stack trace:

Thread 10 Queue : connection Queue (serial)
#0  0x000000010581c3f8 in _dispatch_assert_queue_fail ()
#1  0x000000010581c384 in dispatch_assert_queue ()
#2  0x00000002444c63e0 in swift_task_isCurrentExecutorImpl ()
#3  0x0000000104d71ec4 in closure #1 in ViewController.draw(in:) ()
#4  0x0000000104d71f58 in thunk for @escaping @callee_guaranteed (@guaranteed MTLCommandBuffer) -> () ()
#5  0x0000000105ef1950 in __47-[CaptureMTLCommandBuffer _preCommitWithIndex:]_block_invoke_2 ()
#6  0x00000001c50b35b0 in -[MTLToolsCommandBuffer invokeCompletedHandlers] ()
#7  0x000000019e94d444 in MTLDispatchListApply ()
#8  0x000000019e94f558 in -[_MTLCommandBuffer didCompleteWithStartTime:endTime:error:] ()
#9  0x000000019e95352c in -[_MTLCommandQueue commandBufferDidComplete:startTime:completionTime:error:] ()
#10 0x0000000226ef50b0 in handleMainConnectionReplies ()
#11 0x00000001800c9690 in _xpc_connection_call_event_handler ()
#12 0x00000001800cad90 in _xpc_connection_mach_event ()
#13 0x000000010581a86c in _dispatch_client_callout4 ()
#14 0x0000000105837950 in _dispatch_mach_msg_invoke ()
#15 0x0000000105822870 in _dispatch_lane_serial_drain ()
#16 0x0000000105838c10 in _dispatch_mach_invoke ()
#17 0x0000000105822870 in _dispatch_lane_serial_drain ()
#18 0x00000001058237b0 in _dispatch_lane_invoke ()
#19 0x00000001058301f0 in _dispatch_root_queue_drain_deferred_wlh ()
#20 0x000000010582f75c in _dispatch_workloop_worker_thread ()
#21 0x00000001050abb74 in _pthread_wqthread ()
Answered by CoginCostner in 807638022

As the Apple Engineer stated and as in the stack overflow reply to the same question https://stackoverflow.com/questions/78999756/metal-addcompletedhandler-causes-crash-with-swift-6-ios not having @Sendable is the cause.

The work around (adapting the example built into Xcode: New > Project... > Game > using Metal option):

let semaphore = inFlightSemaphore
commandBuffer.addCompletedHandler { @Sendable (_ commandBuffer)-> Swift.Void in
   semaphore.signal()
}

I am using a different flavour of syntax unless I am told that the above is better practice:

commandBuffer.addCompletedHandler { @Sendable [weak inflightSemaphore] commandBuffer in
   inflightSemaphore?.signal()
}

TBD

It turns out to be a known issue related to the completion handler closure lacking @Sendable.

We're working on a fix and if there is a workaround I'll post it here.

As the Apple Engineer stated and as in the stack overflow reply to the same question https://stackoverflow.com/questions/78999756/metal-addcompletedhandler-causes-crash-with-swift-6-ios not having @Sendable is the cause.

The work around (adapting the example built into Xcode: New > Project... > Game > using Metal option):

let semaphore = inFlightSemaphore
commandBuffer.addCompletedHandler { @Sendable (_ commandBuffer)-> Swift.Void in
   semaphore.signal()
}

I am using a different flavour of syntax unless I am told that the above is better practice:

commandBuffer.addCompletedHandler { @Sendable [weak inflightSemaphore] commandBuffer in
   inflightSemaphore?.signal()
}
Metal addCompletedHandler causes crash with Swift 6 (iOS)
 
 
Q