Skip to content

Scene-graph instancing (§11.3.3)#84

Merged
mxpv merged 16 commits into
mainfrom
instancing
May 29, 2026
Merged

Scene-graph instancing (§11.3.3)#84
mxpv merged 16 commits into
mainfrom
instancing

Conversation

@mxpv
Copy link
Copy Markdown
Owner

@mxpv mxpv commented May 29, 2026

Implements scene-graph instancing on top of the composition core.

  • Behavioral conformance: an instance prim's children and subtree come only from its composition arcs; local opinions on the subtree are discarded.
  • Compose-once sharing: instances with the same composition (an instancing key over their arcs) share one prototype, composed a single time.
  • Query surface: Stage/Prim get_prototype / get_instances / is_prototype / is_in_prototype, a PrimStatus::IN_PROTOTYPE flag, and a PrimPredicate::with_instance_proxies traversal toggle (default stops at instances).
  • Correctness: connection targets inside an instance remap into the queried instance; nested instances are handled; the prototype registry is invalidated on composition change.

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).

mxpv added 9 commits May 29, 2026 14:52
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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_N namespace) 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 thread src/pcp/cache.rs
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 thread src/pcp/cache.rs
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 thread src/pcp/cache.rs
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 thread src/pcp/cache.rs
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 thread src/pcp/cache.rs Outdated
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.
mxpv added 7 commits May 29, 2026 15:23
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.
@mxpv mxpv merged commit 4c02084 into main May 29, 2026
10 checks passed
@mxpv mxpv deleted the instancing branch May 30, 2026 00:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants