I have the following code:
import UIKit
import SpriteKit
class ViewController: UIViewController {
let textureAtlas: SKTextureAtlas
required init?(coder: NSCoder) {
textureAtlas = SKTextureAtlas(dictionary: ["Foo": UIImage(named: "foo.png")!])
super.init(coder: coder)
textureAtlas.preload {
}
}
}
You can add a random foo.png
file to your project. Then running it with Swift 6 mode will crash:
Thread 4 Queue : com.apple.spritekit.preloadQueue (concurrent)
#0 0x00000001099476f5 in _dispatch_assert_queue_fail ()
#1 0x000000010994768f in dispatch_assert_queue ()
#2 0x00007ffc10f5f251 in swift_task_isCurrentExecutorImpl ()
#3 0x0000000109e05f39 in closure #1 in ViewController.init(coder:) ()
#4 0x0000000109e05fa8 in thunk for @escaping @callee_guaranteed () -> () ()
#5 0x0000000109944b3d in _dispatch_call_block_and_release ()
#6 0x0000000109945ec6 in _dispatch_client_callout ()
#7 0x00000001099490f3 in _dispatch_continuation_pop ()
#8 0x0000000109947f20 in _dispatch_async_redirect_invoke ()
#9 0x0000000109959d2c in _dispatch_root_queue_drain ()
#10 0x000000010995a8ef in _dispatch_worker_thread2 ()
#11 0x00000001093c4b43 in _pthread_wqthread ()
#12 0x00000001093c3acf in start_wqthread ()
Enqueued from com.apple.spritekit.preloadQueue (Thread 4) Queue : com.apple.spritekit.preloadQueue (serial)
#0 0x0000000109946bf1 in _dispatch_group_wake ()
#1 0x0000000109949239 in _dispatch_continuation_pop ()
#2 0x0000000109947f20 in _dispatch_async_redirect_invoke ()
#3 0x0000000109959d2c in _dispatch_root_queue_drain ()
#4 0x000000010995a8ef in _dispatch_worker_thread2 ()
#5 0x00000001093c4b43 in _pthread_wqthread ()
#6 0x00000001093c3acf in start_wqthread ()
I have tried:
- Use Swift 5, with strict concurrency checking. No crash, no warning.
- Keep Swift 6, but remove the
preload
call. No crash. However, I need thispreload
for performance reason during the game.
I am not sure why it crashes here. The stack trace is assembly code.
3
Judging from the crash report alone, it seems like a bug in SpriteKit. The completion handler closure gets called on the queue com.apple.spritekit.preloadQueue
, but Swift Concurrency doesn’t know that.
Swift Concurrency assumes that the closure will get called on the same concurrency context as where preload
is called, because the closure isn’t marked @Sendable
.
Presumably the Swift 6 compiler inserts a check at the start of the closure to check if it is running in the correct concurrency context (the swift_task_isCurrentExecutorImpl
line in the trace), but in Swift 5 mode, this check either doesn’t exist, or it is designed to not crash to match the previous behaviour.
Marking the closure @Sendable
fixes the crash.
textureAtlas.preload { @Sendable in
}
It seems like other completion handlers in Objective-C APIs are all marked @Sendable
, like AVAsset.loadTracks
, URLSession.dataTask
, etc. The SpriteKit team probably forgot to do the same for preload
.
4