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 ()
2
Answers
Making the closure Sendable solves the issue.
Building on the answer from @Affinity and leading to a sub point…
It seems like adding @Sendable could be considered a workaround for a pending fix from Apple or will become the norm. At the time of writing with the App Store Xcode 16.0 release version, the example built into Xcode: New > Project… > Game > using Metal option, has been tweaked since Xcode 15 but it is still missing @Sendable and hence fails when switching to Swift 6.
Doing as @Affinity states does fix the example when turning on the Swift 6 build setting:
Sub point: I am in the habit of adding [weak self] to closures to remove strong class references. The renderer class (ViewController in the question) are not always going to be easily migrated to being Sendable and weak self with @Sendable will cause a compilation error. Typically the addCompletedHandler is only going to be signalling a semaphore. Hence, I use a different flavour of syntax unless I am told that the above is a better practice:
Currently, Apple defines the DispatchSemaphore with @unchecked:
So there are questions/concerns over safety and whether using a local copy of the reference class type DispatchObject, as per Apple’s example, has any advantages. In my understanding weak inflightSemaphore is akin to the first code snippet in my reply.