-
-
Notifications
You must be signed in to change notification settings - Fork 21.6k
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 generic arrays and dictionaries in .NET not calling set_typed
#98545
Fix generic arrays and dictionaries in .NET not calling set_typed
#98545
Conversation
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.
Overall I think these are good changes but it's a bummer the calls are so much slower. I'm assuming that untyped vs typed performance difference is similar when using gdscript? If _p->typed.validate
is that expensive, it might be worth (in a different PR) adding internal unchecked_set
methods and using those when calling from a fully typed gdscript or generic c# context.
_underlyingArray = new Array(); | ||
SetTypedForUnderlyingArray(); |
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.
now that there is more common logic, should these array and dictionary constructors generally use : this() { .. }
or is it still worth checking/throwing prior?
@@ -1137,6 +1156,16 @@ public Array(Array array) | |||
throw new ArgumentNullException(nameof(array)); | |||
|
|||
_underlyingArray = array; |
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.
it's existing code but imo it's a little weird that these end up with mutable externally provided underlying arrays, and as a result if array
is not typed and not empty, we can't set typing on the native array. It's probably not worth shallow copying the input array to get around this, but it's a bummer that it won't be able to fix cases where we have a existing saved untyped array as in #98309. At least in that case the data just has to be regenerated. Maybe it would be worth changing set_typed
to allow checking of the existing elements, although in this case it may also run against the refcount <= 1 constraint.
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 personally think that the constructor should shallow copy, because right now you can have the odd situation where you have two collections that are not the same according to object.ReferenceEquals
, but nonetheless refer to the same engine-side array and as such will observe changes made by each other.
However, changing that would be a breaking change with user-observable effects (IIRC this is relevant when there are multiple instances of the same scene) and at the very least I think it's not within scope for this PR.
Also worth noting is that were this constructor changed to shallow copy, the implementation of the Duplicate
method should also change to remain as efficient as possible.
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.
Yeah I agree it should be the behavior but given the current use it would be both a breaking change and make existing code paths of native -> c# array slower; maybe godot 5 lol? I think the current constructors with that behavior could be made internal and explicitly for cases like CreateTakingOwnershipOfDisposableValue
, and then the public constructor making a shallow copy still benefits over the c# collection constructors since it knows it can make a single api call to duplicate rather than 2N calls to read/insert.
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.
Thank you so much for this amazing work, and sorry it took me so long to review.
I appreciate how much work you put into this, trying to reduce the overhead as much as possible, benchmarking the changes, and ensuring NativeAOT works.
My only concern is with setting the type for the underlying array/dictionary in the constructors that "borrow" a reference to an untyped array/dictionary. I share the opinion that these constructors should shallow copy, but unfortunately that'd be a breaking change.
StringName? nativeBase = GodotObject.InternalGetClassNativeBaseName(scriptType); | ||
|
||
godot_string_name nativeBaseName = nativeBase != null | ||
? NativeFuncs.godotsharp_string_name_new_copy((godot_string_name)nativeBase.NativeValue) |
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.
Do we need to copy the StringName?
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'm fairly certain that this is necessary. At the very least, it's consistent with how ScriptManagerBridge.GetScriptNativeName
handles passing a managed StringName to unmanaged code.
If I'm understanding this correctly, it's effectively the same as an implicit call to StringName's copy constructor within C++. This is important to maintain a correct refcount; I tested what happens when a copy isn't made, and I get the following errors when I close the editor:
ERROR: BUG: Unreferenced static string to 0: EditorExportPlugin
at: unref (core/string/string_name.cpp:144)
ERROR: BUG: Unreferenced static string to 0: HBoxContainer
at: unref (core/string/string_name.cpp:144)
ERROR: BUG: Unreferenced static string to 0: EditorPlugin
at: unref (core/string/string_name.cpp:144)
ERROR: BUG: Unreferenced static string to 0: MarginContainer
at: unref (core/string/string_name.cpp:144)
ERROR: BUG: Unreferenced static string to 0: EditorInspectorPlugin
at: unref (core/string/string_name.cpp:144)
ERROR: BUG: Unreferenced static string to 0: RefCounted
at: unref (core/string/string_name.cpp:144)
ERROR: BUG: Unreferenced static string to 0: Node2D
at: unref (core/string/string_name.cpp:144)
I suppose given the static lifetime of the StringNames returned by GodotObject.InternalGetClassNativeBaseName
, it might be safe to pass a pointer to the underlying native form of that instead of constructing a new one, but I am not 100% sure about that so I went with this since I'm more confident that it's safe.
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.
Yes, I think you are 100% correct. I was also thinking about passing a pointer to the underlying native form, but I agree it's safer to do the copy.
modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs
Outdated
Show resolved
Hide resolved
modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs
Outdated
Show resolved
Hide resolved
{ | ||
unsafe | ||
{ | ||
Godot.Bridge.ScriptManagerBridge.GetOrLoadOrCreateScriptForType(typeof(T), &scriptRef); |
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'm guessing we don't expect this to ever create a script here, since if T
is not a native class it must be a C# script so a script reference should already exist.
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.
It is possible for the construction of a collection to trigger ScriptManagerBridge.CreateScriptBridgeForType
. Here's an example project illustrating this: RecursiveReferenceThruArrays.zip
The stacktrace for such a call looks like this:
at System.Environment.get_StackTrace()
at Godot.Bridge.ScriptManagerBridge.CreateScriptBridgeForType(Type scriptType, godot_ref* outScript) in /***/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs:line 565
at Godot.Bridge.ScriptManagerBridge.GetOrCreateScriptBridgeForType(Type scriptType, godot_ref* outScript) in /***/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs:line 469
at Godot.Bridge.ScriptManagerBridge.GetOrCreateScriptBridgeForPath(godot_string* scriptPath, godot_string* c_backtrace, godot_ref* outScript) in /***/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs:line 452
at Godot.Bridge.ScriptManagerBridge.GetOrLoadOrCreateScriptForType(Type scriptType, godot_ref* outScript) in /***/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs:line 540
at Godot.NativeInterop.Marshaling.GetTypedCollectionParameterInfo[T](Type& variantType, godot_string_name& className, godot_ref& script) in /***/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs:line 222
at Godot.Collections.Array`1.SetTypedForUnderlyingArray() in /***/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs:line 1065
at Godot.Collections.Array`1..ctor() in /***/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs:line 1102
at ScriptNodeA.GetGodotPropertyDefaultValues() in /home/juan/GodotMrps/RecursiveReferenceThruArrays/Godot.SourceGenerators/Godot.SourceGenerators.ScriptPropertyDefValGenerator/ScriptNodeA_ScriptPropertyDefVal.generated.cs:line 15
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
at Godot.Bridge.ScriptManagerBridge.GetPropertyDefaultValuesForType(Type type, IntPtr scriptPtr, addDefValFunc) in /***/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs:line 1125
at Godot.Bridge.ScriptManagerBridge.GetPropertyDefaultValues(IntPtr scriptPtr, addDefValFunc) in /***/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs:line 1100
at Godot.Bridge.ScriptManagerBridge.CreateScriptBridgeForType(Type scriptType, godot_ref* outScript) in /***/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs:line 572
at Godot.Bridge.ScriptManagerBridge.GetOrCreateScriptBridgeForType(Type scriptType, godot_ref* outScript) in /***/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs:line 469
at Godot.Bridge.ScriptManagerBridge.GetOrCreateScriptBridgeForPath(godot_string* scriptPath, godot_string* c_backtrace, godot_ref* outScript) in /***/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs:line 452
I'm not aware of any issues that might arise in this scenario.
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 see, thank you. I don't think there should be any issues with this.
3dc784f
to
8ab27a7
Compare
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.
Looks good to me, thanks!
Thanks! |
See #97948 (review), the changes made here are based on this feedback.
I wasn't able to find a way to completely avoid using reflection, but since
ScriptManagerBridge
does rely on it for a few things, I figured that I could generalize what it does use and get something that's still going to work with trimming.Here are some highlights in this PR:
CSharpScript::get_instance_base_type
now returns a cached StringName that is passed in byGodot.Bridge.ScriptManagerBridge.UpdateScriptClassInfo
. This reduces interop overhead and the use of reflection, which is important to speed up thegodotsharp_*_set_typed
functions.GodotObject.InternalGetClassNativeBase
is no longer recursive. Also, it now performs a direct comparison with a reference to theGodotSharp
assembly, and only performs a name comparison in the editor because getting an assembly's name has some overhead.GodotObject.InternalGetClassNativeBaseName
method. This method caches the StringNames it returns, to avoid always getting the static fields' values through reflection.I benchmarked this PR using the following project: TypedCollectionBenchmarkDotnet.zip. I've made sure that it works on NativeAOT.
Here are my results:
JIT
NativeAOT