Skip to content

Remodel schemas into trait-based Prim views#103

Merged
mxpv merged 18 commits into
mainfrom
schema
Jun 4, 2026
Merged

Remodel schemas into trait-based Prim views#103
mxpv merged 18 commits into
mainfrom
schema

Conversation

@mxpv

@mxpv mxpv commented Jun 3, 2026

Copy link
Copy Markdown
Owner

This PR introduces a new schema API that is much closer to its C++
counterpart. Instead of freestanding read_* / *Author functions
returning decoded structs, each schema is modelled as a typed "view"
into a Prim — a newtype wrapper exposing C++-style foo_attr() /
create_foo_attr() handle pairs.

The C++ library models fairly deep schema hierarchies, and each subtype
carries a bunch of common properties we want to reuse. A geometry object
should have Xformable properties so it can be transformed; every
Xformable is in turn Imageable (visibility / purpose), and so on. We
represent that chain with super-traits:

SchemaBase → Imageable → Xformable → Boundable → Gprim → PointBased → Curves
# Lights
SchemaBase → Imageable → Xformable → Boundable → Light → BoundableLight → SphereLight
SchemaBase → Imageable → Xformable → Light → NonboundableLight → DistantLight

A concrete view (Mesh, Camera, Sphere, …) declares its place in
the chain via an impl__schema! macro and inherits every
ancestor's accessors for free.

The other interesting case is cross-family inheritance: UsdLux lights
derive from UsdGeomBoundable / UsdGeomXformable, so a light should
expose the geom transform/extent surface too. This models that directly
— lux, media, and proc enable the geom feature and build on its
trait chain, so e.g. all lights inherit the geom properties.

  • usd::SchemaBase is the root of the view hierarchy (mirrors
    UsdSchemaBase): a view wraps a Prim, declares its SchemaKind, and
    unwraps back via into_prim.

  • Stage becomes a cheap reference-counted handle instead of a borrow
    with a lifetime, which is far easier to work with — Prim /
    Attribute / Relationship now own a Stage clone and are Clone,
    storable independently of the call that produced them. It is Rc +
    per-field cells today, but Stage was refactored so swapping in
    Arc/Mutex for a multithreaded environment is straightforward (in future).

Schemas migrated:

  • UsdGeom
  • UsdLux
  • UsdMedia
  • UsdPhysics
  • UsdProc
  • UsdRender
  • UsdShade
  • UsdSkel
  • UsdUi
  • UsdVol

Examples:

Geom — author and read back through the trait chain:

let stage = usd::Stage::builder().in_memory("scene.usda")?;

// A `Mesh` inherits accessors from its whole chain: `points` from PointBased,
// the subdivision scheme from Mesh itself, `visibility` from Imageable.
let mesh = geom::Mesh::define(&stage, "/World/Mesh")?;
mesh.create_points_attr()?
    .set(vec![[0.0_f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])?;
mesh.create_subdivision_scheme_attr()?.set(geom::SubdivisionScheme::None)?;
mesh.create_visibility_attr()?.set(geom::Visibility::Inherited)?;

// Read it back through a fresh view;
// typed `get` extracts each value directly.
let mesh = geom::Mesh::get(&stage, "/World/Mesh")?.expect("Mesh");
let points = mesh.points_attr().get::<Vec<[f32; 3]>>()?.unwrap_or_default();
let scheme = mesh.subdivision_scheme_attr().get::<geom::SubdivisionScheme>()?;

Lights example:

let stage = usd::Stage::builder().in_memory("scene.usda")?;

// `SphereLight` is both a `Light` (intensity / color) and a geom `Xformable`,
// so it can be placed in the scene — it inherits the geom trait chain.
let light = lux::SphereLight::define(&stage, "/World/Key")?;
light.create_intensity_attr()?.set(5000.0_f32)?;
light.create_color_attr()?.set([1.0_f32, 0.95, 0.9])?;
light.create_radius_attr()?.set(0.5_f32)?;
let light = light.set_translate([0.0, 4.0, 0.0])?;

Generic over any Xformable:

/// Place any transformable prim — a `Mesh`, `Camera`, light, `Xform`, … — by
/// writing its transform stack. Works for every view in the `Xformable` chain.
fn place<X: Xformable>(prim: X, translate: [f64; 3], scale: [f32; 3]) -> Result<X> {
    prim.set_translate(translate)?.set_scale(scale)
}

It's very likely that I missed bunch of APIs at each level, but that should be easy to address on as needed basis.

cc: @bresilla WDYT?

mxpv added 8 commits June 2, 2026 15:14
Wrap the stage's shared state in `Rc<StageInner>` (mirroring C++
UsdStageRefPtr) with a `Deref` to it, so `Stage` clones cheaply and all
interior mutability stays in per-field cells on `StageInner`.

`Prim`, `Attribute`, `Relationship`, and `VariantSets` now own a `Stage`
clone instead of borrowing `&'s Stage`, dropping the `'s` lifetime from
the composed-handle API. Handles are `Clone` and can be stored past the
call that produced them; the schema author modules drop the propagated
lifetime mechanically.

`Stage::initial_load_set` and `population_mask` lose `const` (field
access now goes through the non-const `Rc`/`Deref`).
Introduce `usd::SchemaBase` (C++ `UsdSchemaBase`): a view wraps a `Prim`
and exposes `prim()`, `path()`, `stage()`, `from_prim` / `into_prim`, and
a required `KIND` const (`usd::SchemaKind`) driving the `is_concrete` /
`is_typed` / `is_*_api_schema` queries. A blanket `From<S: SchemaBase>
for Prim` unwraps any view to its prim.
Replace the freestanding read_*/define_* functions and decoded Read*
structs with typed Prim newtype views mirroring the C++ UsdGeom class
hierarchy: a SchemaBase → Imageable → Xformable → Boundable → Gprim →
PointBased → Curves trait chain, with concrete views (Mesh, GeomSubset,
Points, TetMesh, BasisCurves, NurbsCurves, HermiteCurves, NurbsPatch,
PointInstancer) joining the existing Camera/Xform/Scope/shapes. Each
view declares its chain via the impl_geom_schema! macro and exposes
C++-style foo_attr()/create_foo_attr() handle pairs; authoring now lives
on the view instead of a separate author/ module.

Add Attribute::get_metadata for reading primvar interpolation through
the handle, and From<scalar> conversions on sdf::Value so set() takes
bare scalars. Drop the find_geom_prims/read_kind helpers and the decoded
Read* structs; the token enums move into the geom module root.
Convert the freestanding read_*/define_*/apply_* functions and decoded
Read* structs into typed Prim views, mirroring the geom migration. Lights
are UsdGeom prims, so the lux feature now enables geom and reuses its
Imageable/Xformable/Boundable traits: a `Light` interface trait carries
the common UsdLuxLightAPI inputs, BoundableLight/NonboundableLight are the
abstract bases, and the concrete lights, LightFilter, and the LightAPI/
ShapingAPI/ShadowAPI/LightListAPI applied schemas are newtype views with
C++-style foo_attr()/create_foo_attr() handle pairs.

Each view declares its chain via the impl_lux_schema! macro (including an
applied_api arm for the single-apply API views). Drop find_lux_prims/
is_light_type and the Read* structs; keep the token enums. Collapse the
module to tokens/traits/lights, with the shared helpers and macro in mod.
Attribute::get / get_at / get_metadata are now generic over
T: TryFrom<sdf::Value>, so reads decode straight to the Rust type
(attr.get::<f32>()?) or to sdf::Value for the raw value, mirroring
C++ UsdAttribute::Get's templated out-type.

Add From<T> for sdf::Value over scalars, fixed-size vectors/matrices,
and their Vec<…> array forms, so set() takes bare values
(set(vec![4]), set(vec![1.0_f64, …])). Types whose representation maps
to several variants ([*;4] floats, Vec<String>) are omitted so the
wrong variant is never chosen silently.
Convert the media schema to typed Prim views mirroring geom/lux:
SpatialAudio (a geom::Xformable) and the AssetPreviewsAPI single-apply
view, dropping the read_*/define_*/Read* surface. media now enables the
geom feature, since SpatialAudio is UsdGeomXformable-derived.

Add sdf::TimeCode (= SdfTimeCode), a value newtype over f64 with
From/TryFrom<Value>, so timecode attributes read/write typed via
get::<sdf::TimeCode>() / set(sdf::TimeCode(..)).

Settle the schema-module convention: per family, mod.rs (glue + macro +
helpers + token enums), tokens.rs, schema.rs (grouped view structs), and
optional traits.rs. Fold geom's internal.rs into mod.rs, rename lux
lights.rs to schema.rs, merge shade preview.rs into types.rs, and drop
decorative box-drawing divider comments (with a CLAUDE.md rule).
Convert schemas::proc to the trait-view newtype pattern, completing
the geom/lux/media/proc family migration: GenerativeProcedural is now
a geom::Boundable view (proc gains the geom feature) replacing the old
reader/author functions.

Consolidate the schema-view helpers the migrated families had each
copied: the get-gates (get_typed / get_typed_any / get_with_api) move
into schemas::common with a unified slice-based get_with_api, and the
one-liner authoring/apply wrappers (create / create_uniform /
create_uniform_token / apply_api) are inlined at their call sites via
the Attribute builder.
Make schema attribute get/set ergonomic. Each geom token enum gains
From<Enum> for sdf::Value and TryFrom<sdf::Value>, so authoring is
attr.set(SubdivisionScheme::Loop)? and reading is
attr.get::<SubdivisionScheme>()?. Add TryFrom<Value> for the float
vec-array payloads (Vec<[f32; 2/3/4]>) so point/normal/UV attributes
extract directly via get::<Vec<[f32; 3]>>().

Refresh the README example to show reading a geom::Mesh through the
typed view, dropping the older field-read and in-memory authoring
snippets.
@mxpv mxpv changed the title Remodel schemas into trait-based Prim views Remodel schemas into trait-based Prim views (UsdGeom, UsdLux, UsdMedia, and UsdProc) Jun 3, 2026
@mxpv mxpv requested a review from Copilot June 3, 2026 06:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR overhauls the Rust schema surface to mirror the C++ OpenUSD “view over prim” model: schemas become typed, trait-composed views over usd::Prim with foo_attr() / create_foo_attr() accessors, and the stage/handle model is refactored so composed handles are cheaply cloneable and storable.

Changes:

  • Introduces usd::SchemaBase / usd::SchemaKind as the shared foundation for schema views and trait-based schema hierarchies.
  • Refactors usd::Stage into a cheap Rc-backed handle (Stage(Rc<StageInner>)), removing lifetimes from Prim/Attribute/Relationship handles.
  • Migrates UsdGeom, UsdLux, UsdMedia, and UsdProc to the new trait-view model; updates tests, docs, and feature dependencies accordingly.

Reviewed changes

Copilot reviewed 90 out of 90 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/proc_reader.rs Updates integration test to exercise new UsdProc view API.
tests/media_reader.rs Updates integration tests to use UsdMedia views and adds token round-trip checks.
src/usd/stage.rs Refactors Stage into an Rc handle and removes lifetimes from returned composed handles.
src/usd/schema.rs Adds SchemaBase/SchemaKind trait foundation for view hierarchy.
src/usd/mod.rs Wires new schema module and re-exports schema foundation types.
src/usd/collection.rs Adapts collection helpers to lifetime-free Prim/Relationship handles.
src/sdf/value.rs Expands typed conversions and adds From<T> for Value for unambiguous Rust types.
src/sdf/mod.rs Introduces sdf::TimeCode as a first-class value type for timecode attributes.
src/schemas/vol/author.rs Updates authoring helpers to lifetime-free Prim handles.
src/schemas/ui/author.rs Updates UI authoring helpers to lifetime-free Prim/Attribute handles.
src/schemas/skel/author/skeleton.rs Updates skel authoring handle types to lifetime-free Prim.
src/schemas/skel/author/skel_root.rs Updates skel root authoring API to return lifetime-free Prim.
src/schemas/skel/author/skel_animation.rs Updates skel animation authoring handles to lifetime-free Prim.
src/schemas/skel/author/blend_shape.rs Updates blend shape authoring handles to lifetime-free Prim.
src/schemas/skel/author/binding.rs Updates skel binding authoring handles to lifetime-free Prim.
src/schemas/shade/shader.rs Updates shader authoring handles to lifetime-free Prim/Attribute.
src/schemas/shade/preview.rs Removes old preview-surface reader module (moved/re-exposed elsewhere).
src/schemas/shade/mod.rs Adjusts exports to re-home preview-surface APIs under types.
src/schemas/shade/material.rs Updates material/node-graph authoring handles to lifetime-free Prim/Attribute.
src/schemas/shade/connectable.rs Updates connectable authoring helpers to lifetime-free Prim/Attribute.
src/schemas/render/author/var.rs Updates render authoring handles to lifetime-free Prim.
src/schemas/render/author/settings.rs Updates render settings authoring handles and shared trait impls.
src/schemas/render/author/product.rs Updates render product authoring handles and shared trait impls.
src/schemas/render/author/pass.rs Updates render pass authoring handles to lifetime-free Prim.
src/schemas/render/author/base.rs Updates shared render-settings base trait to lifetime-free Prim.
src/schemas/proc/types.rs Removes old decoded read-struct type (replaced by views).
src/schemas/proc/schema.rs Adds GenerativeProcedural schema view with attr/create accessors.
src/schemas/proc/read.rs Removes old read_* API (replaced by typed view getters).
src/schemas/proc/mod.rs Re-structures proc module around views + macro-based trait-chain impl.
src/schemas/proc/author.rs Removes old authoring handle API (replaced by view authoring).
src/schemas/physics/author/scene.rs Updates physics scene authoring handles to lifetime-free Prim.
src/schemas/physics/author/rigid_body.rs Updates physics rigid-body/mass authoring handles to lifetime-free Prim.
src/schemas/physics/author/limit_drive.rs Updates multi-apply physics authoring handles to lifetime-free Prim.
src/schemas/physics/author/joint.rs Updates physics joint authoring handles and shared setter trait.
src/schemas/physics/author/groups.rs Updates physics group/filter authoring handles to lifetime-free Prim.
src/schemas/physics/author/collision.rs Updates collision/material authoring handles to lifetime-free Prim.
src/schemas/mod.rs Updates schema family overview to reflect trait-view migrations.
src/schemas/media/types.rs Removes old decoded types/read struct (replaced by views + enums in mod).
src/schemas/media/read.rs Removes old read_* API (replaced by typed view getters).
src/schemas/media/previews.rs Removes old AssetPreviews free functions (replaced by API view).
src/schemas/media/mod.rs Re-structures media module around views + token enums + trait-chain macro.
src/schemas/media/author.rs Removes old authoring handle API (replaced by view authoring).
src/schemas/lux/traits.rs Introduces trait interfaces for shared light surfaces (Light, marker bases).
src/schemas/lux/author/shaping.rs Removes old shaping/shadow authoring handles (replaced by views).
src/schemas/lux/author/nonboundable.rs Removes old nonboundable light authoring helpers (replaced by views).
src/schemas/lux/author/mod.rs Removes old lux authoring module surface (replaced by views).
src/schemas/lux/author/light_list.rs Removes old LightList authoring helper (replaced by views).
src/schemas/lux/author/light_api.rs Removes old LightAPI authoring helper (replaced by views).
src/schemas/lux/author/dome.rs Removes old dome light authoring helpers (replaced by views).
src/schemas/lux/author/common.rs Removes old lux authoring internals (replaced by view authoring).
src/schemas/lux/author/boundable.rs Removes old boundable light authoring helpers (replaced by views).
src/schemas/geom/xform.rs Removes old freestanding xform reader (replaced by trait/view model).
src/schemas/geom/tokens.rs Adds additional geom token constants used by new trait/view accessors.
src/schemas/geom/read.rs Removes old freestanding geom readers (replaced by trait/view model).
src/schemas/geom/points.rs Adds Points/TetMesh concrete views built on PointBased chain.
src/schemas/geom/pointbased.rs Adds PointBased trait with shared point-geometry accessors.
src/schemas/geom/imageable.rs Adds Imageable trait with accessors + composed compute helpers.
src/schemas/geom/grouping.rs Adds Xform/Scope concrete views.
src/schemas/geom/gprim.rs Adds Gprim trait with shared gprim accessors.
src/schemas/geom/boundable.rs Adds Boundable trait with extent accessors.
src/schemas/geom/author/xform.rs Removes old xform authoring helpers (replaced by view authoring).
src/schemas/geom/author/shapes.rs Removes old shape authoring helpers (replaced by views).
src/schemas/geom/author/mod.rs Removes old geom authoring module surface (replaced by views).
src/schemas/geom/author/mesh.rs Removes old mesh/subset authoring helpers (replaced by views).
src/schemas/geom/author/imageable.rs Removes old imageable authoring helpers (replaced by views).
src/schemas/geom/author/common.rs Removes old geom authoring internals (replaced by views).
src/schemas/geom/author/camera.rs Removes old camera authoring helper (replaced by view).
src/schemas/common.rs Updates shared helpers; adds view “get” gates (get_typed, get_with_api, etc.).
ROADMAP.md Updates roadmap entries to reflect trait-view migration status.
README.md Updates examples to show reading via schema views and Stage API changes.
CLAUDE.md Updates contributor notes/docs to reflect new Stage/schema view architecture.
Cargo.toml Makes lux/media/proc depend on geom; adds derive_more dependency.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/schemas/proc/mod.rs Outdated
Comment thread src/schemas/media/mod.rs Outdated
Comment thread src/schemas/geom/imageable.rs Outdated
Comment thread tests/media_reader.rs Outdated
Comment thread tests/proc_reader.rs Outdated
mxpv added 4 commits June 2, 2026 23:33
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
The per-file token() decode helper in the reader tests is obsolete now
that String: TryFrom<Value> accepts both Token and String. Replace each
call site with get::<String>() / get_metadata::<String>() and remove the
helper from all four reader test files.
Convert schemas::vol to the trait-view pattern, completing the
geom/lux/media/proc/vol family migration: Volume is a geom::Gprim and
the field prims build on geom::Xformable via the FieldBase / FieldAsset
interface traits, with concrete OpenVDBAsset / Field3DAsset views.
Replaces the old reader/author functions and gains the geom feature.

Hoist the impl_token_value! macro (From/TryFrom<Value> for token enums)
from geom into schemas::common so vol's VectorDataRoleHint reuses it
instead of hand-rolling the conversions. Also drop a couple of redundant
self.0 derefs in the vol and media views.
Rewrite the foo_attr() getter doc comments across geom, lux, media, and
proc to the template used for vol: a plain description of what the
attribute is and is for, the class-qualified C++ getter, and the value
type plus the get::<T>() call to read it. Drops the old terse
"attribute handle" one-liners; no code changes.
@mxpv mxpv changed the title Remodel schemas into trait-based Prim views (UsdGeom, UsdLux, UsdMedia, and UsdProc) Remodel schemas into trait-based Prim views Jun 3, 2026
mxpv added 5 commits June 3, 2026 11:31
Convert schemas::physics to the trait-view pattern. The typed prims
(Scene, CollisionGroup, the generic Joint and its five subtypes via the
shared JointBase interface) and the seven single-apply API schemas
follow the established newtype-view pattern. DriveAPI and LimitAPI are
the first multiple-apply views: a {prim, name} newtype keyed by a DOF
instance, with apply/get/get_all (C++ Apply/Get/GetAll) and attributes
at drive:<dof>:physics:* / limit:<dof>:physics:*.

Replaces the old read_*/*Author functions; the JointAxis/DriveType/
CollisionApprox token enums reuse the shared impl_token_value! macro.
The integration test moves to the view API against the fixture.
Convert the UsdShade schema family from read_*/author helpers to the
trait-view newtype pattern. A `Connectable` interface trait carries the
`inputs:`/`outputs:` surface (plus `connectability`, `renderType`, and
the `Attribute::connect_to` connection helper) and backs the typed
`Shader` / `NodeGraph` / `Material` views; `MaterialBindingAPI` becomes a
single-apply view keeping the direct/collection binding resolution, and
`Material::compute_surface_source` + `read_preview_surface` stay as
connection-following computation. Add a `base_name` helper for the
inputs:/outputs: namespace.

Dissolve the bespoke `find_shade_prims` walk (no UsdShade analog) in
favour of stage traversal gated through the typed `get`, mirroring C++
`prim.IsA<UsdShadeMaterial>()`.
Convert UsdSkel from read_*/author helpers to the trait-view newtype
pattern, gaining skel = ["geom"]: SkelRoot and Skeleton are geom
Boundable prims, SkelAnimation and BlendShape are typed, and
SkelBindingAPI is single-apply. Decoded getters (Skeleton::joints /
bind_transforms, SkelBindingAPI::joint_indices / interpolation, …) own
the numeric coercion the toolkit needs.

Keep the time-independent object model (Topology, AnimMapper,
SkeletonResolver, SkinningResolver, SkelAnimQuery, discover_bindings,
the LBS / blend-shape math), rewired to build from the views via
from_skeleton / from_binding constructors. Dissolve the bespoke
find_skel_prims / find_skel_roots / discover_skeletons buckets in favour
of stage traversal gated through the typed get. This completes the
schema trait-view remodel — every family is now on the view pattern.

Also drop the hidden `# Ok::<(), Box<dyn Error + Send + Sync>>(())`
return line from every schema mod.rs `# Example`, switching the bodies
to `.unwrap()` so the source reads as plainly as the rendered docs.
@mxpv mxpv requested a review from Copilot June 3, 2026 21:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

The trait-view migration left several schemas::common helpers without
callers: every family now inlines attribute authoring via the Attribute
builder and reads through view getters. Remove the author_* helpers,
varying_attribute, and the read_asset/read_f64/read_int readers (kept
only by the module-wide allow(dead_code)), and refresh the module doc.
Keep the live get_typed/get_typed_any/get_with_api gates, read_token,
value_as_asset_str, and the impl_token_value\! macro.
@bresilla

bresilla commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

This is great - it's exactly the shape we landed on, and seeing it across all 10 families at once is convincing. The super-trait chain reads really naturally.

The cross-family bit (lights inheriting the geom Xformable/Boundable surface) is the part I wasn't sure how you'd model, and pulling it in via the geom feature is clean. The generic place<X: Xformable> example sells it.

And Stage becoming a storable Clone handle is the big one for me - that was my main worry in #93 for the editor, since I need to hold Prim/Attribute across frames. Rc + cells is fine for now; good that it's set up so Arc/Mutex can drop in when I actually need Send.

I'll hold off on namespace editing until this lands and build it on the new Prim views, so it doesn't fight the reshape. No real notes - happy to see it go in.

@mxpv mxpv merged commit ecd3099 into main Jun 4, 2026
10 checks passed
@mxpv mxpv deleted the schema branch June 4, 2026 15:13
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.

3 participants