-
-
Notifications
You must be signed in to change notification settings - Fork 21.3k
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 cyclic references in GDScript 2.0 #67714
Fix cyclic references in GDScript 2.0 #67714
Conversation
Please note: cyclic references ( |
1409d9d
to
f28d2ab
Compare
I renamed the PR to "Add support for cyclic references in GDScript 2.0". |
38bc7bd
to
268bd1c
Compare
@@ -251,7 +251,10 @@ bool GDScriptTestRunner::make_tests_for_dir(const String &p_dir) { | |||
return false; | |||
} | |||
} else { | |||
if (next.get_extension().to_lower() == "gd") { | |||
if (next.ends_with(".notest.gd")) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added the .notest.gd
exception here, as it was defined elsewhere, but not used. Practical for .gd
files that are loaded by tests, but not tests themselves.
3739d03
to
e2a35ba
Compare
Renamed the PR to "Fix cyclic references in GDScript 2.0", as this fixes a bug (missing cyclic references support). It is not a "new feature" per se. |
f19f994
to
74859af
Compare
I fixed #61386 in this PR because I used code that I added in this one, but I could separate the fix into another PR, if needed. |
Ref<GDScriptParserRef> singl_parser = get_parser_for(autoload.path); | ||
if (singl_parser.is_valid()) { | ||
Error err = singl_parser->raise_status(GDScriptParserRef::INTERFACE_SOLVED); | ||
if (err == OK) { | ||
result = type_from_metatype(singl_parser->get_parser()->head->get_datatype()); | ||
} | ||
} | ||
} else if (ResourceLoader::get_resource_type(autoload.path) == "PackedScene") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the fix for #61386 (the whole else if
).
74859af
to
2e39524
Compare
I just tried beta 6. This isn't quite working.
Player and Enemy both inherit Unit. Many classes including Enemy reference In previous versions we had CRC errors in Enemy when Player was typed as shown above, and we referenced Now in Beta 6, I get the following error in the console upon loading the editor or the game, and there's no reference at all to the GDScript location. The editor does load scenes, but the game does not run.
Upon running a scene, it breaks, and in the editor I'm presented with no info: It does not break on any gdscript. The Someone else has a similar issue, perhaps also caused by a CRC error they don't know about. #69213. If I can make an MRP, I'll make an issue or contribute to that one. However hopefully the cpp line and error will highlight the bug. |
@tinmanjuggernaut Please create an issue and a MRP if possible. The @rune-scape Does it ring a bell to you? |
#69259 fixed my errors above using cyclic references. Thanks @adamscott . |
* Moved the order of progress update to after the actual resource loading to give better % numbers. * Fix a bug introduced by godotengine#67714, which broke cache ignoring.
* Moved the order of progress update to after the actual resource loading to give better % numbers. * Fix a bug introduced by godotengine#67714, which broke cache ignoring.
TL;DR
This PR makes possible to use cyclic references in GDScript 2.0, i.e. preload class (or use by class name)
A
inB
andB
inA
, preload cyclic scenes, use typing in addons.Summary
This PR tweaks the engine to accept and support cyclic references.
What it tries to solve
The current GDScript engine blocks cyclic references as this would normally lead to memory leaks. Why? Because cyclic references artificially increase the refcount of gdscripts.
Standard case
Let's say
Main
loadsA
that loadsB
.Here,
Main
reference count is 1 (referenced by the engine),A
is 1 (referenced byMain
) andB
is 1 (referenced byA
).When the engine unloads,
Main
reference count will be decremented by 1 and will be deleted, because its reference count will hit 0.A
andB
will also be deleted becauseA
lost his reference fromMain
andB
will lose its reference fromA
later.Current cyclic reference issue
Currently, the engine makes it so that using cyclic references is impossible. Why, you say?
Let's bring back our example, but with a cyclic reference. Let's say
Main
loadsA
that loadsB
that loads backA
.Here,
Main
reference count is 1 (referenced by the engine),A
is 2 (referenced byMain
andB
) andB
is 1 (referenced byA
).Do you see the issue? When
Main
will be deleted, there will be no way to deleteA
andB
.A
will lose its reference toMain
and will decrement, but it will not be deleted asA
reference count is now 1.B
is not deleted because it didn't lose its reference toA
.So, the answer is: the engine makes cyclic references impossible due to memory leaks concerns. That explains errors thrown by the parser or the analyzer like
Constant value uses script from "res://b.gd" which is loaded but not compiled
.Proposed solution
Cyclic dependency detection
There is no way to use the reference count as a mean to know if a cyclic
Ref<RefCounted>
needs to be disposed of. IfA
andB
refers to themselves a number of times, the reference count will skyrocket and it would not be easy to track down the exact number of times the count has been incremented.If, someday or someone, comes up with a reliable way to detect these, it would be the ideal solution.
Meanwhile, to fix this issue, I propose to use the existing
SelfList<GDScript> script_list
found in theGDScript
class. This list holds eachGDScript
instance.Take this table as basis for my following example:
Main
A
,B
,X
A
B
C
C
D
,E
D
E
F
F
E
X
Y
Y
F
,Z
Z
On the left column, each script contained in
script_list
and on the right one, each script referenced, contained in its constants (script constants, function constants, ...).In the example, if
B
is disposed,C
andD
will be too, but notE
andF
. How to make sure they are disposed correctly? And how to make sure to not dispose any script that can be legitimately used by another one?When we dispose of
B
, we can check the dependencies of its dependencies. After all, there's a good chance that those will be disposed of withB
. So,B
will check itsget_must_clear_dependencies()
."Must clear" dependencies
GDScript::get_must_clear_dependencies()
is called inGDScript::clear()
to know which dependencies must be forcibly cleared.The process may seem complicated, but it's quite simple. Let's say
Main
runsget_must_clear_dependencies()
.With this, we know for a fact that each dependency in the "must_clear" set can be deleted without impacting the rest of the scripts, even if their reference count is high. That means that it's high only because of cyclic references.
By clearing each of those scripts (
GDScript::clear()
clears all the constants holding scripts), their reference count will necessarily drop to zero, so they will be disposed without any need of hacks.Example
Must clear dependencies of
Main
Must clear dependencies of
Main
is quite simple. It's everyGDScript
because every script comes fromMain
.Step 1
The direct dependencies of
Main
areA
,B
andX
, but as it includes all the dependencies of the dependencies, it includes every class in the diagram.Step 2
A
Main
B
Main
C
B
D
C
E
C
,F
F
E
,Y
X
Main
Y
X
Z
Y
Step 3
As every class is a dependency of
Main
, "can't clear" set is empty.Step 4
As "can't clear" is empty, the dependencies stay the same.
Step 5
It returns every classes.
Must clear dependencies of
B
This is where
must_clear_dependencies
shine, because it doesn't clear everything this time. Based on the algorithm, onlyC
andD
can be cleared without any consequences, asE
andF
are used byY
.Step 1
B
C
,D
,E
,F
The direct dependency of
B
isC
, but as it includes all the dependencies of the dependencies, it includesD
,E
andF
.Step 2
C
B
D
C
E
C
,F
F
E
,Y
Step 3
Y
is outside ofB
(itself),C
,D
,E
andF
. So, we addF
,E
andY
to the "can't clear" set.Step 4
We remove
F
andE
from the dependencies.Step 5
It returns
C
andD
.Backup
If, for some reason, a script was able to slip out of the process, it still has a reference to
script_list
. Then, we can callclear()
in the loop found inGDScriptLanguage::~GDScriptLanguage()
for script instances still left. This adds the ability to force the clear of internal variables of a script, which can trigger the reference count decrement of other scripts.Known issues
Code completion isn't working for cyclic referencesThe code works, but the cleaning isn't optimal. I found a reliable way to calculate cyclic references (seeGDScript::get_cyclic_references_count()
), but my efforts to clear dynamically the script failed. It seems theunreference()
function is somewhat unreliable.GDScript::clear()
andGDScript::get_must_clear_dependencies()
;Shape2D
references, even ifPhysicsServer2D
singleton was destructed, which leads to memory leaks.Shape3D
Code example
So, this code works with this PR, which returns this error on
master
:Constant value uses script from "res://b.gd" which is loaded but not compiled.
:Minimal reproduction project:
cyclic3.zip
Fixes
Fixes #21461 - Some usages of
class_name
can produce cyclic errors unexpectedlyFixes #32165 - Cyclic Errors After Autoload of Imported Asset
Fixes #41027 - Cyclic reference error in GDScript 2.0
Fixes #58551 - Can't preload a scene in AutoLoad script when the scene's script references the AutoLoad
Fixes #58181 - GDScript 2.0: Cannot preload cyclic const script references
Fixes #58871 - Cyclic Autoload Singleton Plugin Bug
Fixes #61043 - New cyclic reference error with autoload and preloaded script
Fixes #65263 - GDScript reload generates error when created dynamically (no backing script)
Fixes #68211 - Can't preload a scene in autoload if that scene uses the autoload
Does not fix (yet)
#61386 - [4.x regression] AutoLoad scenes (not scripts) do not support implicit types
Notes
This PR follows up #65752.