Why does my game take so long to add a scene to tree for the first time?

Godot Version

Godot 4.5

Question

I’ve set up a customizable system to switch scenes but for some reason it is really slow for the size of scenes I’m passing to it. I’ve manged to mitigate it a little using threaded loading, but it’s still slow. One thing I’ve noticed is that RAM usage doesn’t go up until the scene is actually added to the tree, which tells me it’s not actually being loaded into memory until then. And I know this is true because exiting out of the scene (which removes it from the tree) RAM usage stays high, and loading it again is much more snappy.
Here’s my scene switching code:

extends Button
#A bit complicated but makes switching scenes easy and trouble-free

@export_file("*.tscn") var nextScenePath
@export var triggerLoadingScreen : bool = false

@export_group("Extra options...")

## Wether or not to automatically connect the changeScene function to a button's pressed signal.
## This serves to make it quicker to program buttons that change scenes
@export var autoconnectButtonPress : bool = true

var transitionScreen = preload("res://Scenes/Menu/TransitionScreen.tscn")
var nextScene : PackedScene

func _ready():
	#Connect pressed signal (if enabled)
	if autoconnectButtonPress:
		if self is Button:
			pressed.connect(changeScene)
	
	#Ensure the next scene path is valid (otherwise reset to default one)
	if nextScenePath==null:
		printerr("You forgot to select a target scene, dumbass.	-",self)
		nextScenePath = "res://Scenes/Menu/MainMenu.tscn"
	else:
		ResourceLoader.load_threaded_request(nextScenePath,"",true,ResourceLoader.CACHE_MODE_REUSE)

func changeScene():
	
	print(ResourceLoader.load_threaded_get_status(nextScenePath))
	var nextSceneInstance: Node = ResourceLoader.load_threaded_get(nextScenePath).instantiate()
	#Instantiate transition screen and wait for it to cover the screen (if enabled).
	var currentScene = get_tree().get_current_scene()
	var transitionScreenInstance = transitionScreen.instantiate()
	if triggerLoadingScreen:
		get_tree().get_root().add_child(transitionScreenInstance)
		await transitionScreenInstance.screenCovered
	
	#var nextSceneInstance: Node = nextScene.instantiate()
	#Load the next scene and set it as current scene so things like restarting work
	if triggerLoadingScreen:
		nextSceneInstance.ready.connect(transitionScreenInstance.clear)
	get_tree().get_root().add_child(nextSceneInstance)
	get_tree().set_current_scene(nextSceneInstance)
	
	# Delete the previous scene
	print("Deleting scene ", currentScene)
	currentScene.queue_free()
	var ownerScene := get_owner()
	while not (ownerScene.get_parent() is Window):
		ownerScene=ownerScene.get_owner()
	
	# Also delete the root owner in case it's loaded somewhere else
	if not ownerScene.is_queued_for_deletion():
		print("Also deleting ", ownerScene," for good measure.")
		ownerScene.queue_free()

func reloadNextScene():
	#This is more of a utility function, for when the nextScenePath changes, if you do that for some reason
	#(eg, if the next scene is determined at runtime)
	#I probably should have built this into a "setTargetScene(string)" function but oh well, it works
	if nextScenePath==null || nextScenePath.right(5)!=".tscn":
		printerr("You forgot to select a target scene, dumbass.	-",self)
		nextScenePath = "res://Scenes/Menu/MainMenu.tscn"
	else:
		nextScene = load(nextScenePath)
	
# This next part is for debugging
func _process(_delta):
	if ResourceLoader.load_threaded_get_status(nextScenePath) != 3:
		var status = []
		ResourceLoader.load_threaded_get_status(nextScenePath,status)
		print(status)

If you’re curious, the scene I’m using to test this weighs 18kb

What part is slow - accessing, reading, or instantiating? I know OS or antivirus (or a virus) might be intercepting file access, checking it before allowing.
Then, the size of the scene being loaded doesn’t mean much if it references a bunch of dependencies internally.

The slow part seem to be adding the scene to the tree, as disabling that snippet of code specifically makes the lag spike shown in my profiler go away. I’m not running any antiviruses (or viruses) as I’m on Linux.
I will check dependencies for the scene tho

Update: there’s a few scenes worth of dependencies but they’re all really small too. Could those be the issue still?

Other resources like textures are also dependencies that need to be loaded. Also shader compilation for the first time can cause freeze ups.

Is there a way to ensure all the resources are preloaded accordingly? (I can’t use preload as the paths are dynamic)

All the things really don’t add up to a lot though still, but I guess I can try loading a version of the scene without the necessary resources and seeing what changes as an experiment

Well you can’t go around loading. You already do threaded loading. Just display a loader until it’s done. If you’re loading the next part in the middle of the game, you can start loading earlier if you’re sure that you’ll need it or mask the loader with slow opening doors and elevators that go for unspecified number of floors :slight_smile:

If you know your heavy resources, you can individually preload and cache them at startup using ResourceLoader. Subsequent loads that have those as dependencies should then use the caches.

I can’t really display a proper spinner, I think I mentioned it but the game just freezes until the add_child() function has finished. Is there a way to at least prevent that?

So the pause happens after the loading is finished? Then it’s likely shader compilation.

I’m not really sure what you’re doing with that code. The loading is started when that button scene’s _ready() is called. Once the button is pressed, you just assume the loading is finished and call load_threaded_get(). This function will block the main thread if loading is not done. So you should always first check the progress by calling load_threaded_get_status(). Only when it returns that 100% has been loaded, call load_threaded_get(). Otherwise that call will block the processing in your main thread and any spinning/loading animations you might run there will freeze.

I do plan on implementing that, for now I just wrote a code in _process() to tell me what it’s at. As for what’s causing the freezing, I don’t think I’m using any custom shaders but shader precompilation seems to be the issue as doing… This:

As suggested in the wiki seems to have fixed it. Wish there was some sort of RenderingServer.precompile() function.

Ok I actually looked at the visual profiler and what came out is that the parts taking up so much processing time are “Render Shadows (~510ms)”, “Depth prepass (~1070ms)” and “Render 3D transparent pass (~500ms)”.
Confirms that this isn’t a loading speed issue, it’s a rendering issue. But how to I fix this? Should I open another thread?

If this is a single spike, it still probably reflects shader compilation time.

I see. Is there a way to still have a smooth loading screen while it compiles shaders?

Start simple and just have a static screen. Those times aren’t that long.

Did you check in compilation monitors if it’s indeed compilation?


Oh huh… That’s unusual
If it’s relevant, Ive been using the compatibility renderer on the project