Page MenuHome

Fix ids in 'master_collection' always pointing to the original version
Needs ReviewPublic

Authored by Germano Cavalcante (mano-wii) on Dec 21 2021, 8:15 PM.

Details

Summary

Context

The master_collection, referenced in scene->master_collection,
although it works like a collection, is not an actual datablock, but a
member belonging to the scene.

Unlike other collections, it has some special features, such as:

  • Cannot be deleted, renamed, moved, copied, disabled or hidden
  • Cannot be referenced in another collection
  • It is the first collection of the View Layers
  • It is a top level and the only one of the level

It also has some common collection features, such as:

  • It has a list of objects
  • Has a list of other collections

As it is a member belonging to the scene and not a datablock, all the
ids it points to (objects and collections) should theoretically be
referenced in the scene (not the master_collection).

But that's not what we see in the scene_foreach_id code.

In that function, the master_collection is passed as an independent id
where all the other ids it references are "embedded" by the scene.

This causes the referenced objects and collections to be ignored when
the IDWALK_IGNORE_EMBEDDED_ID flag is set.

And so, the master_collection, even being the evaluated version, still
points to original objects and collections.

Currently this is not a problem, as any change to a collection is
updated through the RNA functions in the original collection.

But this can be problematic in patches like D12469: Collections: Support for animating/driving collection properties… where the
depsgraph is needed to update a collection.

Solution

Traverse each "embedded" ID in master_collection in update_id_after_copy (which is called in only one place).

Testing

This patch is also applied on D12469, so to test the solution just:

  • apply that patch
  • in blender make an animation with the visibility of any collections.

If the animation works and no assert is triggered, then the patch was successful.

Diff Detail

Repository
rB Blender
Branch
arcpatch-D13648 (branched from master)
Build Status
Buildable 21121
Build 21121: arc lint + arc unit

Event Timeline

Germano Cavalcante (mano-wii) requested review of this revision.Dec 21 2021, 8:15 PM
Germano Cavalcante (mano-wii) created this revision.

I think the proper reviewers for this should be B&B, not S&S (Brecht and Bastien, not Sergey and Sybren).

Bastien Montagne (mont29) requested changes to this revision.Dec 22 2021, 10:00 AM

This patch is explicitly breaking the very purpose of IDWALK_IGNORE_EMBEDDED_ID, which is (as its description says) to 'not process ID pointers inside embedded IDs'.

It is also violating the whole idea behind current work on refactoring our ID management code, which is to have ID-type-agnostic handling, and get rid of as many special corner cases as possible.


And so, the master_collection, even being the evaluated version, still points to original objects and collections.

Well that is a bug in depsgraph handling of this case then. But it is absolutely not to be handled by breaking generic 'foreach-id' code.

NOTE: am not an expert in depsgraph, but iirc it deals with embedded IDs as if they were regular independent ones? At lest think that's the case for nodetrees, and that is why that IDWALK_IGNORE_EMBEDDED_ID special behavior had to be implemented?
This revision now requires changes to proceed.Dec 22 2021, 10:00 AM

@Bastien Montagne (mont29), as we can see in a comment on BKE_collection_master_add the master_collection is not a datablock, it is a member that uses the struct of Collection, but everything in it is evaluated along with the scene.
Therefore, as far as I can understand, the master_collection cannot be considered an embedded ID/datablock, but the IDs inside it are the embedded ones.

master collections are embedded ID just like root nodetrees, as tagged with LIB_EMBEDDED_DATA

Germano Cavalcante (mano-wii) planned changes to this revision.Dec 22 2021, 5:23 PM
  • Try using a less hacking solution (Remap scene-specific pointers as a "special treatment")

The title of the patch needs some adjustment.

The direction seems to be good: more consistent separation of evaluation and original ID. However, from my understanding to make it fully correct an old standing TODO is to be implemented. See the comment in the scene_setup_view_layers_after_remap. If a base from master collection is not in any animated collections it will not be a part of the dependency graph, hence remapping will not work reliably.

Germano Cavalcante (mano-wii) edited the summary of this revision. (Show Details)
  • Remove disabled objects from the viewe_layer collections

I confirmed that some non-evaluated objects are still present in the master_collection if their base is disabled.
This caused view_layer_objects_base_cache_validate to fail with CLOG_FATAL.

Germano Cavalcante (mano-wii) retitled this revision from Fix master collection not having their "embedded" IDs traversed to Fix ids in 'master_collection' always pointing to the original version.Mar 19 2022, 7:52 PM
Germano Cavalcante (mano-wii) edited the summary of this revision. (Show Details)
source/blender/depsgraph/intern/eval/deg_eval_copy_on_write.cc
453

layer_collection or lc. Former is preferred in the new code I'd say.

459

The layer collection which is given to the function is evaluated, and intuitively its objects are remapped to evaluated IDs. The deg_check_id_in_depsgraph expects original ID. So there is mismatch, and possibly missing remapping.

508–509

Why only first layer collection?

Germano Cavalcante (mano-wii) marked 3 inline comments as done.
  • Rename layer -> layer_collection
  • Detect if the object is to be removed by simply checking if it is the evaluated version (instead of using the deg_check_id_in_depsgraph)

I still need to test applying it on the D12469 but I believe it is ready for the review.

source/blender/depsgraph/intern/eval/deg_eval_copy_on_write.cc
459

Jeez, I noticed later when reading this function! (not used to working on different OS)

If the object is the evaluated version, that indicates that it is "in depsgraph" right?
In that case maybe the deg_check_base_in_depsgraph function could be optimized a little bit by checking base->object != base->base_orig->object?

Basically I "resolved" by checking if the object is the evaluated one.

I also renamed the is_object_enabled variable to is_in_depsgraph, as this is apparently what the other function intends to check.

508–509

This also makes me a little confused.
The comment in {https://developer.blender.org/diffusion/B/browse/master/source/blender/makesdna/DNA_layer_types.h$0-139} says:

/** A view layer has one top level layer collection, because a scene has only one top level
 * collection. The layer_collections list always contains a single element. ListBase is
 * convenient when applying functions to all layer collections recursively. */

So this "convenience" depends on whether in the recursive function it is preferable to pass a list or the collection directly.
(Personally I think it would be preferable if this were a collection instead of a list).

source/blender/depsgraph/intern/eval/deg_eval_copy_on_write.cc
459

Being in the dependency graph and being evaluated are different things. Here you really interested whether the object is in the graph or not.

Unless you have a measurable speedup I will strongly suggest doing generic semantically easy to understand check.

508–509

Interesting.

Still wouldn't really write logic relying on such specifics, especially since it is quite easy to write a generic code:

void view_layer_remove_disabled_objects_in_collections_recursive(const Depsgraph *depsgraph, const ListBase& layer_collections);
void view_layer_remove_disabled_objects_in_collection_recursive(const Depsgraph *depsgraph,
                                                                LayerCollection *layer_collection) {
  ...
  view_layer_remove_disabled_objects_in_collections_recursive(depsgraph, layer_collection->layer_collections);
}
void view_layer_remove_disabled_objects_in_collections_recursive(const Depsgraph *depsgraph, const ListBase& layer_collections) {
  LISTBASE_FOREACH (LayerCollection *, layer_collection, &layer_collections) {
    view_layer_remove_disabled_objects_in_collection_recursive(depsgraph, layer_collection);
  }
}
...
view_layer_remove_disabled_objects_in_collections_recursive(depsgraph, view_layer_eval->layer_collections);
Germano Cavalcante (mano-wii) marked an inline comment as done.EditedMar 21 2022, 5:18 PM

Much of what is done in view_layer_remove_disabled_objects_in_collections_recursive(...) was originally based on view_layer_remove_disabled_bases(...) which checks whether the object is_object_enabled by checking if the original version pointed to by the Base is in the depsgraph.

In my opinion, the is_object_enabled description itself is misleading, as "disabled" objects can remain in view_layer->object_bases if it is in depsgraph.

The description of view_layer_remove_disabled_bases says:

"We are using original base since the object which evaluated base points to is not yet copied...".

But the function where it's called is named "scene_setup_view_layers_after_remap", if it's after remap, then why aren't the objects in view_layer->object_bases the "copied" ones? Or are they?


The real purpose of view_layer_remove_disabled_objects_in_collections_recursive is to remove objects that no longer have Base in view_layer->object_bases (because they were removed in view_layer_remove_disabled_bases).

For that we have some solutions:

  • Do the same test on view_layer_remove_disabled_bases to check what was removed or
  • Once a Base is removed in view_layer_remove_disabled_bases look for all references to the base object in the view_layer collections. Or
  • Use BLI_ghash_lookup(view_layer->object_bases_hash, cob->ob) == nullptr or BKE_view_layer_base_find or
  • Somehow tag the objects that are removed.

For the first solution, it would be more efficient and safe to change how tests are done in view_layer_remove_disabled_bases. Using DEG_is_evaluated_object alone would be nice.

The second solution appears to be of O(n*m) complexity in the worst case.

For the third solution, we would have to initialize the lazy object_bases_hash. But, except with patch D12469, object_bases_hash is never initialized to an evaluated view_layer.
Creating this hash map seems to remove the purpose of the lazy initialization.

For the fourth solution, as far as I've seen, there doesn't seem to be a support for temporarily tagging an object.

(There must be other solutions as well).

Germano Cavalcante (mano-wii) marked an inline comment as done.
  • Pass the ListBase instead of LayerCollection as param
  • Updates based on the proposed change to only use DEG_is_evaluated_object

The remap operates on pointers. The pointer values (aka memory addresses) are known after building a depsgeraph. The issue is: even if you know pointer it doesn't mean you can dereference it in a meaningful manner. When the copy-on-write operation is run on a view later it is not guaranteed that copy-on-write operation was run on all objects which the view layer uses (in fact, I am not sure that is even possible, since intuitively object depends on view layer, not vice versa). Hence reading from evaluated object's fields from view layer cop-on-write is is malformed: the result will depend on exact threading order, could result reading zero-ed memory from the evaluated ID memory allocation, or could result accessing stale data from the previous dependency graph evaluation. Accessing original ID from the copy-on-write operation is OK since the original IDs are expected to be available at that time, and it does allow to overcome the evaluation order described above without going into complicated relations which will be hard to maintain and will have negative effect on performance.

The remap only remaps IDs which are "known" by the dependency graph, and leaves "unknown" at their original values. On the one hand this is because not all IDs are covered with copy-on-write, and on another hand this is because the remapping function does not know how to properly remove ID which is not needed by the dependency graph from user of that ID. The copy-on-write is copying an entire datablock, similar to what would happen if you duplicate the ID from the UI.

So in order to have a copy-on-written ID datablock which doesn't reference original IDs when the ID is expected to be covered with copy-on-write the following steps are needed:

  1. Duplicate datablock into a pre-allocated memory.
  2. Run generic function to remap known evaluated pointers.
  3. Remove parts of the datablock which are not relevant to the current dependency graph.

The order is such due to convenience of having first two steps happening together as they are needed by all ID types, and after that a per-ID code can be run.
This should also explain why in the middle of the process you can have view layer referencing objects which are not covered by the depednecy graph.

If you have some concrete suggestions about how to make it more clear and obvious in the code please let us know.

For the first solution, it would be more efficient and safe to change how tests are done in view_layer_remove_disabled_bases. Using DEG_is_evaluated_object alone would be nice.

The tag of the object being evaluated is not guaranteed to be assigned yet. Similarly to the reason why you can not access fields of evaluated object described above.
Using deg_check_id_in_depsgraphon the original's object's ID is currently the way to do it. The tricky part is to get an original ID from evaluated one without dereferencing the evaluated. Iterating over both original and evaluated collections (similar to how it is done in the view_layer_update_orig_base_pointers for bases) is a bit verbose but easy-to-fit into the current design solution.

The complexity will be O(N * log(N)) which isn't too horrible and is not worse that it is currently (due to complexity of view_layer_remove_disabled_bases). If optimizing this step shows up user measurable performance improvement the new can look into complicating the code to gain this performance. Until then just live within similar ballpark.