Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix loading of cyclic scenes from GDScript #92326

Conversation

adamscott
Copy link
Member

@adamscott adamscott commented May 24, 2024

Makes it possible for ClassA of SceneA to refer to ClassB of SceneB that refers to ClassA back.

This PR creates Ref<Resource> ResourceLoader::get_loading_resource(const String &p_path, Error *r_error). This function returns a (shallow) resource that is currently being loaded. If not, it returns nullptr and the FAILED error. This is important, because most of the time, the returned reference will be not valid, but will still point later to the loaded resource.

This allows GDScriptAnalyzer to "load" a PackedScene when analyzing a script file even if the scene is currently loading, preventing a cyclic load error.

Fixes #70985
Fixes #90362
Fixes #90954

@adamscott adamscott force-pushed the gdscript-cyclic-scenes-workaround branch 2 times, most recently from e0519cb to a1e390b Compare May 25, 2024 13:31
@adamscott
Copy link
Member Author

Please note: I changed the name of the function from get_shallow_resource to get_loading_resource as it seems that the name better explains what the function does: the function will fail if the resource has been successfully loaded.

// Returns a shallow reference of the loading resource.
// Will fail if the resource is not loading.
Ref<Resource> ResourceLoader::get_loading_resource(const String &p_path, Error *r_error) {
ERR_FAIL_COND_V(r_error == nullptr, nullptr);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If passing is required I'd pass a reference instead to make the requirement explicit

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given there's no nuance to report about the error, I think returning a null Ref is enough, so r_error can go away.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're at it, I'm not very sure about the "shallow" wording. Since this is async, by the time the caller deals with the resource, the completeness in its properties having been set and references to dependencies being filled ranges from none (we could call that "shallow") to full. Also, this wouldn't be thread safe, in the sense that there's no sync point with the potentially arbitrary loading thread, which can lead to the caller not only seeing an incomplete resource, but running into data races or race conditions.

Do you think it would be an option to wait for the full resource to be loaded? That would guarantee safety.

If the caller can be happy with the resource in a shallow state (that is, own properties set) the loader needs to be enhanced to support it properly. However, bear in mind that the caller can't safely access anything that can still change (references to external resources, properties already set but that will change due to one of those "queued update" callbacks running later, etc.).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For GDScript this doesn't matter in most cases. Only if the user preload() a currently loading resource and do something with the resource at the point. This is probably only possible in a static initializer (explicit or implicit).

If we choose this approach, then we should add a comment that ResourceLoader::get_loading_resource() is intended for GDScript (due to how it works) and should not be used elsewhere in the engine.

Alternatively, we could make such a "shallow" resource null until the end of loading. In both cases it will look unexpected to the user. Not sure which option is better, but probably null option is more explicit.

If the required non-GDScript resource is currently being loaded, then we should not call ResourceLoader::load(). But we need get it sometime, and its loading will complete later than the script. What could be bad if the script executes a code before that, for instance the static initializer.

Here we have two options:

  1. Add a hook to ResourceLoader that allow us to get the resource when it is fully loaded. Until than it is null in the script.
  2. Add a posibility to ResourceLoader to get a shallow resource, i.e. a currently loading resource, that is not fully initialized yet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a thought here: I think this only needs to be aware of other loads happening on the same thread (in other words, in the same stack). In that case, ResourceLoader could have a different function for that case explicitly that wouldn't involve any interaction with other threads. I could implement it if you think that'd do the trick.

Copy link
Member Author

@adamscott adamscott Jun 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given there's no nuance to report about the error, I think returning a null Ref is enough, so r_error can go away.

It's impossible. 99% of the time, the returned reference will be null. Hence why I force the error return value. We can't differentiate a successful and a failing request only by the returned reference, as both will be null.

Copy link
Member Author

@adamscott adamscott Jun 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we choose this approach, then we should add a comment that ResourceLoader::get_loading_resource() is intended for GDScript (due to how it works) and should not be used elsewhere in the engine.

I don't think it's a good idea. Like, I think we could push this technique to support cyclic loading elsewhere too (cyclic node scenes references?). But I agree that this function should have a warning or something like that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a thought here: I think this only needs to be aware of other loads happening on the same thread (in other words, in the same stack). In that case, ResourceLoader could have a different function for that case explicitly that wouldn't involve any interaction with other threads. I could implement it if you think that'd do the trick.

Elaborating on that:

Since this PR is about a script that is cyclically referenced, ResourceLoader already has a data member that fits the bill: load_paths_stack. That's unique per thread, so checking there is good as it keeps the concern within the dependency chain of the current load chain, deterministically as no threads are involved. However, that vector would need to be modified to it contains the resource references as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this PR is about a script that is cyclically referenced

This PR is more about anything that is cyclically referenced in the context of script loading.

modules/gdscript/gdscript_analyzer.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_analyzer.cpp Outdated Show resolved Hide resolved
@adamscott adamscott force-pushed the gdscript-cyclic-scenes-workaround branch 3 times, most recently from a308f6d to 71fda08 Compare June 4, 2024 16:30
core/io/resource_loader.cpp Outdated Show resolved Hide resolved
@akien-mga
Copy link
Member

CC @rune-scape as you've been poking at loosely related issues I think, and we could use more eyes on this important fix for release blocking issues.

@adamscott adamscott force-pushed the gdscript-cyclic-scenes-workaround branch from 71fda08 to d8169fd Compare June 18, 2024 14:39
@adamscott
Copy link
Member Author

@dalexeev @RandomShaper I removed the cache check. ResourceLoader::get_loading_resource will fail if the resource has been loaded.

akien-mga
akien-mga previously approved these changes Jun 19, 2024
Copy link
Member

@akien-mga akien-mga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's YOLO merge and see what happens, this fixes a number of critical regressions.

We can iterate on the design of the core API later, or flag it as something that's a hack only for GDScript and shouldn't be used in other parts of the engine.

@akien-mga
Copy link
Member

Actually I tested again with the Planet Crasher MRP from #90362 (the ZIP archive, not the Git repo which has since changed and no longer seems to exhibit the bug).

This PR solves this error:

SCRIPT ERROR: Parse Error: Could not preload resource file "res://items/weapon_drop.tscn".
          at: GDScript::reload (res://characters/character.gd:420)
SCRIPT ERROR: Compile Error: 
          at: GDScript::reload (res://items/weapon_drop.gd:-1)
ERROR: Failed to load script "res://items/weapon_drop.gd" with error "Parse error".
   at: load (./modules/gdscript/gdscript.cpp:2951)

But introduces these errors instead:

ERROR: Parameter "method" is null.
   at: is_builtin_method_vararg (./core/variant/variant_call.cpp:1398)
ERROR: Parameter "method" is null.
   at: get_builtin_method_argument_count (./core/variant/variant_call.cpp:1324)
ERROR: Parameter "method" is null.
   at: get_builtin_method_return_type (./core/variant/variant_call.cpp:1377)
ERROR: Parameter "method" is null.
   at: get_validated_builtin_method (./core/variant/variant_call.cpp:1303)
ERROR: Parameter "method" is null.
   at: get_validated_builtin_method (./core/variant/variant_call.cpp:1303)

@akien-mga
Copy link
Member

Superseded by #93346. Thanks for the great work!

@akien-mga akien-mga closed this Jun 26, 2024
@akien-mga akien-mga removed this from the 4.3 milestone Jun 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
5 participants