( Hydra 1.0 ) Moving one native instance recomputes transforms for every nested instance

Hi,

I have a fundamental question regarding native instancing in Hydra 1.0.

Specs:

  • Maya USD 0.33.0
  • OpenUSD 25.11
  • Windows

I have an asset that are built out of instanceable sub-pieces, and those assets
then get instanced a lot in a layer. See attached StressTest_HierarchyRefs.usda

StressTest_HierarchyRefs.usda is the top layer and contains a four-level chain of references, instanceable = true at every level, with a single UsdGeomCube at the bottom: 125 towers, 25 floors in each tower, 8 rows in each floor, 8 cubes in each row. In total 166 instance-able prims.

In Maya, when I move one of those instances in the top layer ( e.g Tower_24 ) the frame rate drops drastically that is almost unusable to interact with the scene. See below

Tower_Performance_StressTest_HierarchyRefs

On the other hand, if I reduce the level of instancing and only keep the top layer to be instance able ( see StressTest.usda ) then I get a better frame rate ~ 7 fps.

Tower_Performance_StressTest

I’ve spent sometimes in UsdImagingInstanceAdapter and I think I understand
why, but I’d rather have someone who knows this code tell me whether I’ve read it
correctly:

I think the first is that a moved instance isn’t tracked individually. Its transform
lives in an array on the instancer, so the only thing Hydra can be told is that
the array is stale:

// instanceAdapter.cpp:1278, ProcessPropertyChange
// Transform changes to instance prims end up getting folded into the
// "hydra:instanceTransforms" instance-rate primvar.
if (UsdGeomXformable::IsTransformationAffectedByAttrNamed(propertyName)) {
    return HdChangeTracker::DirtyPrimvar;
}

Then when an instance is populated, the adapter walks down the whole chain of nested instancers and registers a dependency on every one of them:

std::queue<SdfPath> depInstancePaths;
depInstancePaths.push(instancePath);
while (!depInstancePaths.empty()) {
    ...
    index->AddDependency(depInstancerPath, prim);
    index->MarkInstancerDirty(depInstancerPath,
            HdChangeTracker::DirtyPrimvar | HdChangeTracker::DirtyInstanceIndex);
    for (SdfPath const& nestedInstance : depInstancerData.nestedInstances) {
        depInstancePaths.push(nestedInstance);
    }
}

Once an instance is dirty, its transform array is rebuilt from scratch, sized to
the full flattened count rather than to whatever actually changed:

if (requestedBits & HdChangeTracker::DirtyPrimvar) {
    VtMatrix4dArray instanceXforms;
    if (_ComputeInstanceTransforms(prim, &instanceXforms, time)) {

// instanceAdapter.cpp:721
void Initialize(size_t numInstances) { result.resize(numInstances); ... }

_ComputeInstanceTransformFn::operator()

GfMatrix4d xform(1.0);
for (UsdPrim const& prim : instanceContext) { 
   xform = xform * adapter->GetTransform(prim, prim.GetPath(), time, ignoreRootTransform);
}

I added some debugging code in UsdImagingInstanceAdapter::_RunForAllInstancesToDraw


TF_WARN("_RunForAllInstancesToDraw [%s] %s rows=%zu",
        ArchGetDemangled(typeid(Functor)).c_str(),
        instancer.GetPath().GetText(),
        instancerData->numInstancesToDraw);

fn->Initialize(instancerData->numInstancesToDraw);

This function is called from multiple sites ( e.g _ComputeInstanceMapFn, _PopulateInstanceSelectionFn, _GetScenePrimPathsFn )

// Warning: _RunForAllInstancesToDraw [UsdImagingInstanceAdapter::_ComputeInstanceMapFn] /Root/Tower_0 rows=125
// Warning: _RunForAllInstancesToDraw [UsdImagingInstanceAdapter::_PopulateInstanceSelectionFn] /Root/Tower_0 rows=125
// Warning: _RunForAllInstancesToDraw [UsdImagingInstanceAdapter::_ComputeInstanceMapFn] /__Prototype_5/Row_0 rows=25000
// Warning: _RunForAllInstancesToDraw [UsdImagingInstanceAdapter::_PopulateInstanceSelectionFn] /__Prototype_5/Row_0 rows=25000
// Warning: _RunForAllInstancesToDraw [UsdImagingInstanceAdapter::_ComputeInstanceMapFn] /__Prototype_6/Floor_0 rows=3125
// Warning: _RunForAllInstancesToDraw [UsdImagingInstanceAdapter::_PopulateInstanceSelectionFn] /__Prototype_6/Floor_0 rows=3125
// Warning: _RunForAllInstancesToDraw [UsdImagingInstanceAdapter::_ComputeInstanceMapFn] /__Prototype_7/Cube_0 rows=200000
// Warning: _RunForAllInstancesToDraw [UsdImagingInstanceAdapter::_PopulateInstanceSelectionFn] /__Prototype_7/Cube_0 rows=200000
// Warning: _RunForAllInstancesToDraw [UsdImagingInstanceAdapter::_ComputeInstanceMapFn] /__Prototype_7/Cube_0 rows=200000
// Warning: _RunForAllInstancesToDraw [UsdImagingInstanceAdapter::_GetScenePrimPathsFn] /__Prototype_7/Cube_0 rows=200000
// Warning: _RunForAllInstancesToDraw [UsdImagingInstanceAdapter::_ComputeInstanceMapFn] /__Prototype_7/Cube_0 rows=200000
// Warning: _RunForAllInstancesToDraw [UsdImagingInstanceAdapter::_ComputeInstanceMapFn] /__Prototype_7/Cube_0 rows=200000
rows(Tower) =             125  =     125
rows(Floor) =        25 × 125  =   3,125
rows(Row)   =    8 × 25 × 125  =  25,000
rows(Cube)  = 8 × 8 × 25 × 125 = 200,000

Am I interpreting this correctly? does a transform edit on one top-level instance dirty every nested instances, with each one rebuilding its whole instanceTransforms?

Any help is appreciated.

TowerOfCubes_HierarchyRefs.usda (5.6 KB)

TowerOfCubes.usda (268.9 KB)

StressTest_HierarchyRefs.usda (30.2 KB)

StressTest.usda (28.5 KB)

Row.usda (1.8 KB)

Floor.usda (1.8 KB)

Cube.usda (118 Bytes)