SceneKit app seriously hangs when run in fullscreen

I've been running my SceneKit game for many weeks in Xcode without performance issues. The game itself is finished, so I thought I could go on with publishing it on the App Store, but when archiving it in Xcode and running the archived app, I noticed that it seriously hangs.

The hangs only seem to happen when I run the game in fullscreen mode. I tried disabling game mode, but the hangs still happen. Only when I run in windowed mode the game runs smoothly.

Instruments confirms that there are many serious hangs, but it also reports that CPU usage is quite low during those hangs, on average about 15%. From what I know, hangs happen when the main thread is busy, but how can that be when CPU usage is so low, and why does it only happen in fullscreen mode for release builds?

Answered by DTS Engineer in 817975022

It looks like we've provided a response:

"SceneKit doesn't require modification to be performed on the main thread. Quite the opposite. Doing work in the renderer delegation callbacks allows for direct access to the presentation tree and doesn't go through a transaction. Dispatching to the main thread (with a Swift Task) is an anti-pattern in this case."

I was able to recreate the issue with a small sample project that repeatedly calls SCNView.projectPoint(_:) in a block dispatched to the main actor inside the SCNSceneRendererDelegate.renderer(_:didApplyAnimationsAtTime) method.

In my personal project, the issue happens when calling that method 6 times (in order to position 6 sprites in the overlay SpriteKit scene that should follow objects in the SceneKit scene), but in the sample I call it 200 times to reliably reproduce the issue.

As mentioned previously, the hangs only happen when running the app in fullscreen, but I noticed that when moving the mouse to the top of the screen to show the menu bar, the game runs smoothly. As soon as the menu bar disappears again, or if I click a main menu item to show the submenu, the game immediately hangs again and shows maybe 2 or even less frames per second. Sometimes the hangs continue happening unless I show the menu bar again, but sometimes the game begins running smoothly again after a couple seconds.

This is again confirmed by Instruments.

Interestingly, if I comment out the line

Task { @MainActor in

and the corresponding closing curly bracket 4 lines down, there is no hang anymore, not in fullscreen, and not when clicking the main menu:

Is this an issue with SCNView.projectPoint(_:)? Or with the delegate method? Or with doing SceneKit things on the main thread? I thought that we're supposed to add nodes and do modifications on the main thread, so how could I avoid thread races if in this delegate method the only solution is not to use the main thread?

class GameViewController: NSViewController, SCNSceneRendererDelegate {
    
    var scnView: SCNView?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let scene = SCNScene(named: "art.scnassets/ship.scn")!
        let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
        ship.runAction(.customAction(duration: 999, action: { node, t in
            Task { @MainActor in
                node.eulerAngles.y = t
            }
        }))
        let scnView = self.view as! SCNView
        self.scnView = scnView
        scnView.scene = scene
        scnView.delegate = self
    }
    
    func renderer(_ renderer: any SCNSceneRenderer, didApplyAnimationsAtTime time: TimeInterval) {
        Task { @MainActor in
            for _ in 0..<200 {
                let _ = scnView?.projectPoint(SCNVector3(x: 0, y: 0, z: 0))
            }
        }
    }
}

Note: occasionally the hang doesn't happen while debugging the app in Xcode, but until now it always happened when profiling the app in Instruments.

Hi,

Please contact us so that we can engage with you in more depth. We will post back any noteworthy results for the benefit of the community.

In the meantime I already filed FB15737374 and opened Case-ID 10297510 to discuss a different issue I'm having, and the TSI engineer suggested as a workaround to update the SpriteKit scene in its SKScene.update(_:) method (although the delegate methods seem to work as well), which poses the question of how to avoid race conditions when accessing my custom data needed both in SceneKit and in SpriteKit (since the first uses its own thread, and the latter is annotated @MainActor). For now, it is proving to be a difficult conversation.

It looks like we've provided a response:

"SceneKit doesn't require modification to be performed on the main thread. Quite the opposite. Doing work in the renderer delegation callbacks allows for direct access to the presentation tree and doesn't go through a transaction. Dispatching to the main thread (with a Swift Task) is an anti-pattern in this case."

Dispatching to the main thread (with a Swift Task) is an anti-pattern in this case

Correct, and as I mentioned, this poses the question of how to avoid race conditions when accessing my custom data needed both in SceneKit and in SpriteKit (since the first uses its own thread, and the latter is annotated @MainActor).

SceneKit app seriously hangs when run in fullscreen
 
 
Q