Overhaul resource duplication#100673
Conversation
fc28733 to
90ec9fc
Compare
90ec9fc to
bbfe1db
Compare
|
I tested both |
415b488 to
76c16f9
Compare
76c16f9 to
fa193cb
Compare
|
Dear reviewers. I'm sorry, but I have needed to update this PR with a bunch of changes to support additional duplicate modes, for greater flexibility and compatibility preservation. I've updated all the commit messages as well as the description of the PR, which includes them. I haven't rebased, so you can more easily see the changes recently made. |
fa193cb to
eda7476
Compare
|
I so very appreciate this PR and really look forward to these changes making it into the engine! Thank you for addressing this. In the meantime I wanted to mention that I am very lost in the differences between |
|
What is needed to get this merged? |
|
A look from Core maintainers. It's a pretty extensive change so it may need a lot of scrutiny. |
The problem is |
|
I tested the MRP from the issue and the array is empty after duplication. |
eda7476 to
be34608
Compare
|
@KoBeWi, thanks a lot for testing this. There was indeed a bug involving scripted properties. Given the new code deals directly with resources, it needed special treatment for those properties so the assignments are not rejected; namely:
I have fixed that in my latest push. A subsequent push will be made, rebased and with formatting fixed. |
be34608 to
5c0587f
Compare
|
Fixed. I've also added an update to the latest part of the PR description. Quoting it here:
|
Thanks to a refactor, `Resource::duplicate_for_local_scene()` and `Resource::duplicate()` are now both users of the same, parametrized, implementation.
`Resource::duplicate()` now honors deepness in a more consistent and predictable fashion. `Resource::duplicate_deep()` is added (instead of just adding a parameter to the former, for compatibility needs).
The behavior after this change is as follows:
- Deep (`deep=true`, formerly `subresources=true`):
- Previously, only resources found as direct property values of the one to copy would be, recursively, duplicated.
- Now, in addition, arrays and dictionaries are walked so the copy is truly deep, and only local subresources found across are copied.
- Previously, subresources would be duplicated as many times as being referenced throughout the main resource.
- Now, each subresource is only duplicated once and from that point, a referenced to that single copy is used. That's the enhanced behavior that `duplicate_for_local_scene()` already featured.
- The behavior with respect to packed arrays is still duplication.
- Formerly, arrays and dictionaries were recursive duplicated, with resources ignored.
- Now, arrays and dictionaries are recursive duplicated, with resources duplicated.
- When doing it through `duplicate_deep()`, there's a` deep_subresources_mode` parameter, with various possibilites to control if no resources are duplicated (so arrays, etc. are, but keeping referencing the originals), if only the internal ones are (resources with no non-local path, the default), or if all of them are. The default is to copy every subresource, just like `duplicate(true)`.
- Not deep (`deep=false`, formerly `subresources=false`): <a name="resource-shallow"></a>
- Previously, the first level of resources found as direct property values would be duplicated unconditionally. Packed arrays, arrays and dictionaries were non-recursively duplicated.
- Now, no subresource found at any level in any form will be duplicated, but the original reference kept instead. Packed arrays, arrays and dictionaries are referenced, not duplicated at all.
- Now, resources found as values of always-duplicate properties are duplicated, recursively or not matching what was requested for the root call.
This commit also changes what's the virtual method to override to customize the duplication (now it's the protected `_duplicate()` instead of the public `duplicate()`).
This in the scope of a duplication triggered via any type in the `Variant` realm. that is, the following: `Variant` itself, `Array` and `Dictionary`. That includes invoking `duplicate()` from scripts. A `duplicate_deep(deep_subresources_mode)` method is added to `Variant`, `Array` and `Dictionary` (for compatibility reasons, simply adding an extra parameter was not possible). The default value for it is `RESOURCE_DEEP_DUPLICATE_NONE`, which is like calling `duplicate(true)`. Remarks: - The results of copying resources via those `Variant` types are exactly the same as if the copy were initiated from the `Resource` type at C++. - In order to keep some separation between `Variant` and the higher-level animal which is `Resource`, `Variant` still contains the original code for that, so it's self-sufficient unless there's a `Resource` involved. Once the deep copy finds a `Resource` that has to be copied according to the duplication parameters, the algorithm invokes the `Resource` duplication machinery. When the stack is unwind back to a nesting level `Variant` can handle, `Variant` duplication logic keeps functioning. While that is good from a responsibility separation standpoint, that would have a caveat: `Variant` would not be aware of the mapping between original and duplicate subresources and so wouldn't be able to keep preventing multiple duplicates. To avoid that, this commit also introduces a wormwhole, a sharing mechanism by which `Variant` and `Resource` can collaborate in managing the lifetime of the original-to-duplicates map. The user-visible benefit is that the overduplicate prevention works as broadly as the whole `Variant` entity being copied, including all nesting levels, regardless how disconnected the data members containing resources may be across al the nesting levels. In other words, despite the aforementioned division of duties between `Variant` and `Resource` duplication logic, the duplicates map is shared among them. It's created when first finding a `Resource` and, however how deep the copy was working at that point, the map kept alive unitl the stack is unwind to the root user call, until the first step of the recursion. Thanks to that common map of duplicates, this commit is able to fix the issue that `Resource::duplicate_for_local_scene()` used to ignore overridden duplicate logic.
30fcfe5 to
23ac332
Compare
23ac332 to
6841b45
Compare
Calinou
left a comment
There was a problem hiding this comment.
Tested locally (by editing the 3D platformer demo project), it works as expected. Code looks good to me.
I like the addition of duplicate_deep() for more explicit code.
|
PS: I'm curious if this PR would affect a rebase of #86779 in any way. Is it still technically feasible? |
|
Thanks! |
|
Thank you! |
|
The enum name is wrongly formatted, which breaks our codegen. When it is used as an argument, in extension_api.json its type is specified as which desugars to: #define BIND_ENUM_CONSTANT(m_constant) \
::ClassDB::bind_integer_constant(get_class_static(), __constant_get_enum_name(m_constant, #m_constant), #m_constant, m_constant);godot/core/variant/type_info.h Lines 207 to 210 in 99f5a3d godot/core/object/class_db.cpp Line 1111 in 99f5a3d |


Fixes #74918.
2025-05-19: Fixes #96627.
TL;DR
This unifies the different paths in Godot for resource duplication so they all have the same base behavior and only differ in what is predictable. Also, an extremely comprehensive set of tests is added.
Long description
Each commit takes a self-contained step towards the overall goal. Quoting their messages for the details:
Overhaul
Resource::duplicate_for_local_scene()PROPERTY_USAGE_NEVER_DUPLICATE.Overhaul
Resource::duplicate()(Edited 2025-01-21.)Thanks to a refactor,
Resource::duplicate_for_local_scene()andResource::duplicate()are now both users of the same, parametrized, implementation.Resource::duplicate()now honors deepness in a more consistent and predictable fashion.Resource::duplicate_deep()is added (instead of just adding a parameter to the former, for compatibility needs).The behavior after this change is as follows:
deep=true, formerlysubresources=true):duplicate_for_local_scene()already featured.duplicate_deep(), there's adeep_subresources_modeparameter, with various possibilites to control if no resources are duplicated (so arrays, etc. are, but keeping referencing the originals), if only the internal ones are (resources with no non-local path, the default), or if all of them are. The default is to copy every subresource, just likeduplicate(true).deep=false, formerlysubresources=false):This commit also changes what's the virtual method to override to customize the duplication (now it's the protected
_duplicate()instead of the publicduplicate()).Overhaul
Variant::duplicate()for resources (Edited 2025-01-21.)This in the scope of a duplication triggered via any type in the
Variantrealm. that is, the following:Variantitself,ArrayandDictionary. That includes invokingduplicate()from scripts.A
duplicate_deep(deep_subresources_mode)method is added toVariant,ArrayandDictionary(for compatibility reasons, simply adding an extra parameter was not possible). The default value for it isRESOURCE_DEEP_DUPLICATE_NONE, which is like callingduplicate(true).Remarks:
Varianttypes are exactly the same as if the copy were initiated from theResourcetype at C++.Variantand the higher-level animal which isResource,Variantstill contains the original code for that, so it's self-sufficient unless there's aResourceinvolved. Once the deep copy finds aResourcethat has to be copied according to the duplication parameters, the algorithm invokes theResourceduplication machinery. When the stack is unwind back to a nesting levelVariantcan handle,Variantduplication logic keeps functioning.While that is good from a responsibility separation standpoint, that would have a caveat:
Variantwould not be aware of the mapping between original and duplicate subresources and so wouldn't be able to keep preventing multiple duplicates.To avoid that, this commit also introduces a wormwhole, a sharing mechanism by which
VariantandResourcecan collaborate in managing the lifetime of the original-to-duplicates map. The user-visible benefit is that the overduplicate prevention works as broadly as the wholeVariantentity being copied, including all nesting levels, regardless how disconnected the data members containing resources may be across al the nesting levels. In other words, despite the aforementioned division of duties betweenVariantandResourceduplication logic, the duplicates map is shared among them. It's created when first finding aResourceand, however how deep the copy was working at that point, the map kept alive unitl the stack is unwind to the root user call, until the first step of the recursion.Thanks to that common map of duplicates, this commit is able to fix the issue that
Resource::duplicate_for_local_scene()used to ignore overridden duplicate logic.Caveats (Edited 2025-01-21.)
It may be the case that user projects are relying on the former behaviors. On the one hand, most of the time that means they were taking unreasonable or buggy behaviors as good, so I'd say it's not a big problem to take the all workings away.
Notes on other duplication entry points (Added 2025-01-03.)
In order for the scope of this PR to be better understood and also have a list of potential changes for subsequent PRs, let's be mindful of the following:
Arrayproperties that are typed as object, ignoresDictionary.DictionaryandArray.*: All of the workflows above use shallow duplication. Therefore, even if not directly modified by this PR, each individual resource duplication is affected by the behavior changes to shallow resource duplication. (Added 2025-05-21): In order to keep the current behavior of resouces assigned to properties not being duplicated at all, a check is added to the
Nodeduplication algorithm.I've tested this locally and also the test suite is pretty comprehensive. Nonetheless, some extra testing is more than welcome.