iosswiftsprite-kit

Crash when preloading SKTextureAtlas in Swift 6


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:

I am not sure why it crashes here. The stack trace is assembly code.


Solution

  • 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.