From a81df55106bcdf98f4b57d7ffbbd44f9fbe97b97 Mon Sep 17 00:00:00 2001
From: Trim Bresilla
Date: Sun, 4 Oct 2026 18:38:45 +0200
Subject: [PATCH 1/9] perf(ar): hand over cursor buffer in read_all
---
crates/openusd/src/ar.rs | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/crates/openusd/src/ar.rs b/crates/openusd/src/ar.rs
index cbc726a9..4a8fbb4a 100644
--- a/crates/openusd/src/ar.rs
+++ b/crates/openusd/src/ar.rs
@@ -137,6 +137,17 @@ impl Asset for io::Cursor> {
fn size(&self) -> io::Result {
Ok(self.get_ref().len() as u64)
}
+
+ /// A cursor still at its start hands its buffer over instead of copying
+ /// it, leaving itself empty.
+ fn read_all(&mut self) -> io::Result> {
+ if self.position() != 0 {
+ let mut buf = Vec::new();
+ self.read_to_end(&mut buf)?;
+ return Ok(buf);
+ }
+ Ok(std::mem::take(self.get_mut()))
+ }
}
/// Interface for resolving asset paths to physical locations.
@@ -1057,6 +1068,21 @@ mod tests {
assert_eq!(result, data);
}
+ /// A cursor at its start hands its buffer over; one already read into
+ /// returns the rest.
+ #[test]
+ fn cursor_asset_read_all_moves_buffer() {
+ let data = b"hello world".to_vec();
+ let pointer = data.as_ptr();
+ let mut asset = io::Cursor::new(data);
+ let moved = asset.read_all().unwrap();
+ assert_eq!(moved.as_ptr(), pointer);
+
+ let mut asset = io::Cursor::new(b"hello world".to_vec());
+ asset.seek(io::SeekFrom::Start(6)).unwrap();
+ assert_eq!(asset.read_all().unwrap(), b"world");
+ }
+
#[test]
fn cursor_asset_seek() {
let data = b"hello world".to_vec();
From c7606f355c34ad03e4a8c1ac185b4e2194835904 Mon Sep 17 00:00:00 2001
From: Trim Bresilla
Date: Sun, 4 Oct 2026 18:40:33 +0200
Subject: [PATCH 2/9] perf(ar): share asset bytes with file formats
---
crates/openusd/src/ar.rs | 8 +++
crates/openusd/src/sdf/file_format.rs | 13 +++-
crates/openusd/src/sdf/layer_registry.rs | 24 +++++--
crates/openusd/src/usdc/mod.rs | 79 +++++++++++++++++++++++-
4 files changed, 117 insertions(+), 7 deletions(-)
diff --git a/crates/openusd/src/ar.rs b/crates/openusd/src/ar.rs
index 4a8fbb4a..3441d4d0 100644
--- a/crates/openusd/src/ar.rs
+++ b/crates/openusd/src/ar.rs
@@ -118,6 +118,14 @@ pub trait Asset: Read + Seek + Send {
/// Returns the total size of the asset in bytes.
fn size(&self) -> io::Result;
+ /// The complete asset as bytes shared with the caller, whatever the
+ /// cursor position, for an asset already held in memory (C++
+ /// `ArAsset::GetBuffer`). A format that decodes in place keeps them
+ /// instead of copying; `None`, the default, has the asset read instead.
+ fn shared_bytes(&self) -> Option> {
+ None
+ }
+
/// Reads the entire asset into a byte buffer.
fn read_all(&mut self) -> io::Result> {
let size = self.size()? as usize;
diff --git a/crates/openusd/src/sdf/file_format.rs b/crates/openusd/src/sdf/file_format.rs
index 06339e0d..f5d6c786 100644
--- a/crates/openusd/src/sdf/file_format.rs
+++ b/crates/openusd/src/sdf/file_format.rs
@@ -163,6 +163,13 @@ pub trait FileFormat: Sync {
/// decode without a copy while bytes just read off disk move in.
fn read_bytes(&self, bytes: Cow<'static, [u8]>, source_name: &str) -> Result;
+ /// [`read_bytes`](Self::read_bytes) for bytes shared with an asset
+ /// ([`ar::Asset::shared_bytes`]). A format that decodes in place keeps
+ /// them; the default copies them into `read_bytes`.
+ fn read_shared_bytes(&self, bytes: std::sync::Arc<[u8]>, source_name: &str) -> Result {
+ self.read_bytes(bytes.as_ref().to_vec().into(), source_name)
+ }
+
/// Read a layer's data from `resolved`, opening the asset (and any
/// sibling assets) through `resolver`.
///
@@ -170,7 +177,11 @@ pub trait FileFormat: Sync {
/// [`read_bytes`](Self::read_bytes); one that reaches for sibling assets
/// overrides this.
fn read(&self, resolver: &dyn ar::Resolver, resolved: &ar::ResolvedPath) -> Result {
- let bytes = resolver.open_asset(resolved)?.read_all()?;
+ let mut asset = resolver.open_asset(resolved)?;
+ if let Some(bytes) = asset.shared_bytes() {
+ return self.read_shared_bytes(bytes, &resolved.to_string());
+ }
+ let bytes = asset.read_all()?;
self.read_bytes(bytes.into(), &resolved.to_string())
}
diff --git a/crates/openusd/src/sdf/layer_registry.rs b/crates/openusd/src/sdf/layer_registry.rs
index 4cbcd58c..22c62308 100644
--- a/crates/openusd/src/sdf/layer_registry.rs
+++ b/crates/openusd/src/sdf/layer_registry.rs
@@ -256,6 +256,20 @@ impl LayerRegistry {
.read_bytes(bytes, source_name)
}
+ /// [`read_bytes`](Self::read_bytes) for bytes shared with an asset, which
+ /// a format that decodes in place keeps rather than copies.
+ pub fn read_shared_bytes(
+ bytes: std::sync::Arc<[u8]>,
+ source_name: &str,
+ ) -> Result {
+ DEFAULT_FORMATS
+ .iter()
+ .copied()
+ .find(|format| format.matches_content(&bytes))
+ .ok_or_else(|| sdf::FormatError::Unrecognized(source_name.into()))?
+ .read_shared_bytes(bytes, source_name)
+ }
+
/// Find the format claiming `ext` (without the leading dot, case-insensitive),
/// e.g. `"usda"` or `"usd"`. C++ `SdfFileFormat::FindByExtension`.
pub fn find_by_extension(ext: &str) -> Option<&'static dyn sdf::FileFormat> {
@@ -454,11 +468,11 @@ impl LayerRegistry {
fn read(&self, resolved: &ar::ResolvedPath) -> Result {
let ext = resolved.extension();
if ext.eq_ignore_ascii_case("usd") {
- let bytes = self
- .resolver
- .open_asset(resolved)
- .and_then(|mut asset| asset.read_all())
- .map_err(sdf::FormatError::from)?;
+ let mut asset = self.resolver.open_asset(resolved).map_err(sdf::FormatError::from)?;
+ if let Some(bytes) = asset.shared_bytes() {
+ return Ok(Self::read_shared_bytes(bytes, &resolved.to_string())?);
+ }
+ let bytes = asset.read_all().map_err(sdf::FormatError::from)?;
return Ok(Self::read_bytes(bytes.into(), &resolved.to_string())?);
}
Ok(Self::find_by_extension(&ext)
diff --git a/crates/openusd/src/usdc/mod.rs b/crates/openusd/src/usdc/mod.rs
index e501854b..45a0f0d6 100644
--- a/crates/openusd/src/usdc/mod.rs
+++ b/crates/openusd/src/usdc/mod.rs
@@ -381,6 +381,16 @@ impl sdf::FileFormat for UsdcFileFormat {
Ok(Box::new(data))
}
+ fn read_shared_bytes(
+ &self,
+ bytes: std::sync::Arc<[u8]>,
+ _source_name: &str,
+ ) -> Result {
+ let data =
+ CrateData::open(io::Cursor::new(bytes), true).map_err(|error| sdf::FormatError::Decode(Box::new(error)))?;
+ Ok(Box::new(data))
+ }
+
fn matches_content(&self, prefix: &[u8]) -> bool {
prefix.starts_with(MAGIC)
}
@@ -399,7 +409,74 @@ const CRATE_PROPERTY_CHILDREN: &str = "properties";
#[cfg(test)]
mod tests {
use super::*;
- use crate::Result;
+ use crate::sdf::FileFormat;
+ use crate::{Result, ar};
+ use std::sync::Arc;
+
+ /// An asset over shared bytes, as a resolver holding assets in memory
+ /// serves them.
+ struct SharedAsset(io::Cursor>);
+
+ impl io::Read for SharedAsset {
+ fn read(&mut self, buf: &mut [u8]) -> io::Result {
+ self.0.read(buf)
+ }
+ }
+
+ impl io::Seek for SharedAsset {
+ fn seek(&mut self, pos: io::SeekFrom) -> io::Result {
+ self.0.seek(pos)
+ }
+ }
+
+ impl ar::Asset for SharedAsset {
+ fn size(&self) -> io::Result {
+ Ok(self.0.get_ref().len() as u64)
+ }
+
+ fn shared_bytes(&self) -> Option> {
+ Some(self.0.get_ref().clone())
+ }
+ }
+
+ struct SharedResolver(Arc<[u8]>);
+
+ impl ar::Resolver for SharedResolver {
+ fn create_identifier(&self, asset_path: &str, _anchor: Option<&ar::ResolvedPath>) -> String {
+ asset_path.to_string()
+ }
+
+ fn resolve(&self, asset_path: &str) -> Option {
+ Some(ar::ResolvedPath::new(asset_path))
+ }
+
+ fn resolve_for_new_asset(&self, asset_path: &str) -> Option {
+ Some(ar::ResolvedPath::new(asset_path))
+ }
+
+ fn open_asset(&self, _resolved_path: &ar::ResolvedPath) -> io::Result> {
+ Ok(Box::new(SharedAsset(io::Cursor::new(self.0.clone()))))
+ }
+ }
+
+ /// A crate layer read from an asset that shares its bytes decodes from
+ /// those bytes, holding a reference rather than a copy.
+ #[test]
+ fn shared_asset_bytes_are_kept() -> Result<()> {
+ let mut layer = sdf::Data::new();
+ layer.create_spec(sdf::Path::abs_root(), sdf::SpecType::PseudoRoot);
+ let mut bytes = io::Cursor::new(Vec::new());
+ CrateWriter::write(&layer, &mut bytes)?;
+ let bytes: Arc<[u8]> = bytes.into_inner().into();
+
+ let resolver = SharedResolver(bytes.clone());
+ let data = UsdcFileFormat.read(&resolver, &ar::ResolvedPath::new("shared.usdc"))?;
+ assert!(data.has_spec(&sdf::Path::abs_root()));
+ assert_eq!(Arc::strong_count(&bytes), 3);
+ drop(data);
+ assert_eq!(Arc::strong_count(&bytes), 2);
+ Ok(())
+ }
use crate::gf;
use crate::gf::f16;
From 17361c0e34202a7d51cf50ce102b39111f6aad95 Mon Sep 17 00:00:00 2001
From: Trim Bresilla
Date: Sun, 4 Oct 2026 18:41:26 +0200
Subject: [PATCH 3/9] perf(sdf): share a path's text between clones
---
crates/openusd/src/sdf/path.rs | 98 +++++++++++++++++++++++++---------
1 file changed, 72 insertions(+), 26 deletions(-)
diff --git a/crates/openusd/src/sdf/path.rs b/crates/openusd/src/sdf/path.rs
index 413e629b..9db10c65 100644
--- a/crates/openusd/src/sdf/path.rs
+++ b/crates/openusd/src/sdf/path.rs
@@ -40,9 +40,12 @@ pub fn try_into_path(path: impl IntoPath) -> Result {
/// Parsing via [`Path::new`] (or [`FromStr`]) validates this grammar and
/// rejects malformed text with a [`PathParseError`]. The empty path is not
/// parseable; construct it with [`Path::default`].
+///
+/// Clones share the path's text rather than copying it, as C++ `SdfPath`
+/// copies share one pooled path node.
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Path {
- path: String,
+ path: std::sync::Arc,
}
impl fmt::Display for Path {
@@ -54,7 +57,7 @@ impl fmt::Display for Path {
#[cfg(feature = "serde")]
impl serde::Serialize for Path {
fn serialize(&self, serializer: S) -> Result {
- self.path.serialize(serializer)
+ self.as_str().serialize(serializer)
}
}
@@ -94,7 +97,9 @@ impl FromStr for Path {
fn from_str(s: &str) -> Result {
Path::validate(s)?;
- Ok(Path { path: s.to_string() })
+ Ok(Path {
+ path: s.to_owned().into(),
+ })
}
}
@@ -121,7 +126,9 @@ impl Path {
path.is_empty() || Path::validate(path).is_ok(),
"from_str_unchecked on invalid path {path:?}"
);
- Path { path: path.to_string() }
+ Path {
+ path: path.to_owned().into(),
+ }
}
#[inline]
@@ -132,7 +139,7 @@ impl Path {
/// Returns `true` if this is the absolute root path `/` (pseudo-root).
#[inline]
pub fn is_abs_root(&self) -> bool {
- self.path == "/"
+ self.as_str() == "/"
}
/// Whether this path's prim is a root prim — a direct child of the
@@ -162,7 +169,7 @@ impl Path {
// The pseudo-root, the empty path, and the `.`/`..` relative anchors
// cannot own properties; appending to them would build an unparseable
// path like `/.foo`.
- if self.is_empty() || self.is_abs_root() || self.path == "." || self.path.ends_with("..") {
+ if self.is_empty() || self.is_abs_root() || self.as_str() == "." || self.path.ends_with("..") {
return Err(fail(0, "path cannot own properties"));
}
if !Path::is_valid_namespace_identifier(property) {
@@ -172,11 +179,11 @@ impl Path {
));
}
- let mut new_path = self.path.clone();
+ let mut new_path = self.path.to_string();
new_path.push('.');
new_path.push_str(property);
- Ok(Path { path: new_path })
+ Ok(Path { path: new_path.into() })
}
/// Appends `path` (parsed if given as a string) under this path with a `/`
@@ -187,7 +194,7 @@ impl Path {
if self.is_abs() && append.is_abs() {
return Err(PathParseError {
- input: append.path,
+ input: append.path.to_string(),
offset: 0,
reason: "cannot append an absolute path to an absolute path",
});
@@ -195,7 +202,7 @@ impl Path {
if self.is_property_path() {
return Err(PathParseError {
- input: self.path.clone(),
+ input: self.path.to_string(),
offset: self.path.rfind('.').unwrap_or(0),
reason: "cannot append a path to a property path",
});
@@ -208,7 +215,7 @@ impl Path {
// The reflexive base is the identity anchor: appending to `.` yields
// the argument itself (C++ `SdfPath::AppendPath` on the reflexive
// relative path).
- if self.path == "." {
+ if self.as_str() == "." {
return Ok(append);
}
@@ -217,7 +224,7 @@ impl Path {
// a valid path.
if append.as_str().starts_with('.') {
return Err(PathParseError {
- input: append.path,
+ input: append.path.to_string(),
offset: 0,
reason: "cannot append a `.`-anchored path under a prim path",
});
@@ -225,7 +232,7 @@ impl Path {
// If base is slash only.
// "/" + "foo/bar" => "/foo/bar"
- let combined = if self.path.as_str() == "/" {
+ let combined = if self.as_str() == "/" {
format!("/{}", append.path)
} else if self.is_prim_variant_selection_path() {
// A prim child attaches directly to a variant selection with no
@@ -235,7 +242,7 @@ impl Path {
format!("{}/{}", self.path, append.path)
};
- Ok(Path { path: combined })
+ Ok(Path { path: combined.into() })
}
pub fn is_property_path(&self) -> bool {
@@ -278,13 +285,13 @@ impl Path {
/// path.
pub fn is_prim_path(&self) -> bool {
// The relative anchors: `.`, and a `..(/..)*` run with nothing after it.
- if self.path == "." || (!self.path.is_empty() && self.path.split('/').all(|seg| seg == "..")) {
+ if self.as_str() == "." || (!self.path.is_empty() && self.path.split('/').all(|seg| seg == "..")) {
return true;
}
// Strip the anchor the grammar allows ahead of the prim chain, then ask
// the prim-chain iterator what the last component was. A non-empty
// remainder means a property tail the chain could not consume.
- let mut rest = self.path.as_str();
+ let mut rest = self.as_str();
if let Some(after) = rest.strip_prefix('/') {
rest = after;
} else {
@@ -476,7 +483,7 @@ impl Path {
/// "/A{set=sel}" -> Some(Variant { set: "set", selection: "sel" })
/// ```
pub fn last_element(&self) -> Option> {
- if self.path.is_empty() || self.path == "/" {
+ if self.path.is_empty() || self.as_str() == "/" {
return None;
}
// A property's element is the whole property name (everything after
@@ -517,7 +524,7 @@ impl Path {
/// "" -> None
/// ```
pub fn parent(&self) -> Option {
- if self.path.is_empty() || self.path == "/" {
+ if self.path.is_empty() || self.as_str() == "/" {
return None;
}
// Drop a trailing `{set=sel}` variant selection.
@@ -622,7 +629,7 @@ impl Path {
/// "" -> None
/// ```
pub fn name(&self) -> Option<&str> {
- if self.path.is_empty() || self.path == "/" {
+ if self.path.is_empty() || self.as_str() == "/" {
return None;
}
// The final prim name begins after the rightmost `/` or `}` (a child of
@@ -633,7 +640,7 @@ impl Path {
if start < self.path.len() {
Some(&self.path[start..])
} else {
- Some(self.path.rsplit_once('/').map_or(self.path.as_str(), |(_, name)| name))
+ Some(self.path.rsplit_once('/').map_or(self.as_str(), |(_, name)| name))
}
}
@@ -702,7 +709,7 @@ impl Path {
return self.clone();
}
let mut out = String::with_capacity(self.path.len());
- let mut rest = self.path.as_str();
+ let mut rest = self.as_str();
let mut depth = 0usize;
let mut chars = rest.char_indices();
// Splice out each depth-0 `{…}` span; bracketed spans pass through.
@@ -875,7 +882,7 @@ impl Path {
if self.is_empty()
|| self.is_abs_root()
|| self.is_property_path()
- || self.path == "."
+ || self.as_str() == "."
|| self.path.ends_with("..")
|| self.path.ends_with(']')
{
@@ -1335,10 +1342,10 @@ impl TryFrom<&str> for Path {
impl TryFrom for Path {
type Error = PathParseError;
- /// Validates and reuses `value`'s allocation.
+ /// Validates `value` and shares its existing text allocation.
fn try_from(value: String) -> Result {
Path::validate(&value)?;
- Ok(Path { path: value })
+ Ok(Path { path: value.into() })
}
}
@@ -1356,6 +1363,39 @@ mod tests {
use super::*;
+ #[test]
+ fn clones_share_text_and_derivations_preserve_original() {
+ let path = Path::new("/Root/Branch").unwrap();
+ let clone = path.clone();
+ assert!(std::sync::Arc::ptr_eq(&path.path, &clone.path));
+ assert_eq!(clone.append_property("size").unwrap().as_str(), "/Root/Branch.size");
+ assert_eq!(clone.append_path("Leaf").unwrap().as_str(), "/Root/Branch/Leaf");
+ assert_eq!(path.as_str(), "/Root/Branch");
+ let text = String::from("/Root/Branch");
+ let allocation = text.as_ptr();
+ let independent = Path::try_from(text).unwrap();
+ assert_eq!(independent.as_str().as_ptr(), allocation);
+ assert_eq!(path, independent);
+ let mut paths = std::collections::HashSet::new();
+ paths.insert(path);
+ assert!(paths.contains(&independent));
+ }
+
+ #[cfg(feature = "serde")]
+ #[test]
+ fn shared_paths_preserve_string_serialization() {
+ for path in [
+ Path::default(),
+ Path::abs_root(),
+ Path::new("/Root.rel[/Target]").unwrap(),
+ ] {
+ let encoded = serde_json::to_string(&path).unwrap();
+ assert_eq!(encoded, serde_json::to_string(path.as_str()).unwrap());
+ assert_eq!(serde_json::from_str::(&encoded).unwrap(), path);
+ }
+ assert!(serde_json::from_str::("\"/invalid path\"").is_err());
+ }
+
/// `is_prim_path` classifies by the path's final element, so every tail
/// shape lands where C++ `SdfPath::IsPrimPath` puts it — including `.` and
/// `..`, which C++ builds as prim nodes, and `/A.rel[/T]`, which the
@@ -1384,7 +1424,9 @@ mod tests {
/// Builds a `Path` directly from `path`, skipping validation — for
/// exercising lenient read-side behavior on malformed input.
fn raw(path: &str) -> Path {
- Path { path: path.to_string() }
+ Path {
+ path: path.to_owned().into(),
+ }
}
#[test]
@@ -1668,7 +1710,11 @@ mod tests {
#[test]
fn test_split_property() {
- let split = |s: &str| raw(s).split_property().map(|(p, n)| (p.path, n.to_owned()));
+ let split = |s: &str| {
+ raw(s)
+ .split_property()
+ .map(|(p, n)| (p.as_str().to_owned(), n.to_owned()))
+ };
let owned = |p: &str, n: &str| Some((p.to_owned(), n.to_owned()));
assert_eq!(split("/World/Mesh.points"), owned("/World/Mesh", "points"));
From beba7b096cee48247352cc10efd5a99a4c9ae403 Mon Sep 17 00:00:00 2001
From: Trim Bresilla
Date: Sun, 4 Oct 2026 18:44:21 +0200
Subject: [PATCH 4/9] perf(usd): read root layers once when opening
---
crates/openusd/src/sdf/layer_registry.rs | 75 ++++++++++++++++++------
crates/openusd/src/usd/stage.rs | 53 ++++++++++++++---
crates/openusd/tests/stage.rs | 21 +++++++
3 files changed, 124 insertions(+), 25 deletions(-)
diff --git a/crates/openusd/src/sdf/layer_registry.rs b/crates/openusd/src/sdf/layer_registry.rs
index 22c62308..01bb0f5f 100644
--- a/crates/openusd/src/sdf/layer_registry.rs
+++ b/crates/openusd/src/sdf/layer_registry.rs
@@ -147,6 +147,18 @@ pub struct LayerRegistry {
resolver: Box,
}
+/// A stack's root layer, read ahead of its stack by
+/// [`LayerRegistry::prepare_root`] and handed to
+/// [`LayerRegistry::open_prepared_stack`] so it is not read twice.
+pub(crate) struct PreparedLayer {
+ /// The layer's canonical identifier.
+ pub identifier: String,
+ /// Where the layer was found.
+ pub resolved: ar::ResolvedPath,
+ /// The layer's data.
+ pub data: sdf::LayerData,
+}
+
impl Default for LayerRegistry {
/// A registry over the filesystem [`DefaultResolver`](ar::DefaultResolver)
/// and the built-in formats — what [`Stage::builder`](crate::usd::Stage)
@@ -306,30 +318,32 @@ impl LayerRegistry {
}
}
- /// The `expressionVariables` authored on the single layer at `asset_path`
- /// (anchored against `anchor`), read without opening its sublayers — the shallow
- /// read the stage root stack needs to compose its root and session layers' own
- /// variables into one context before either region's sublayer subtree is
- /// collected. An empty identifier yields an empty map; a resolve or read failure
- /// propagates.
- ///
- /// TODO(perf): the layer read here is read again when its stack is collected;
- /// the registry does not cache reads, so a root or session layer is parsed twice
- /// at open.
- pub(crate) fn own_expression_variables(
+ /// Reads the single layer at `asset_path` (anchored against `anchor`)
+ /// without opening its sublayers: the shallow read the stage root stack
+ /// needs to compose its root and session layers' own expression variables
+ /// into one context before either region's sublayer subtree is collected.
+ /// The read is then handed to
+ /// [`open_prepared_stack`](Self::open_prepared_stack), so the layer is
+ /// parsed once at open. An empty identifier yields `None`; a resolve or
+ /// read failure propagates.
+ pub(crate) fn prepare_root(
&self,
asset_path: &str,
anchor: Option<&ar::ResolvedPath>,
- ) -> Result, LoadError> {
+ ) -> Result
+ )
+ {
+ }
+ def "Unloaded" (
+ payload = @./payload.usda@
+ )
+ {
+ }
+ def "Inst" (
+ instanceable = true
+ references =
+ )
+ {
+ }
+ def "Deep"
+ {
+ over "A"
+ {
+ def "B"
+ {
+ }
+ }
+ def "C"
+ {
+ class "D"
+ {
+ def "E"
+ {
+ }
+ }
+ }
+ }
+}
+"#,
+ )?;
+ let root = root.to_str().expect("utf-8 temp path");
+ for load in [InitialLoadSet::LoadAll, InitialLoadSet::LoadNone] {
+ let stage = Stage::builder().load(load).open(root)?;
+ stage.prim("/World/Loaded")?.load(LoadPolicy::WithDescendants);
+ let mut all = Vec::new();
+ stage.traverse(PrimPredicate::ALL, |path| all.push(path.clone()))?;
+ for predicate in [PrimPredicate::DEFAULT, PrimPredicate::DEFAULT_PROXIES] {
+ let mut expected = Vec::new();
+ let mut instances: Vec = Vec::new();
+ for path in &all {
+ let below_instance = instances
+ .iter()
+ .any(|instance| path.has_prefix(instance) && path != instance);
+ let status = stage.prim_status(path.clone())?;
+ if status.contains(PrimStatus::INSTANCE) {
+ instances.push(path.clone());
+ }
+ if predicate.matches(status) && (predicate == PrimPredicate::DEFAULT_PROXIES || !below_instance) {
+ expected.push(path.clone());
+ }
+ }
+ let mut visited = Vec::new();
+ stage.traverse(predicate, |path| visited.push(path.clone()))?;
+ assert_eq!(visited, expected, "{load:?} {predicate:?}");
+ assert!(visited.len() > 5);
+ }
+ }
+ Ok(())
+}
+
/// A prim sorts its properties into attributes and relationships by their
/// composed spec, and a prototype root, which reads no opinions, has neither.
#[test]