Scene-graph instancing (§11.3.3)#84
Merged
Merged
Conversation
Per spec 11.3.3, an instance prim's children come only from its composition arcs and local opinions on its descendants are ignored. composed_children drops local-opinion nodes for instance prims, and ensure_index prunes them from descendant indices (carried via a within_instance context flag), so every downstream query sees the arc-only composition. The instance prim's own properties are unaffected. Also records CLAUDE.md authoring conventions: no planning-phase references in code, and wrap prose at 80 columns.
Instances with the same composition now share one prototype (spec 11.3.3): an instancing key derived from each instance's arc-introduced opinions maps to a canonical instance, and descendant queries on other instances with that key redirect into the canonical instance's already arc-only subtree. Non-canonical instance subtrees are therefore never composed. Carries TODOs for the query surface, invalidation, relationship-target remapping, nested instances, and a rayon seam for parallel prototype composition.
Record the guideline to mark performance/parallelism opportunities with TODO(rayon)/TODO(perf) in new and refactored code rather than optimizing prematurely.
Expose shared prototypes (spec 11.3.3): Stage/Prim get_prototype, get_instances, prototypes, is_prototype, is_in_prototype, a PrimStatus::IN_PROTOTYPE bit, and a PrimPredicate::with_instance_proxies toggle. Default traversal stops at instance prims; ALL and instance-proxy traversal descend. The synthetic /__Prototype_N namespace is addressable, redirecting into the canonical instance's shared subtree.
Changes::apply now clears the scene-graph instancing prototype registry whenever prim indices are invalidated, so a stale instance-to-prototype mapping cannot survive an authoring change to instanceable or an instance's arcs (spec 11.3.3). The registry rebuilds lazily on the next instancing query.
A connection authored inside a prototype resolves into the canonical instance's namespace when shared; connection_paths now maps targets that land inside the prototype back to the queried instance (spec 11.3.3 + 11.3.4). redirect_anchor factors out the origin/canonical prefix pair used for redirection. Relationship targetPaths need the same remap once relationship target forwarding lands.
A nested instance (an instance inside a prototype's subtree) already resolves correctly: the redirect walks to the nearest enclosing instance, so the nested prim gets its own shared prototype and resolves values within the queried instance. Add a test and replace the stale TODOs that claimed otherwise.
An inert prim spec add whose spec authors instanceable changes the prim's instancing composition even though the add is inert; the change classifier now promotes it to significant so the subtree recomposes (spec 11.3.3). A direct instanceable info-change was already significant. Adds Cache::layer_authors_field.
Reflect the instancing work: shared prototypes, the prototype query surface, IN_PROTOTYPE, instance-proxy traversal, connection-target remap, nested instances, and registry invalidation. Note the deferred materialized prototype namespace and relationship-target remap as remaining gaps.
There was a problem hiding this comment.
Pull request overview
Implements OpenUSD scene-graph instancing semantics (§11.3.3) on top of the existing composition cache, adding prototype sharing, instance-proxy traversal behavior, and new Stage/Prim query APIs.
Changes:
- Adds prototype registration/sharing (canonical instance +
/__Prototype_Nnamespace) and redirects non-canonical instance descendant queries to reuse composed results. - Enforces instancing conformance rules: instance descendant local opinions are discarded; instance children come only from composition arcs; connection targets remap back into the queried instance.
- Extends query surface and traversal: prototype/instance queries,
PrimStatus::IN_PROTOTYPE, and a traversal toggle for instance proxies (default stops at instances).
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/usd/stage.rs | Adds instancing-related status/predicate behavior, Stage prototype/instance query APIs, and conformance tests. |
| src/usd/prim.rs | Mirrors new Stage instancing APIs on Prim and adds a small test. |
| src/pcp/index.rs | Adds PrimIndex::retain_nodes to support pruning local opinions in instance subtrees. |
| src/pcp/change.rs | Promotes inert instanceable adds to significant changes and invalidates prototype registry on composition changes. |
| src/pcp/cache.rs | Implements prototype registry, canonical redirection (effective_path), arc-only composition in instance subtrees, and connection-path remapping. |
| ROADMAP.md | Marks scene-graph instancing as complete on main and updates related stage-query notes. |
| fixtures/instancing.usda | Adds fixture for instance subtree arc-only behavior and local override suppression. |
| fixtures/instancing_shared.usda | Adds fixture for shared prototype composition across identical instances. |
| fixtures/instancing_nested.usda | Adds fixture for nested instancing behavior. |
| fixtures/instancing_connections.usda | Adds fixture for connection-target remapping within instances. |
| CLAUDE.md | Adds contributor guidelines related to wording, wrapping, and performance TODO conventions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+667
to
+672
| /// Returns the registered `/__Prototype_N` roots, in registration order. | ||
| pub(crate) fn prototypes(&self) -> Vec<Path> { | ||
| let mut roots: Vec<&Prototype> = self.prototypes.values().collect(); | ||
| roots.sort_by(|a, b| a.path.cmp(&b.path)); | ||
| roots.into_iter().map(|prototype| prototype.path.clone()).collect() | ||
| } |
Comment on lines
+700
to
+708
| fn redirect_prim(&mut self, prim: &Path) -> Result<Path> { | ||
| match self.redirect_anchor(prim)? { | ||
| Some((origin, canonical)) => { | ||
| let tail = &prim.as_str()[origin.as_str().len()..]; | ||
| Path::new(&format!("{canonical}{tail}")) | ||
| } | ||
| None => Ok(prim.clone()), | ||
| } | ||
| } |
Comment on lines
+747
to
+759
| fn effective_path(&mut self, path: &Path) -> Result<Path> { | ||
| let prim = path.prim_path(); | ||
| let redirected = self.redirect_prim(&prim)?; | ||
| if redirected == prim { | ||
| return Ok(path.clone()); | ||
| } | ||
| if path.is_property_path() { | ||
| let suffix = &path.as_str()[prim.as_str().len()..]; | ||
| Ok(Path::new(&format!("{redirected}{suffix}"))?) | ||
| } else { | ||
| Ok(redirected) | ||
| } | ||
| } |
Comment on lines
+802
to
+806
| // Resolve against the canonical instance's subtree when shared. | ||
| let resolved_prim = match &anchor { | ||
| Some((origin, canonical)) => Path::new(&format!("{canonical}{}", &prim.as_str()[origin.as_str().len()..]))?, | ||
| None => prim, | ||
| }; |
Comment on lines
+1061
to
+1064
| // TODO: instances are composed independently per occurrence. Composing | ||
| // identical instances once through a shared prototype representation | ||
| // (an instancing key plus synthesized `/__Prototype_N` prims) is not | ||
| // yet implemented. |
A local override on an instance descendant could carry its own composition arc (e.g. a reference). The post-build prune dropped only the local root node and left the reference/inherit/variant node that arc spawned, so those opinions leaked past the prototype (spec 11.3.3). Skip the local root site at composition time for any prim whose parent context is within an instance, so local arcs are never followed in the first place. Add a regression fixture and test.
Schema and connection readers walked the stage with the default traversal predicate, which stops at instance prims, so any prim inside an instance was silently skipped — a regression once scene-graph instancing landed. They now use PrimPredicate::DEFAULT_PROXIES, which descends into instance subtrees (instance proxies), since prototypes are not yet materialized as separately traversable roots. Collapse traverse / traverse_all / traverse_with_predicate into a single traverse(predicate, visitor) so the predicate is always explicit at the call site.
instances_of returned instances in registration order, which depends on the order the caller queried get_prototype, and prototypes() sorted lexicographically so /__Prototype_10 preceded /__Prototype_2. Sort instances by namespace path and return prototype roots in numeric registration order (tracked via an index on Prototype), so both queries are deterministic regardless of query order.
is_instance, get_prototype, get_instances, and prototypes ignored the population mask, so a masked-out prim could still report as an instance, mint a prototype, or appear among a prototype's instances. Gate the per-prim queries on the mask (matching has_composition_arc) and exclude masked-out instances from the prototype-set queries.
redirect_prim and connection_paths rebuilt the redirected path by slicing the origin prefix off the string and formatting in the canonical prefix. Path::replace_prefix does exactly this mapping (already used for the reverse remap of connection targets), handling path-boundary cases without manual string arithmetic.
value_at redirected the attribute path through effective_path, then called has_spec, which redirected the already-redirected path a second time — and each redirect walks the path's ancestors and can register prototypes. Split out has_spec_at, which assumes a redirected path, and call it from value_at. Also mark effective_path as a memoization seam.
The registry kept two maps — prototypes keyed by instancing key and a reverse prototype_roots from path to key — so every path-direction query (redirect, instances_of, is_prototype, is_in_prototype) did a double lookup. Key the prototypes map by its /__Prototype_N root instead, making those a single lookup, and keep a small key-to-root index only for registration dedup. Drop the now redundant Prototype.path field and extract the duplicated ancestor walk into enclosing_prototype_root.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements scene-graph instancing on top of the composition core.
Deferred: materialized /__Prototype_N namespace (prototype-root attributes are alias-backed by the canonical instance), relationship-target remap (gated on relationship target forwarding, 12.4).