Pruning prims based on their data source contents (and handling dirty notifications)?

Hello,

We are trying to create a filtering scene index that prunes out prims from the scene hierarchy. The condition that determines whether a prim is pruned out or not is based on the data contained in its data sources (for example in our case, if a mesh has >10k vertices). However, this is causing some issues when it comes to handling dirty notifications.

If we simply override GetPrim like this

HdSceneIndexPrim PruningSceneIndex::GetPrim(const SdfPath& primPath) const
{
    HdSceneIndexPrim prim = _GetInputSceneIndex()->GetPrim(primPath);
    return ShouldBePruned(prim) ? HdSceneIndexPrim() : prim;
}

we run into the following issue : if a prim is initially pruned out (say it has 15k vertices in our example), but we then receive a dirty notification and it should no longer be pruned (e.g. it now has 5k vertices), the downstream observers will receive a dirty notification for a prim which they have never known anything about, but most importantly : they won’t be aware of the other data sources that the prim might have and which weren’t dirtied.

A concrete example of this issue is that in our case, a prim which is initially pruned and then becomes visible will appear at the origin rather than at the position of its xform, as from the point of view of downstream observers, the prim was initially empty, was never added and no xform was dirtied.

Conversely, if a prim was initially not pruned out and should now be pruned, we should also notify observers of such a change. This is less of a problem as we can simply send a PrimsRemoved notification for pruned prims (and though they might get sent multiple times for the same prim, I believe these should be idempotent?)

I see two ways to resolve this issue, however both have significant downsides, so I was wondering if there was a recommended way to handle such situations.

Method 1 : When we receive a dirty notification for a prim, check if it is pruned : if it is, send a PrimsRemoved notifications, and if it isn’t, send a PrimsAdded notification to trigger a full re-sync. For example :

void PruningSceneIndex::_PrimsDirtied(
    const HdSceneIndexBase&                         sender,
    const HdSceneIndexObserver::DirtiedPrimEntries& entries)
{
    HdSceneIndexObserver::RemovedPrimEntries removedEntries;
    HdSceneIndexObserver::AddedPrimEntries   addedEntries;

    for (const auto& entry : entries) {
        HdSceneIndexPrim prim = _GetInputSceneIndex()->GetPrim(entry.primPath);
        if (ShouldBePruned(prim)) {
            removedEntries.emplace_back(entry.primPath);
        } else {
            addedEntries.emplace_back(entry.primPath, prim.primType);
        }
    }

    _SendPrimsRemoved(removedEntries);
    _SendPrimsAdded(addedEntries);
}

This has the obvious significant downside of causing full prim re-syncs each time any data source on a visible prim is dirtied, doing lots of wasteful work. I’ve also tried sending dirty notifications on the HdDataSourceLocatorSet::UniversalSet() instead of sending PrimsAdded notifications, but this was causing a crash in HdSt_FlatNormalsComputationGPU, and would still be just as wasteful anyways.

Method 2 : Track and store which prims are pruned. This solves the problem with method 1, but adds some statefulness and complexity to the code, as now :

  • We need to override _PrimsAdded and _PrimsRemoved to update whatever container stores the pruning status of prims
  • There are 4 different possible cases for each dirtied prim in _PrimsDirtied :
      1. Its pruning status did NOT change :
      • 1a. The prim is pruned → drop the DirtiedPrimEntry
      • 1b. The prim is visible → forward the DirtiedPrimEntry
      1. Its pruning status DID change :
      • 2a. The prim is now pruned → send a RemovedPrimEntry
      • 2b. The prim is now visible → send an AddedPrimEntry
  • In order to handle those 4 _PrimsDirtied cases correctly at all times, we need to have the correct initial pruning status for each prim, which would require traversing the whole scene hierarchy in the constructor, e.g. :
PruningSceneIndex::PruningSceneIndex(const HdSceneIndexBaseRefPtr& inputSceneIndex)
    : HdSingleInputFilteringSceneIndexBase(inputSceneIndex)
{
    std::stack<SdfPath> primPathsToTraverse({ SdfPath::AbsoluteRootPath() });
    while (!primPathsToTraverse.empty()) {
        SdfPath currPrimPath = primPathsToTraverse.top();
        primPathsToTraverse.pop();
        UpdatePruningStatus(currPrimPath); // This would be the method to check and store the initial pruning status
        for (const auto& childPath : inputSceneIndex->GetChildPrimPaths(currPrimPath)) {
            primPathsToTraverse.push(childPath);
        }
    }
}

If we start having multiple of these types of pruning scene indices, constructing the scene index graph could become quite slow as each of them does its own traversal of the scene hierarchy. It seems that this is sort of what UsdImaging_NiPrototypePruningSceneIndex does in its constructor, however that class doesn’t do a full traversal of the scene, and instead only looks at the direct children of the root prim, so the impact is lessened in that case.

Any thoughts on how to approach this problem would be appreciated, thanks!

Philippe

To make everything work you’ll want to make sure that the existence & type of a prim returned by GetPrim matches the PrimsAdded/PrimsRemoved notices you’ve sent. One possibility is to leave the type hierarchy alone, and only change the data; e.g. for pruned prims, set an “exclude” flag for the renderer, or zero out the points buffer, or something. But barring that, my approach would be:

  1. On _PrimsAdded, loop through the added prims. For any meshes, call _ShouldPrune (which calls GetPrim() and looks at the points buffer), and if that returns true, change the type from “mesh” to TfToken().
  2. On GetPrim, if the input prim is a mesh call _ShouldPrune and change the type the same way as in PrimsAdded.
  3. On _PrimsDirtied, loop through the dirtied prims. If one of them is a mesh (you can check with GetPrim) and it has dirty points (dirtyLocators.Intersects(“primvars/points”)), add it to a new PrimsAdded message that you send from _PrimsDirtied with the correct updated type. If you keep a path->bool map tracking what you’ve pruned in PrimsAdded, you can further only add this resync message when the pruning state changes (instead of whenever points are dirty).

Let me know if this makes sense. Thanks!
Tom

1 Like