This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is a pure Rust implementation of OpenUSD (Universal Scene Description), Pixar's open-source framework for 3D computer graphics data interchange. The project aims to provide native Rust support without C++ dependencies for reading and writing USD files.
The repository is a Cargo workspace of three crates. crates/openusd is the
core library, crates/openusd-schemas the domain schemas layered on it, and
crates/openusd-build the schema view generator that openusd-schemas runs as
a build dependency; shared package properties, dependency versions, and the
lint table live in the root Cargo.toml. Module paths in the bullets below are
relative to a crate's src/.
The core crate mirrors the C++ OpenUSD SDK's module layout. The bullets below
are a navigational map — what each module is, where to start reading, and the
types that name its main seams. They say what a type is for, not how it behaves:
each module's own //! doc comment, and each item's own doc comment, are the
source of truth for specifics.
-
tf- Tools Foundation (C++Tf): low-level utilities, chieflytf::Token, the interned-identifier string behind everyTfToken-equivalent API. It is the single filetf.rs. -
vt/- Value Types (C++Vt):vt::Value(C++VtValue), the value an attribute or metadata field holds, andvt::Dictionary(C++VtDictionary), with the casts out of a value (FromValue,CastError, andvt/coerce.rs, C++VtValue::CastToTypeid), the scalar and array kinds (vt/array.rs), and the text C++ streams for one (vt/stream_out.rs, C++VtStreamOut).Valueis a closed enum, so it names thesdfvalue types it holds. Start atvt/mod.rs. -
sdf/- Scene Description Foundations (C++Sdf): the core data model.AbstractDatais the unified read/write backend interface the text, binary, and archive readers all implement;PathandLayer(C++SdfLayer) are the everyday types. ALayer's edits run throughLayer::edit(a closure over aLayerEditview) as atomic copy-on-write transactions that derive aChangeListfor composition invalidation and fire per-layerLayerSinks at the commit seam (sdf/layer.rs,sdf/change.rs,sdf/sink.rs); spec authoring lives on the typed views (PrimSpec::newand friends). Also here: the pluggableFileFormatread/write seam, theLayerRegistrythat owns asset loading and format dispatch (composition opens reference/payload targets on demand through it, so an un-visited subtree never loads) and holds layers shared throughLayerHandles (C++SdfLayer::FindOrOpen,sdf/layer_store.rs, with the commit protocol its edits run insdf/commit.rs), the variable-expression engine (sdf/expr.rs), the asset-path resolution tail that anchors anassetvalue against its authoring layer and evaluates any expression in it (sdf/asset_path.rs, C++SdfResolveAssetPaths;pcpandusdeach supply their own anchor and variable scope), the path-expression engine behind pattern-based collections (sdf/path_expr/), the namespace-aware spec-copy primitive (sdf/copy.rs), and the list edits:Spec::edit_listrewrites a list-op field, andPathListEdit(C++SdfPathEditorProxy) edits a spec's path lists anchored at its prim, a property's targets or connections and a prim's inherits or specializes. Start atsdf/mod.rs. -
usda/- Text format.usda: logos lexer + recursive-descent parser.Parserbuilds the scene hierarchy over aCursor(one-token lookahead and the span bookkeeping behindParseError's line/column diagnostics), delegating value decoding to the free functions intypes;TextWriteremits. -
usdc/- Binary crate format.usdc(compressed).CrateData/CrateWriter. -
usdz/- ZIP-based.usdzpackage.Archive/ArchiveWriter. -
ar/- Asset Resolution (C++Ar): theResolvertrait maps@...@asset paths to physical locations;DefaultResolversearches the filesystem.CacheScope(C++ArResolverScopedCache) is the span, on one thread, over which opened packages are kept.UdimPath(C++UsdShadeUdimUtils) is a texture path standing for a set of tiles, with the tiles a resolver finds for it. -
pcp/- Prim Cache Population, the composition engine (C++Pcp): LIVERPS strength ordering across layers, kept a pure function of(graph, context, cached indices)so it stays parallelizable. Composition drives layer loading: a reference/payload target opens on first use and the demand travels out throughBuildOutputto the stage's load barrier. A build reads layer content throughLayerView, the graph with one borrow of its layers' store. Start atpcp/mod.rs— it has the LIVERPS overview and a per-file structure table. -
usd/- Composed stage API (C++Usd):usd::Stageis the handle that delegates composition topcp::IndexCache;Prim,Attribute,Relationship, and the schema views areClonevalue types over it, and stage-tier authoring routes through the currentEditTarget, withStage::editbatching several prim and property writes into one layer transaction andStage::batch_editfor atomic multi-layer edits. Notable sub-surfaces:usd/schema_registry.rs+usd/prim_definition.rs+usd/prim_type_info.rs(C++UsdSchemaRegistry/UsdPrimDefinition/UsdPrimTypeInfo— the schema type table and the property fallbacks it supplies, built from per-family manifest + schematics text),usd/resolve_info.rs(ResolveInfo/ResolveInfoSource— where a value read found its answer, overpcp::value_resolve's shared walk),usd/sink.rs(StageSinkcomposed-change observers,Provenance),usd/diff.rs(the transferableDiffandStage::apply_diff),usd/capture.rs(UndoStage/ReplayStage, recording wrappers over the change seam),usd/editor.rs(namespace editing),usd/list_edit.rs(ListEdit, C++UsdInherits/UsdSpecializes/UsdReferences/UsdPayloads, a prim's arc lists edited on the edit target),usd/object.rs(ObjectandProperty, C++UsdObjectandUsdProperty: the metadata a prim and its properties share),usd/composition.rs(privateStageComposition: owns the layer graph, index cache, and pending-edit queue, and the operations over them — reconciliation, the composed-query/load fixpoint, demand loading, interning, muting, load rules, construction — calling back toStagethroughCompositionHooks). Start atusd/stage.rs. Public users import modules (use openusd::{sdf, usd};); the only root-level re-exports areopenusd::Errorandopenusd::Result(src/error.rs), the crate-wide error enum every module error nests into. A#[macro_export]macro necessarily binds at the root too:impl_token_valueis#[doc(hidden)]there and documented asvt::impl_token_value, the module that owns what it is about, andinclude_schema!is the one macro documented at the root itself. -
usd_utils/- Utilities over layers and stages (C++UsdUtils):all_dependencies(C++UsdUtilsComputeAllDependencies),create_usdz_package(C++UsdUtilsCreateNewUsdzPackage) andmodify_asset_paths(C++UsdUtilsModifyAssetPaths).walk.rsis the walk over the asset paths a layer authors, under aVisitpolicy;discover.rsthe breadth-first discovery of the files a root reaches (Root, an asset path or a layer in hand;Discovery/Source, with a scope of its own for each package the stage holds layers in);package.rsthe placement of each source at one entry and the write. Start atusd_utils/mod.rs. -
openusd-schemas- A separate crate (crates/openusd-schemas), not a module of the core: domain schemas layered onsdf/usd, not part of the AOUSD core spec. It depends onopenusd, so the core must never reference it. Feature-gated per family (geom,lux,media,physics,proc,render,semantics,shade,skel,ui,vol;shadeand several others enablegeom, andlux,physicsandrenderenableshade). See the table in itslib.rs. -
openusd-build- A separate crate (crates/openusd-build): the schema view generatoropenusd-schemasruns as a build dependency. It loads a schema library's schematics and manifest, and the metadata fields and kinds itsplugInfo.jsondeclares, resolves and validates them over the composed stage, and emits the Rust views and token tables the schema crate includes (load→resolve→validate→emit; start at itslib.rs). Its golden tests undertests/regenerate withUPDATE_EXPECTED=1. -
kind- Kinds (C++Kind):kind::Registry(C++KindRegistry) is the table of known kinds and their base kinds, holding the built-in model hierarchy plus the kinds a site declares askind::Decls. It is the single filekind.rsand depends ontfonly. A schema family carries the declarations (usd::SchemaFamily::kinds), and a stage reads the table throughusd::SchemaRegistry::kinds. -
gf/- Graphics Foundations (C++Gf):bytemuck::Podvector / quaternion / matrix types for bulk binary serialization, row-major / row-vector convention, each bridging tovt::ValueviaFrom/TryFrom;gf::Rotation(C++GfRotation), the axis-and-angle rotation the xformOp rotations are built through;gf::DualQuatd(C++GfDualQuatd), the rigid transform dual-quaternion skinning blends;gf::Interval(C++GfInterval), the open-or-closed-ended span a time-sample query is asked over;gf::Range3dandgf::BBox3d(C++GfRange3d,GfBBox3d), the axis-aligned box and the box-with-a-matrix that bounds are computed in; andgf::Cameraandgf::Frustum(C++GfCamera,GfFrustum), a camera as a plain value and the volume it sees, withgf::ProjectionParamsfor what a projection matrix projects andgf::Planeandgf::Rayfor the frustum's intersection tests. Seegf/mod.rsfor the conventions.
# Build the workspace (use --all-features to include the gated schema families)
cargo build --workspace --all-features
# Run tests (including comprehensive format validation tests)
cargo test --workspace --all-targets --all-features
# Lint with Clippy (strict warnings as errors)
cargo clippy --workspace --all-targets --all-features -- -D warnings
# Format code
cargo fmt
# Check formatting
cargo fmt --all -- --check --files-with-diff
# Generate documentation (a broken or private intra-doc link is an error)
cargo doc --workspace --no-deps --all-features
# Run security/dependency audits
cargo deny check
# Run examples
cargo run -p openusd --example dump_usdc -- path/to/file.usdKeep ROADMAP.md rows concise. Each Notes cell should cover three things only:
- Whether it is done — the status emoji in the Status column handles this; the Notes cell does not need to restate it.
- What to read — name the key types or traits a reader would look for
(e.g.
sdf::ChangeList,usd::NamespaceEditor). Do not describe what those types do; their own doc comments are the source of truth. - What is remaining — a short
Remaining — X; Yclause, or aRemaining:bullet list when there are several distinct items, for anything materially incomplete. Drop it once nothing is left.
Avoid enumerating every method, variant, or edge case handled. If the implementation is complete and unremarkable, a one-line entry is fine.
When implementing a new feature from the spec:
- Read
docs/aousd_core_spec_1.0.1.pdfto understand how the feature works and what it does - Research how the C++ OpenUSD implementation handles it: https://github.com/PixarAnimationStudios/OpenUSD
- Review the Python reference implementation if applicable:
vendor/core-spec-supplemental-release_dec2025/
- Project targets Rust version specified in
rust-toolchain.tomlwith MSRV defined inclippy.toml - Maximum line width: 120 characters (rustfmt.toml)
- All warnings treated as errors in CI
- The
[lints]section ofCargo.tomlsets every lint todeny, so findings block a plaincargo buildand are never optional. Silence one only with a narrowly scoped#[allow(...)]carrying a comment for why; do not downgrade or delete the entry inCargo.tomlto make a finding go away. Words thatclippy::doc_markdownshould ignore go inclippy.toml'sdoc-valid-idents - Comprehensive test coverage (50% minimum) with grcov
- Security auditing with cargo-deny
- Pre-1.0: backward compatibility is not a constraint. Prefer the cleanest design and change or remove public APIs freely; don't keep deprecated shims, compatibility shims, or worse-but-compatible behavior. Update all call sites in the same change.
- Prefer an architecturally correct design over a quick, dirty, or bandaid fix.
Fix the root cause at the layer that owns the concept, not the symptom at the
call site. A special case layered onto shared infrastructure (an
if this one formatbranch, or a value re-derived from a string because the right field was not carried) signals the fix is at the wrong altitude — generalize the mechanism or carry the missing state instead. When a module mirrors C++ OpenUSD, factor the concern the way the C++ model does (e.g. a layer's lexical identifier vs. its resolved real path). - Fix a gap you find in the current session, even when it sits outside the
module you set out to change. Leave a
TODOonly for a large remaining gap that needs its own planning, or for one whose fix diverges significantly from the work currently planned. Name the missing generalization in it. ATODOnever stands in for a fix that fits in the current change.TODO(rayon)/TODO(perf)markers for performance seams are separate (see Code Quality).
- Write clean and idiomatic Rust code
- Less is better - prefer functionality offered by stdlib
- Don't write a function or method whose body is just a call to another
free-standing function (a thin delegating wrapper, e.g.
fn open_stack(&self, p) { open_layer_stack(self.resolver(), p) }). Call the free function directly at the call site. A method earns its place only when it adds something — encapsulating private state, a non-trivial default, or an invariant - Keep
pcpcomposition free of interior mutability: noCell/RefCell(and noMutex) onLayerGraph,IndexCache, the indexer, or anything a per-prim build reads. A build is a pure function of its&-inputs, so anything it discovers (errors, demanded-but-unloaded layers) travels out through its return value (BuildOutput), not a shared mutable cell. Accumulate such results in an owned field and merge them up; the cache holds the deferred list as a plainVecit mutates through&mut self - Order a file top-down by importance so the first thing a reader sees is the
main type, not a helper: the primary type definition (and the structs it
depends on) first, then its
implblocks, then free-standing helper functions, then the#[cfg(test)] mod tests. Don't open a file with a small private helper. - Code requires documentation
- Proof read and reword docs and/or comments as needed. The
writing-reviewskill is the revision pass for comments, docs, and commit messages; run it before handing any of them over - Do not use
**bold** — descriptionpattern in doc comments or bullet lists; use plain text or link directly to the item instead - A doc comment documents only its own item. Don't describe another type,
module, or method inside it (e.g. don't enumerate a
Layer's methods in aRelocatetype alias's doc); document each item on the item itself and use an intra-doc link ([`Foo`]) when a cross-reference is genuinely needed - Do not use decorative box-drawing section-divider comments (e.g.
// ── Section ──────); group code with a plain//comment or rely on the item's own doc comment - Never remove comments during refactoring if they are still applicable
- Comments must describe the code as it stands, not its edit history or the
alternatives it didn't take. Don't justify the absence or removal of code, and
don't contrast the chosen approach with a rejected one (e.g. "no separate X
pre-check is needed here", "X was removed because…", "assign directly rather
than through Y", "instead of calling Z", "we use A so we don't B"). This
includes contrasts with a prior implementation's performance or shape — "a
subtree walk rather than a full scan", "O(1) instead of the previous O(n)",
"now keyed by path" — which only make sense to someone who saw the old code;
state the present behavior positively ("walks the
prefixsubtree") instead. Such notes only make sense to someone who saw the prior version or the alternative and are noise to a fresh reader. State what the present code does and a rationale that stands on its own, not what it no longer does or could have done. - Don't reference planning phases or steps (e.g. "Phase 1", "Step 2") in code,
comments, names, fixtures, or commit messages; describe what the code does or,
for deferred work, name the missing feature in a
TODO - Wrap prose at 80 characters — Markdown, plans, design write-ups, and doc-comment text; Rust code still follows rustfmt (120)
- Mark performance/parallelism opportunities with a
TODO(rayon)(orTODO(perf)) comment in new code and when refactoring existing code, instead of optimizing prematurely; say what is independent or parallelizable so the seam is actionable later - Prefer laconic names. Pick the shortest name that reads clearly beside its
siblings, and cite the C++ counterpart in the doc comment; a C++ name is not
transliterated into the identifier (
PointInstancer::mask_atfor C++ComputeMaskAtTime,Xformable::affects_transformforIsTransformationAffectedByAttrNamed). A name does not repeat what its type or module already says (Subset::create, notSubset::create_geom_subset). Drop acompute_orget_prefix (BBox3d::centroid,TetMesh::surface_faces), keepingcompute_only where the plain name is the read of the authored value (compute_points_at_timebeside the generatedpoints_at) - Re-export key types from module roots so users can access them without deep
paths (e.g.
sdf::FieldKeynotsdf::schema::FieldKey) - Reference types through their module, not a fully-qualified or bare path:
use crate::{sdf, gf, tf};then writesdf::TimeCode,gf::Vec3f,tf::Token. Don't write inlinecrate::tf::Token::from(...)(oropenusd::tf::Tokenin tests), and don'tuse crate::tf::Token;to get a bareTokenwhentf::Tokenreads clearly (it also avoids clashes, e.g. theusdalexer's ownToken) - Don't write a fully-qualified
stdpath inline (e.g.std::collections::HashMap::new(),std::cell::RefCell::new(...),std::mem::swap(...)). Add auseat the top of the file and reference the short name (HashMap::new(),RefCell::new(...),mem::swap(...)afteruse std::mem). This applies to everystd::(and external-crate) path: import it, then use the short form - Avoid raw path string manipulations; use
Pathmethods instead of building or parsing path strings manually - Mutate a
Cell/RefCellthrough its own methods, not a deref-assignment: prefercell.replace(v),cell.replace_with(|old| …),cell.take(), and (forCell)cell.set(v)/cell.get()over*cell.borrow_mut() = vor*cell.borrow_mut() += …; when you hold&mut self, reach the value withcell.get_mut()to skip the runtime borrow entirely. Use aCell(not aRefCell) for aCopyfield like a counter or flag - Don't add "Generated with Claude Code" or "Co-Authored-By: Claude" to commits, PRs, or release notes
Integration tests live in each crate's tests/ directory and read fixture
files from its fixtures/. Three suites in the core crate are compliance
checks against external references: composition.rs regenerates the vendor
pcp.txt composition dumps byte-for-byte, text_format.rs compares parsed
.usda files with the Python reference implementation's output, and
text_writer_baselines.rs writes C++ testSdfParsing baselines back
byte-for-byte. The openusd-build
goldens under its tests/ regenerate with UPDATE_EXPECTED=1.
Prefer using USD assets from vendor/usd-wg-assets/ for test fixtures when a
suitable file exists. Only add new files to a crate's fixtures/ when vendor
assets don't cover the specific case needed.
Test code never sits beside production code. Every test-only item — a helper
function, a fixture builder, a constructor that only tests call — lives inside
a #[cfg(test)] mod tests { … } block, and nothing test-only appears at a
module's top level even gated with #[cfg(test)]. That includes
#[cfg(test)] impl Type { … } blocks: a #[cfg(test)] attribute on an item in
the main body still puts test code in the middle of the file a reader is trying
to follow.
A helper several modules' tests share lives in the mod tests of the module
that owns the type it is about, declared pub(crate), and is reached by path:
crate::sdf::layer_registry::tests::collect_with_arcs(…). Don't add a separate
test_support module for it.
Test function names are terse: 2–4 underscore-separated words. Match the
existing naming convention of the file. Prefer add_api_schema_dup_delete over
add_api_schema_clears_duplicate_delete_opinions, light_api_skips_non_light
over read_light_api_returns_none_on_non_light_prim. Drop redundant prefixes
like read_/reads_ when the test file already targets a reader; favour the
subject + outcome (light_api_via_applied_schema) over verbose sentences.
To pull a typed payload out of a Value in tests, use the EnumTryAs-generated
try_as_*() accessor followed by unwrap()/expect("…") rather than a let Value::Variant(x) = … else { panic!() } block. Keep the descriptive message by
preferring expect.
Key external dependencies:
bitflags- Bitflag sets (e.g.PrimPredicate)bytemuck- Safe transmutation for binary dataderive_more- Derive macros for the arithmetic operators ongfvectors andDerefon thesdfspec viewshalf- 16-bit floating point support (re-exported asf16)logos- Lexer generator for USDA tokenizationlz4_flex- Compression for binary formatmemmap2(optional,mmapfeature) - Read-only file mappings a resolver opts into withar::DefaultResolver::map_filesnum-traits- Numeric traitsregex-lite- Lightweight regex engine for thematches_regexexpression functionrustc-hash- The hasher behindsdf::PathMapandsdf::PathSet, whose keys carry a hash of their ownstrum- Enum derive macros (Display, EnumIs, EnumTryAs, IntoStaticStr, etc.)thiserror- Error type derive macros behind every error type, from the per-module errors (sdf::AuthoringError,pcp::QueryError) up to the rootopenusd::Errorthey nest intowindows-sys(Windows only) - The Win32 security API, through which a replaced layer file keeps its access control listzip- USDZ archive readingzmij- The shortest digits of a float or double, chosen as C++ double-conversion chooses them, behind the text writer's numbersserde(optional,serdefeature) - Serialization support
Domain schemas live in the separate openusd-schemas crate, one feature per
family (geom, lux, media, physics, proc, render, semantics,
shade, skel, ui, vol); use --workspace --all-features when building,
testing, or linting.
The project maintains a minimal dependency footprint and uses cargo-deny to prevent license conflicts and vulnerability introduction. Allowed licenses: MIT, Apache-2.0, Zlib, Unicode-3.0.