Exporting ovstage to USD#

The population export API (ovstage_population_export.h) authors a selected slice of ovstage state into USD. Use it to bake simulation results, produce a sparse overlay, or persist application-selected properties without passing OpenUSD C++ objects across the public ABI.

Note

Export is a snapshot-authoring operation, not a live synchronization system. USD-to-ovstage population is intentionally lossy, so export cannot reconstruct every opinion from the original USD source.

Destination Ownership and Persistence#

The public C ABI addresses USD storage with strings so no OpenUSD object crosses the boundary. Choose the destination form from the required lifetime:

  • One snapshot, one durable file: ovstage_population_export_to_usd_file and Python population.export_to_usd_file create an empty private destination, export once, save, and close. For populated USD hierarchies, Python population.export_typed_hierarchy_to_usd_file supplies the common typed export policies automatically. Existing stored content is not read; a successful save replaces it. This workflow requires no pxr Python bindings.

  • Several exports, one durable file: create an ovstage_population_export_destination_handle_t or Python ExportDestination. Exports accumulate in memory until an explicit *_destination_save / destination.save(). EMPTY starts without reading storage; OPEN_EXISTING opens and preserves an existing layer stack and authors its root layer. Closing or context-manager exit never saves.

Every destination is opened and owned by ovstage. There is no route that authors into a USD layer or stage owned by the calling application: OpenUSD does not promise that a layer or stage identifier means the same thing in two USD runtimes, so such a route would only work when the caller happens to share ovstage’s USD build. Hand off through storage instead — export to a file, then open or reload that file in the host’s own USD runtime.

Reusable destinations are bound to their source ovstage instance, which must outlive them. Only one active destination may own a canonical identifier in the process. A successful save writes the complete current destination and leaves it open for later export/save calls, but OpenUSD does not promise portable atomic replacement; a failed save may have modified storage. A failed export leaves the reusable destination’s in-memory state unchanged. Dropping a live Python ExportDestination queues a best-effort no-save release and emits ResourceWarning; use with or call close() explicitly instead.

Export a Typed USD Hierarchy#

When the runtime stage was populated from USD, use the typed-hierarchy helper to export an ordinary subtree without rebuilding its USD representation as rule tables. This example preserves the Sphere and Cube types, each prim’s own applied Physics API schema, and the supported attributes declared by those schemas:

    scene = """#usda 1.0
def Xform "World"
{
    def Xform "Props"
    {
        def Sphere "Ball" (prepend apiSchemas = ["PhysicsCollisionAPI"])
        {
            double radius = 1.5
            bool physics:collisionEnabled = true
        }
        def Cube "Crate" (prepend apiSchemas = ["PhysicsRigidBodyAPI"])
        {
            double size = 2
            bool physics:rigidBodyEnabled = true
        }
    }
}
"""
    population.open_usd_from_string(
        stage,
        scene,
        ordinal=1,
        time_code=math.nan,
        domains=PopulationDomain.ALL,
    )
    stage.advance_write_floor(1).wait()

    output = tmp_path / "typed-hierarchy.usda"
    report = population.export_typed_hierarchy_to_usd_file(
        stage,
        str(output),
        "/World/Props",
        1,
    )

The helper selects root_path and its descendants, authors typed DEF prims from usd-prim-type, applies each prim’s recognized usd-schemas, and projects supported schema-declared attributes under their source names. It also handles recognized omni:xform values. Application-owned custom attributes remain excluded unless their namespaces are named with include_custom_namespaces=[...].

Export Runtime-Created Data with Rules#

Runtime-created columns may not carry enough USD representation metadata for the typed helper. This example writes local transforms, seals the ordinal, then exports and saves the recognized omni:xform column. The descriptor defaults produce sparse OVER prims and exact USD xform ops:

prim_paths = ["/World/Red", "/World/Green", "/World/Blue"]
translations = [(0.0, 0.0, 0.0), (2.0, 0.0, 0.0), (4.0, 0.0, 0.0)]

with PathDictionary(stage) as paths:
    path_list = paths.create_path_list_from_strings(prim_paths)
    try:
        with stage.query_from_path_list(path_list) as query:
            stage.write_attribute(
                query,
                "omni:xform",
                ordinal=1,
                tensors=_translation_matrices(translations),
                is_array=False,
                semantic=AttributeSemantic.MATRIX,
            ).wait()
    finally:
        paths.destroy_path_list(path_list)
stage.advance_write_floor(ordinal=1).wait()

# The one-shot API creates an empty destination, exports, saves, and closes.
report = population.export_to_usd_file(
    stage,
    str(destination),
    ordinal=1,
    attribute_rules=["omni:xform"],
)

The destination is replaced by a layer containing this export. This is an overlay, not a standalone typed scene. Use the typed-hierarchy helper when the source carries USD type and schema metadata; use explicit rules when runtime columns must be mapped to a different USD representation.

Reusable Destination#

Use a reusable destination when several selected exports must accumulate before one save, or when existing composed content must be preserved. Saving is always explicit; leaving the context without save() discards unsaved changes:

with population.ExportDestination.create(stage, str(destination)) as usd:
    usd.export(
        ordinal=1,
        prim_predicate=population.PrimPredicate.is_under_path("/World/Red"),
        attribute_rules=[{"source_attribute_name": "temperature", "usd_type_name": "float"}],
    )
    usd.export(
        ordinal=1,
        prim_predicate=population.PrimPredicate.is_under_path("/World/Blue"),
        attribute_rules=[{"source_attribute_name": "temperature", "usd_type_name": "float"}],
    )
    usd.save()

Copy Custom Attributes Without Renaming#

An empty destination name already means “reuse the source name.” A glob rule can therefore publish an application-owned namespace without listing every column. Custom type inference is explicit because arbitrary runtime storage does not otherwise imply a USD value type:

report = population.export_to_usd_file(
    stage,
    str(destination),
    ordinal=1,
    attribute_rules=[
        {
            "source_attribute_name": "userProperties:*",
            "source_attribute_name_match": population.OVSTAGE_POPULATION_EXPORT_NAME_MATCH_GLOB,
            "flags": (
                population.OVSTAGE_POPULATION_EXPORT_RULE_INFER_CUSTOM_TYPE
                | population.OVSTAGE_POPULATION_EXPORT_RULE_REQUIRED
            ),
        }
    ],
)

Map Runtime Data to a USD Schema#

Explicit source and destination names matter when the runtime model differs from the USD schema. Here a solver-owned radius column is mapped onto Sphere.radius. DEF mode reads the source usd-prim-type metadata to create a durable Sphere, while the rule states the destination attribute’s USD type explicitly:

report = population.export_to_usd_file(
    stage,
    str(destination),
    ordinal=1,
    layer_mode=population.OVSTAGE_POPULATION_EXPORT_LAYER_MODE_DEF,
    attribute_rules=[
        {
            "source_attribute_name": "solver:collisionRadius",
            "destination_attribute_name": "radius",
            "usd_type_name": "double",
            "flags": population.OVSTAGE_POPULATION_EXPORT_RULE_REQUIRED,
        }
    ],
)

Export a Typed Mesh Subtree#

The same helper handles a common geometry subtree without an attribute allowlist or blanket API-schema rules. The tested source contains two Meshes with geometry and different Physics APIs; the call is only the root selection:

report = population.export_typed_hierarchy_to_usd_file(
    stage,
    str(destination),
    "/World/SimulatedMeshes",
    ordinal=1,
)

The exporter derives each Mesh type and preserves the APIs recorded on that specific prim: the static Mesh remains collision-only while the dynamic Mesh also receives PhysicsRigidBodyAPI. Token-valued properties such as orientation and subdivisionScheme export when their runtime values are pre-interned token-ID rows and the selected schema or an explicit rule supplies the token type. Relationships and connections still require property-edge rules; positive connection export remains unavailable.

Export Only Changed Properties#

After a baseline export, a later file can contain only ordinary properties dirtied in the open interval (since_ordinal, ordinal]. The result is an overlay: whoever consumes it must compose it over the baseline export:

report = population.export_to_usd_file(
    stage,
    str(changed_destination),
    ordinal=2,
    selection=population.OVSTAGE_POPULATION_EXPORT_SELECTION_CHANGED_PROPERTIES_SINCE_ORDINAL,
    since_ordinal=1,
    attribute_rules=[rule],
)

Metadata-Driven C Descriptor#

The C API expresses the same metadata-driven policy through the public ovstage_population_export_desc_init_typed_hierarchy convenience initializer. It initializes a snapshot descriptor, selects the root and descendants, chooses DEF mode, applies recorded schemas, and selects schema-declared projection. The caller-owned root ovx_string_t object and the character bytes referenced by that view remain borrowed by the descriptor through a synchronous call or successful async enqueue; do not destroy or change either sooner.

    constexpr char source[] = R"usd(#usda 1.0
def Xform "World"
{
    def Xform "Props"
    {
        def Sphere "Ball" (prepend apiSchemas = ["PhysicsCollisionAPI"])
        {
            double radius = 1.5
            bool physics:collisionEnabled = true
        }
        def Cube "Crate" (prepend apiSchemas = ["PhysicsRigidBodyAPI"])
        {
            double size = 2
            bool physics:rigidBodyEnabled = true
        }
    }
}
)usd";
    ASSERT_TRUE(waitPopulation(runtime.get(),
        ovstage_population_open_usd_from_string(
            runtime.get(), str(source), 1, NAN, OVSTAGE_POPULATION_DOMAIN_ALL),
        "open_usd_from_string"));
    ovstage_write_floor_desc_t floor{};
    floor.ordinal = 1;
    floor.scope = OVSTAGE_SCOPE_ALL;
    ASSERT_TRUE(waitOp(runtime.get(), ovstage_advance_write_floor(runtime.get(), &floor), "advance_write_floor"));

    const ovx_string_t root = str("/World/Props");
    ovstage_population_export_desc_t desc{};
    ASSERT_EQ(ovstage_population_export_desc_init_typed_hierarchy(&desc, 1, &root), OVSTAGE_OK);

    ovstage_population_export_report_t report{};
    ASSERT_EQ(ovstage_population_export_to_usd_file(
                  runtime.get(), str(destination.identifier()), &desc, &report),
              OVSTAGE_OK);

Shared population predicates select source prims and properties. Rule tables are destination-authoring instructions for renaming, custom data, and other explicit mappings. Schema-declared projection can author supported properties without a matching attribute rule.

Descriptor Model#

Initialize every descriptor with ovstage_population_export_desc_init_snapshot or ovstage_population_export_desc_init. A snapshot descriptor records the current committed ordinal expected by the caller. struct_size identifies this header revision; it is not an automatic compatibility mode for older descriptor layouts. Export rejects stale and future ordinals rather than silently reading a different snapshot.

Selection#

EXPLICIT_PREDICATE exports the current committed state selected by the predicates and rules. CHANGED_PROPERTIES_SINCE_ORDINAL further intersects property authoring with the open interval (since_ordinal, ordinal]. It is useful for incremental overlays, but it does not make the output a complete standalone description: selected values are authored as current USD default opinions, never as intermediate time samples.

prim_predicate and property_predicate use the same immutable predicate graphs as generic USD-to-ovstage population. Compose path, concrete type, applied-schema, property-name, namespace, and property-kind leaves with AND, OR, and NOT. Predicates select source candidates; rule-local path and name matching routes selected data to destination opinions.

Rule Tables#

Rule

Authors

Important controls

Prim

Prim specs and optional type names

Exact/prefix path, OVER/DEF, unknown-type metadata key

API schema

Single- or multiple-apply API schemas

Schema name, instance name, required flag

Attribute

Values and recognized transforms

Exact/glob source name, rename, explicit/schema/inferred USD type

Property edge

Relationships, material bindings, or connections

Explicit property kind and destination value type for connections

Metadata

Prim metadata, layer metadata/custom data, or references

Explicit destination kind and optional key rename

Set OVSTAGE_POPULATION_EXPORT_RULE_REQUIRED when failure to match, convert, or author a rule must fail the operation. Optional rules instead contribute to the report’s skipped or unsupported counters. Type inference for custom attributes is opt-in through RULE_INFER_CUSTOM_TYPE; an explicit USD type is usually clearer at a public boundary.

Layer and Transform Policy#

LAYER_MODE_OVER is the conservative default for sparse overlays. LAYER_MODE_DEF creates durable definitions and uses a prim rule or readable usd-prim-type metadata to choose type names.

SOURCE_API_SCHEMAS_APPLY_RECORDED applies recognized effective schemas from usd-schemas on each selected prim. PROJECTION_SCHEMA_DECLARED then authors selected runtime attributes declared by the concrete type or those applied schemas. Explicit rules still take precedence and remain necessary for renaming, custom namespaces, relationships, connections, and metadata.

The default transform policy converts recognized local transforms into USD xform ops when the value has an exact supported decomposition. TRANSFORM_USD_MATRIX_OP authors an explicit matrix op, while TRANSFORM_NONE omits recognized transform columns. Unsupported transform states are reported rather than silently approximated.

Reports and Failures#

The report summarizes examined, exported, skipped, and unsupported items. Its source_states_unavailable counter records a rejected stale or future source ordinal. Use required rules and the returned ovstage_api_status_t for correctness requirements. In Python, a failing blocking export or terminal async wait raises OvstageError.

Asynchronous Convenience API#

The async functions deep-copy the descriptor graph and enqueue work on the source instance’s population FIFO. The one-shot file operation does not complete until the file has been saved:

operation = population.export_to_usd_file_async(
    stage,
    str(destination),
    ordinal=1,
    attribute_rules=["omni:xform"],
)
report = operation.wait()
const ovstage_population_enqueue_result_t enqueue =
    ovstage_population_export_enqueue_to_usd_file(
        runtime.get(), str(destination.identifier()), &desc);
ASSERT_EQ(enqueue.status, OVSTAGE_OK);

ovstage_population_export_report_t report{};
ASSERT_EQ(ovstage_population_export_wait_op(
              runtime.get(), enqueue.op_index, OVSTAGE_TIMEOUT_INFINITE, &report),
          OVSTAGE_OK);

Every accepted async operation must receive an export-specific terminal wait. A timeout does not consume the result, so wait again. Reusable-destination create, export, save, and destroy operations share the source instance’s population FIFO; they may be enqueued in order before waiting. The create call reserves a handle synchronously, but only its terminal wait proves that opening succeeded. Do not mix synchronous calls with pending async work on the same destination. An export-specific wait observes only its export/destination operation. Use the generic population wait separately to observe and drain unrelated population operations (including their failures).

Limitations#

Export authors the runtime representation available at one committed ordinal. It is not a reversible copy of the USD file that originally populated ovstage. The following limitations are important when choosing rules and interpreting the result.

Export reads one snapshot, not its history#

Export reads the values present at ordinal. since_ordinal limits which dirty properties are selected, but it does not provide every value that existed between the two ordinals. For example, if radius changed from 1 to 2 and then to 3, an incremental export authors the current value 3, not two historical edits. Historical payload or topology addressing is not available.

Applied API schemas require an explicit policy#

The low-level descriptor defaults to explicit API-schema rules. Set SOURCE_API_SCHEMAS_APPLY_RECORDED, or use the Python typed-hierarchy helper, to preserve each selected prim’s recognized usd-schemas. Unrecognized or malformed schema metadata follows unknown_metadata_policy. Explicit API-schema rules remain necessary when the destination schema assignment should differ from the source.

Some USD property representations cannot be exported#

Pre-interned token-ID scalar and array rows can be authored as USD token and token-array values when a selected schema or explicit rule supplies the token type. For example, a Mesh’s token-valued subdivisionScheme exports through a matching attribute rule. Attribute connection targets, such as a shader inputs:roughness connection, still cannot be authored by a positive connection rule, and a value plus its connection cannot be round-tripped together.

Relationship and material-binding target lists are supported when the source column carries the documented relationship-path semantic. Always choose relationship, connection, or material binding explicitly; a zero-initialized property rule is invalid. Optional unsupported rules increase the report’s skipped or unsupported counters. Mark a rule required when this must instead fail the export.

Changed-property mode is precise for values, not prim structure#

Changed-property selection can identify supported dirty attribute values. It cannot always distinguish a changed prim spec or topology from an ordinary value change. For example, changing only /World/Ball.radius can correctly produce a sparse radius opinion, while a broad changed prim rule may also re-author the /World/Ball prim spec even though its type and hierarchy did not change. Use changed attribute rules for value overlays; do not rely on changed prim rules as an exact structural layer diff.

Reusable OPEN_EXISTING destinations preserve unrelated destination opinions, but export is not a general USD composition copier or source-provenance filter. One-shot file export intentionally starts empty and replaces the stored destination.

Where to Go Next#