Population (USD → ovstage)#

Population transfers selected, composed USD data into the runtime stage. Use ovstage.population to load a scene, apply USD edits, or sample animated values at an ordinal. Population has its own operation queue and waits, separate from direct attribute reads and writes. See Asynchronous Operations.

Note

These entry points are pre-release and provisional (“Draft — API in flux”).

Supply an ordinal above the applicable write floors and wait for population to complete. Apply any direct writes that override populated values afterward, using a newer ordinal. Once all updates are complete, advance the write floor and wait for completion before reading or rendering the results. See Choosing Ordinals for sequencing and Ordinal Admission Policies for policy configuration.

Loading a Scene#

Use ovstage.population.open_usd() for a file or ovstage.population.open_usd_from_string() for USDA text. Both block until population completes; their _async variants return an operation. Advance the watermark and wait before reading the values. The following examples load a scene at a chosen ordinal:

# Load a USD file and populate the runtime table in one op, at the
# caller-owned ordinal 1, then seal that ordinal so reads can see it.
# The application owns the ordinal lifecycle; population never opens
# its own.
population.open_usd(
    stage, str(SCENE), ordinal=1, time=0.0, domains=PopulationDomain.RENDERING
)
stage.advance_write_floor(ordinal=1).wait()
// One population op loads the USD file into the runtime table at ordinal 1;
// sealing that ordinal makes it readable. An explicit time code keeps later
// USD-change synchronization on the same supported timeline sample;
// RENDERING selects meshes/lights/cameras.
ovstage_population_enqueue_result_t popEnq =
    ovstage_population_open_usd_from_file(stage, ovx_string_t{ scenePath.c_str(), scenePath.size() },
                                          /*ordinal*/ 1, /*time*/ 0.0, OVSTAGE_POPULATION_DOMAIN_RENDERING);
waitPop(stage, popEnq, "open_usd");

ovstage_write_floor_desc_t floor1{};
floor1.ordinal = 1;
floor1.scope = OVSTAGE_SCOPE_ALL;
ovstage_enqueue_result_t enq = ovstage_advance_write_floor(stage, &floor1);
waitOp(stage, enq, "advance_write_floor");

Population Domains#

A population domain is a predefined selection of USD prims and properties needed by a consuming library. Pass PopulationDomain values through the domains argument when loading USD:

Domain

What it selects

RENDERING

Scene data for rendering, including meshes, lights, materials, and cameras.

PHYSICS

Scene data for physics, including bodies, colliders, joints, materials, and the geometry and hierarchy they need.

ALL

Both RENDERING and PHYSICS. This does not mean every USD property.

NONE

No predefined selection. Use explicit selectors to choose the prims and properties to populate.

Combine domains with bitwise OR, for example PopulationDomain.RENDERING | PopulationDomain.PHYSICS. Selecting a domain loads data; it does not start a renderer or run a physics simulation. Explicit selectors add to the domain’s selection, as described below.

Physics Selection#

PHYSICS selects prims with supported physics types or applied APIs, including scenes, rigid bodies, colliders, joints, articulations, and physics materials. It also includes supporting prims needed to interpret that data:

Data

What population includes

Physics properties

Attributes and relationships whose names are declared by the supported physics schemas. Unrelated geometry and display properties inherited from base schemas are excluded unless a separate rule includes them.

Shape geometry

Selected geometry properties for colliders and other supported physics roles, such as mesh points and topology or primitive dimensions. A plain render mesh is not selected just because it has geometry.

Relationship targets and subsets

Supporting geometry named by physics relationships, and GeomSubset children of colliders, including their indices and element type.

Materials

Physics material properties and bindings, including prims that author material:binding:physics to a physics material even when those prims have no physics API themselves.

Point instancers

PointInstancer prims whose prototypes contain physics data. Includes their schema properties except extent, plus inactiveIds metadata.

Hierarchy and metadata

Ancestors and transform data, as described in Ancestors and Transforms. Stage metadata includes metersPerUnit, kilogramsPerUnit, and upAxis. On prims selected by a physics schema, population also requests customData:physics:localSpaceVelocities.

Selected attributes can use schema defaults without an authored value. Some physics defaults are computed instead: for example, an unauthored scene gravity uses the stage’s units and up axis. Population also resolves physics material bindings and supported physics collections for consumers.

Applying an arbitrary custom API does not make its properties part of the physics domain. Add explicit selectors for application attributes. Those selectors extend the physics selection; they do not restrict it.

The examples below use scene.usda from the population example. It contains a rigid-body Cube at /World/Body below an Xform at /World. Load its physics data first. The later snippets use the same stage and paths inside the Python context managers; in C, keep stage and dict until the end of the example. scene names the adjacent scene.usda file:

from pathlib import Path

import ovstage
from ovstage import OrdinalRange, PopulationDomain, population

scene = str(Path(__file__).with_name("scene.usda"))
with ovstage.Stage("example.population") as stage, ovstage.PathDictionary(stage) as paths:
    # Select physics data, including Body's rigid-body API and its ancestors.
    population.open_usd(stage, scene, ordinal=1, domains=PopulationDomain.PHYSICS)
    stage.advance_write_floor(ordinal=1).wait()
    # Continue the following examples inside these context managers.

waitPop() checks population completion; waitOp() waits for runtime operations and releases them. check() and checkOvx() stop on errors.

const std::string scene = argc > 1 ? argv[1] : "scene.usda";
const ovx_string_t scenePath{scene.c_str(), scene.size()};
ovstage_instance_desc_t instanceDesc{};
instanceDesc.name = "example.population";
ovstage_instance_t* stage = nullptr;
check(nullptr, ovstage_create_instance(&instanceDesc, &stage), "create_instance");
auto* dict = getPathDictionary(stage);

// Select physics data and wait for population before making it readable.
waitPop(stage, ovstage_population_open_usd_from_file(
    stage, scenePath, 1, OVSTAGE_POPULATION_DEFAULT_TIME,
    OVSTAGE_POPULATION_DOMAIN_PHYSICS), "open_usd");
ovstage_write_floor_desc_t watermark{};
watermark.scope = OVSTAGE_SCOPE_ALL;
watermark.ordinal = 1;
waitOp(stage, ovstage_advance_write_floor(stage, &watermark), "advance watermark");
// Keep stage and dict alive until all examples finish.

Rendering Selection#

RENDERING builds the scene data needed by the renderer. It handles supported geometry, lights, cameras, materials, volumes, and render settings, together with their transforms, hierarchy, and instances. It does not copy every USD prim and property unchanged.

Property selection depends on the kind of prim:

Data

What population includes

Geometry, lights, and cameras

The attributes handled for that prim type. Values can come from authored opinions, schema defaults, or rendering-specific defaults. Some runtime attributes are derived, such as combined transforms.

Applied APIs

Properties of the APIs supported by the relevant rendering handler. Registering and applying an arbitrary API does not guarantee that all its properties are populated.

Primvars

Supported primvars are collected separately. They are not restricted to a fixed list of schema attributes.

Materials and shaders

Material networks, shader inputs and outputs, and their connections use material-specific population. These properties need not be declared in a registered USD schema.

RenderSettings, RenderProduct, and RenderVar

Their supported rendering properties and an additional pass over supported authored attributes.

Other custom attributes

Generally omitted unless explicitly selected or the extra pass described below is enabled.

Schema-defined properties can therefore be present even when they are absent from the USDA text. Conversely, an authored custom property can be absent from the runtime stage. Attribute presence is not evidence that the value was authored.

For application data, set a population description’s domains to PopulationDomain.RENDERING and provide explicit selectors as described below. The selectors add properties; they do not narrow the rendering domain’s selection.

A boolean populateAllAuthoredAttributes entry in the root or session layer’s customLayerData, set to true, enables an extra pass over authored attributes on prims already populated. The hosting application can also enable this behavior. The extra pass skips attributes already present or without values, xformOp* and primvars:* attributes, shader prims, instance proxies, and unsupported types, including asset arrays, string arrays, and time codes. Use selectors when you need to name exactly which additional properties to copy.

Selecting Prims and Properties#

Use ovstage.population.Desc for more control. It contains domains and a list of ovstage.population.Selector objects. The _with_desc functions accept several descriptions; they combine the domains with OR and append the selectors in order.

Each selector pairs a prim condition with a property condition. It selects which prims to populate and which of their properties to include. If several selectors match, their property selections are combined. An omitted property condition selects no properties. Selectors can also request prim and property metadata.

Build conditions with ovstage.population.PrimPredicate and ovstage.population.PropertyPredicate:

Unsupported kinds, cycles, or graphs exceeding the depth or node limits fail at submission with a diagnostic. In C, predicate nodes and string arrays are borrowed and must remain valid through the call. Python builders retain them.

For the remaining examples, select /World/Body and name the properties to populate. The description also requests metadata, explained below. Populate these values at ordinal 2, after the physics-domain load at ordinal 1:

# Select only /World/Body. is_under_path("/World") also includes its descendants.
body_condition = population.PrimPredicate.has_path("/World/Body")

# Request the values used below, plus metadata on those properties.
selector = population.Selector(
    prim_predicate=body_condition,
    property_predicate=population.PropertyPredicate.has_name(
        "size", "temperature", "state", "points", "emptyName", "emptyArray", "extent"
    ),
    prim_metadata_paths=["customData:label"],
    property_metadata_paths=["customData:unit"],
)

desc = population.Desc(
    domains=PopulationDomain.NONE,
    selectors=[selector],
    stage_metadata_paths=["timeCodesPerSecond"],
)
population.open_usd_with_desc(
    stage, scene, ordinal=2, time=population.DEFAULT_TIME, descs=[desc]
)
stage.advance_write_floor(ordinal=2).wait()
const ovx_string_t bodyPath[] = {literal_to_ovx_string("/World/Body")};
// Match this exact prim; is_under_path would select a subtree instead.
auto bodyCondition = ovstage_population_prim_predicate_has_path(bodyPath, 1);

const ovx_string_t primMetadata[] = {literal_to_ovx_string("customData:label")};
const ovx_string_t propertyMetadata[] = {literal_to_ovx_string("customData:unit")};
ovstage_population_selector_t selector{};
selector.prim_predicate = bodyCondition;
const ovx_string_t properties[] = {
    literal_to_ovx_string("size"), literal_to_ovx_string("temperature"), literal_to_ovx_string("state"),
    literal_to_ovx_string("points"), literal_to_ovx_string("emptyName"),
    literal_to_ovx_string("emptyArray"), literal_to_ovx_string("extent")};
selector.property_predicate = ovstage_population_property_predicate_has_name(properties, 7);
selector.prim_metadata_paths = primMetadata;
selector.prim_metadata_path_count = 1;
selector.property_metadata_paths = propertyMetadata;
selector.property_metadata_path_count = 1;

const ovx_string_t stageMetadata[] = {literal_to_ovx_string("timeCodesPerSecond")};
ovstage_population_desc_t desc{};
desc.domains = OVSTAGE_POPULATION_DOMAIN_NONE;
desc.selectors = &selector;
desc.selector_count = 1;
desc.stage_metadata_paths = stageMetadata;
desc.stage_metadata_path_count = 1;
// Population copies the description and the referenced arrays during submission.
waitPop(stage, ovstage_population_open_usd_from_file_with_desc(
    stage, scenePath, 2, OVSTAGE_POPULATION_DEFAULT_TIME, &desc, 1), "open with description");
watermark.ordinal = 2;
waitOp(stage, ovstage_advance_write_floor(stage, &watermark), "advance watermark");

See ovstage.population.PrimPredicateKind and ovstage.population.PropertyPredicateKind for the supported conditions.

Which Properties Become Runtime Attributes?#

Selecting a prim does not select all its properties. Domains choose the properties they need; explicit selectors add the properties their conditions match. Schema membership is not required for an explicit selection: the example above includes the custom temperature attribute by name.

For explicit selectors and the PHYSICS domain, population reads the selected properties through USD:

  • A selected attribute is populated if USD resolves a value and ovstage supports its value type. The value can come from an authored default, a time sample, or a registered schema’s default. It need not be authored in the scene file.

  • A schema-declared attribute with no resolved value is skipped. Merely declaring or selecting an attribute does not create a runtime value.

  • A selected relationship is populated only if it has authored targets. An explicitly authored empty target list is included as an empty array.

Schema defaults require the schema definitions to be registered with the USD runtime used by population. For additional schema families, call ovstage.population.register_usd_schemas() before any code in the process first reads USD schema definitions, including population, export, or another USD consumer. Registration makes those definitions available; it does not select their properties for population.

Domains can also produce derived data or supply domain-specific defaults. See Rendering Selection for the rendering domain’s selection and conversion rules.

The example scene declares /World/Body as a Cube but never authors size. The selector above explicitly requests both size and the custom temperature attribute. Reading them returns the Cube schema’s default size of 2 and the authored temperature of 20:

with paths.create_path_list_from_strings(["/World/Body"]) as selected:
    with stage.query_from_path_list(selected) as query:
        with stage.read_attributes(query, [paths.intern_token("size")], OrdinalRange.latest(2)) as read:
            read.wait()
            for group in read.groups():
                with group:
                    print(float(group.array(0).ravel()[0]))
                    # 2.0

with paths.create_path_list_from_strings(["/World/Body"]) as selected:
    with stage.query_from_path_list(selected) as query:
        with stage.read_attributes(query, [paths.intern_token("temperature")], OrdinalRange.latest(2)) as read:
            read.wait()
            for group in read.groups():
                with group:
                    print(float(group.array(0).ravel()[0]))
                    # 20.0
{
    const ovx_string_t selected[] = {literal_to_ovx_string("/World/Body")};
    ovx_primpath_list_t pathList{};
    checkOvx(dict, path_dictionary_create_path_list_from_strings(dict, selected, 1, &pathList), "paths");
    ovstage_query_handle_t query{};
    check(stage, ovstage_query_from_path_list(stage, pathList, &query), "query");
    const ovx_string_t name = literal_to_ovx_string("size");
    ovx_token_t attribute{};
    checkOvx(dict, path_dictionary_create_tokens_from_strings(dict, &name, 1, &attribute), "attribute");
    ovstage_ordinal_range_t range{};
    range.end_ordinal = 2;
    ovstage_read_handle_t read{};
    waitOp(stage, ovstage_read_attributes(stage, query, &attribute, 1, range, &read), "read");
    ovstage_read_group_t group{};
    check(stage, ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group), "fetch");
    const DLTensor& tensor = group.data.tensors[0];
    const auto* values = reinterpret_cast<const double*>(
        static_cast<const uint8_t*>(tensor.data) + tensor.byte_offset);
    std::printf("%.1f\n", static_cast<double>(values[0]));
    // 2.0
    // Release the borrowed data before releasing the read and query.
    ovstage_release_group(stage, &group);
    waitOp(stage, ovstage_release_read(stage, read), "release read");
    waitOp(stage, ovstage_release_query(stage, query), "release query");
    path_dictionary_release_path_list_reference(dict, pathList);
}

{
    const ovx_string_t selected[] = {literal_to_ovx_string("/World/Body")};
    ovx_primpath_list_t pathList{};
    checkOvx(dict, path_dictionary_create_path_list_from_strings(dict, selected, 1, &pathList), "paths");
    ovstage_query_handle_t query{};
    check(stage, ovstage_query_from_path_list(stage, pathList, &query), "query");
    const ovx_string_t name = literal_to_ovx_string("temperature");
    ovx_token_t attribute{};
    checkOvx(dict, path_dictionary_create_tokens_from_strings(dict, &name, 1, &attribute), "attribute");
    ovstage_ordinal_range_t range{};
    range.end_ordinal = 2;
    ovstage_read_handle_t read{};
    waitOp(stage, ovstage_read_attributes(stage, query, &attribute, 1, range, &read), "read");
    ovstage_read_group_t group{};
    check(stage, ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group), "fetch");
    const DLTensor& tensor = group.data.tensors[0];
    const auto* values = reinterpret_cast<const float*>(
        static_cast<const uint8_t*>(tensor.data) + tensor.byte_offset);
    std::printf("%.1f\n", static_cast<double>(values[0]));
    // 20.0
    // Release the borrowed data before releasing the read and query.
    ovstage_release_group(stage, &group);
    waitOp(stage, ovstage_release_read(stage, read), "release read");
    waitOp(stage, ovstage_release_query(stage, query), "release query");
    path_dictionary_release_path_list_reference(dict, pathList);
}

USD Value Types#

The USD value type determines the runtime type. For schema attributes, this is the type declared by the schema:

  1. Numeric values retain their scalar type and bit width. DLPack lanes record the component count.

  2. Arrays use the element’s dtype with is_array=True. Other values use one fixed-size value per prim.

  3. Strings and paths become 64-bit identifiers resolved through the Path Dictionary.

  4. Numeric values authored in a different type are converted to the declared type when possible; see Values Authored in Another Type Than Declared.

The tables describe storage types and canonical write dtypes. Returned reads and maps expand component widths into trailing dimensions with scalar lanes; see DLPack Tensor Exchange.

The tables use {code, bits, lanes} for ovstage.DLDataType and ovstage.AttributeSemantic names for semantics.

Implicit Conversions#

Population converts some USD values for the runtime stage:

  • Strings and paths: store identifiers from the Path Dictionary. Exceptions store text bytes; see Properties That Do Not Follow Their USD Type.

  • Assets: store identifiers for the authored path and the path USD resolved. Generic attribute population does not resolve the asset again; rendering material inputs have the UDIM exception described below.

  • Empty strings: use identifier 0 when stored as identifiers. See Empty Values.

  • USD roles: become attribute semantics. For example, point3f uses three floats with POINT semantic.

  • Schema-specific properties: can use a different type or semantic. See Properties That Do Not Follow Their USD Type.

  • Numeric type mismatches: convert to the schema’s declared type when possible. See Values Authored in Another Type Than Declared.

  • Transforms: combine USD transform operations into omni:xform. See Transforms.

Export uses the stored values, including these conversions.

Numeric scalars#

USD type

dtype

is_array

semantic

bool

{bool, 8, 1}

no

NONE

uchar

{uint, 8, 1}

no

NONE

int

{int, 32, 1}

no

NONE

int64

{int, 64, 1}

no

NONE

uint

{uint, 32, 1}

no

NONE

uint64

{uint, 64, 1}

no

NONE

half

{float, 16, 1}

no

NONE

float

{float, 32, 1}

no

NONE

double

{float, 64, 1}

no

NONE

half2, half3, half4

{float, 16, 2|3|4}

no

NONE

float2, float3, float4

{float, 32, 2|3|4}

no

NONE

double2, double3, double4

{float, 64, 2|3|4}

no

NONE

int2, int3, int4

{int, 32, 2|3|4}

no

NONE

quath, quatf, quatd

{float, 16|32|64, 4}

no

QUATERNION

matrix2d, matrix3d, matrix4d

{float, 64, 4|9|16}

no

MATRIX

timecode

{float, 64, 1}

no

TIME_CODE

USD roles change the semantic, not the storage type. For example, point3f uses {float, 32, 3} with POINT. The corresponding roles are NORMAL for normal3f, VECTOR for vector3f, COLOR for color3f, and TEXTURE_COORDINATE for texCoord2f and texCoord3f. frame4d uses the same storage as matrix4d but has semantic FRAME instead of MATRIX.

The scene authors float temperature = 20 on /World/Body. After selecting that property and populating at ordinal 2, read its value:

with paths.create_path_list_from_strings(["/World/Body"]) as selected:
    with stage.query_from_path_list(selected) as query:
        with stage.read_attributes(query, [paths.intern_token("temperature")], OrdinalRange.latest(2)) as read:
            read.wait()
            for group in read.groups():
                with group:
                    print(float(group.array(0).ravel()[0]))
                    # 20.0
{
    const ovx_string_t selected[] = {literal_to_ovx_string("/World/Body")};
    ovx_primpath_list_t pathList{};
    checkOvx(dict, path_dictionary_create_path_list_from_strings(dict, selected, 1, &pathList), "paths");
    ovstage_query_handle_t query{};
    check(stage, ovstage_query_from_path_list(stage, pathList, &query), "query");
    const ovx_string_t name = literal_to_ovx_string("temperature");
    ovx_token_t attribute{};
    checkOvx(dict, path_dictionary_create_tokens_from_strings(dict, &name, 1, &attribute), "attribute");
    ovstage_ordinal_range_t range{};
    range.end_ordinal = 2;
    ovstage_read_handle_t read{};
    waitOp(stage, ovstage_read_attributes(stage, query, &attribute, 1, range, &read), "read");
    ovstage_read_group_t group{};
    check(stage, ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group), "fetch");
    const DLTensor& tensor = group.data.tensors[0];
    const auto* values = reinterpret_cast<const float*>(
        static_cast<const uint8_t*>(tensor.data) + tensor.byte_offset);
    std::printf("%.1f\n", static_cast<double>(values[0]));
    // 20.0
    // Release the borrowed data before releasing the read and query.
    ovstage_release_group(stage, &group);
    waitOp(stage, ovstage_release_read(stage, read), "release read");
    waitOp(stage, ovstage_release_query(stage, query), "release query");
    path_dictionary_release_path_list_reference(dict, pathList);
}

String-like scalars#

USD type

dtype

is_array

semantic

token

{uint, 64, 1}

no

TOKEN_ID

string

{uint, 64, 1}

no

NONE

asset

{uint, 64, 2}

no

ASSET_PATH_ID

pathExpression

{uint, 64, 1}

no

PATH_EXPRESSION_STRING

An asset stores an (authored, resolved) pair of identifiers. The resolved value is whatever USD supplied to generic attribute population; that path does not resolve the asset again. Rendering material inputs containing <UDIM> are an exception: rendering derives a resolved pattern from a tile that resolves. If no tile resolves, the resolved half is empty, even if USD supplied an anchored pattern; the authored half is retained. A string has semantic NONE, so runtime semantic alone cannot distinguish it from uint64. Consult the USD type when that distinction matters.

The authored token state = "ready" becomes an identifier. Resolve it through the path dictionary while the read group is alive:

with paths.create_path_list_from_strings(["/World/Body"]) as selected:
    with stage.query_from_path_list(selected) as query:
        with stage.read_attributes(query, [paths.intern_token("state")], OrdinalRange.latest(2)) as read:
            read.wait()
            for group in read.groups():
                with group:
                    token = int(group.array(0).ravel()[0])
                    print(paths.token_to_string(token))
                    # ready
{
    const ovx_string_t selected[] = {literal_to_ovx_string("/World/Body")};
    ovx_primpath_list_t pathList{};
    checkOvx(dict, path_dictionary_create_path_list_from_strings(dict, selected, 1, &pathList), "paths");
    ovstage_query_handle_t query{};
    check(stage, ovstage_query_from_path_list(stage, pathList, &query), "query");
    const ovx_string_t name = literal_to_ovx_string("state");
    ovx_token_t attribute{};
    checkOvx(dict, path_dictionary_create_tokens_from_strings(dict, &name, 1, &attribute), "attribute");
    ovstage_ordinal_range_t range{};
    range.end_ordinal = 2;
    ovstage_read_handle_t read{};
    waitOp(stage, ovstage_read_attributes(stage, query, &attribute, 1, range, &read), "read");
    ovstage_read_group_t group{};
    check(stage, ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group), "fetch");
    const DLTensor& tensor = group.data.tensors[0];
    const auto* values = reinterpret_cast<const uint64_t*>(
        static_cast<const uint8_t*>(tensor.data) + tensor.byte_offset);
    ovx_token_t token = values[0];
    ovx_string_t text{};
    checkOvx(dict, path_dictionary_get_strings_from_tokens(dict, &token, 1, &text), "resolve token");
    std::printf("%.*s\n", static_cast<int>(text.length), text.ptr);
    // ready
    // Release the borrowed data before releasing the read and query.
    ovstage_release_group(stage, &group);
    waitOp(stage, ovstage_release_read(stage, read), "release read");
    waitOp(stage, ovstage_release_query(stage, query), "release query");
    path_dictionary_release_path_list_reference(dict, pathList);
}

Arrays#

A USD array populates with is_array=True and its element’s dtype and semantic: float3[] is {float, 32, 3}, and matrix4d[] is {float, 64, 16} with MATRIX.

token[], string[], and pathExpression[] store one identifier per element. asset[] stores one pair per element. Their tensor element counts therefore match the USD array lengths.

Relationship targets use path identifiers with {uint, 64, 1}, RELATIONSHIP_PATH_ID, and is_array=True. An authored relationship with no targets is present as an empty value, rather than absent.

The scene authors two points in point3f[] points. Read the array from the one-prim query and view its three components per point:

with paths.create_path_list_from_strings(["/World/Body"]) as selected:
    with stage.query_from_path_list(selected) as query:
        with stage.read_attributes(query, [paths.intern_token("points")], OrdinalRange.latest(2)) as read:
            read.wait()
            for group in read.groups():
                with group:
                    print(group.array(0).reshape(-1, 3).tolist())
                    # [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]
{
    const ovx_string_t selected[] = {literal_to_ovx_string("/World/Body")};
    ovx_primpath_list_t pathList{};
    checkOvx(dict, path_dictionary_create_path_list_from_strings(dict, selected, 1, &pathList), "paths");
    ovstage_query_handle_t query{};
    check(stage, ovstage_query_from_path_list(stage, pathList, &query), "query");
    const ovx_string_t name = literal_to_ovx_string("points");
    ovx_token_t attribute{};
    checkOvx(dict, path_dictionary_create_tokens_from_strings(dict, &name, 1, &attribute), "attribute");
    ovstage_ordinal_range_t range{};
    range.end_ordinal = 2;
    ovstage_read_handle_t read{};
    waitOp(stage, ovstage_read_attributes(stage, query, &attribute, 1, range, &read), "read");
    ovstage_read_group_t group{};
    check(stage, ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group), "fetch");
    const DLTensor& tensor = group.data.tensors[0];
    // The tensor has shape (2, 3): two points with scalar float32 lanes.
    std::printf("points: %lld, lanes: %u\n", static_cast<long long>(tensor.shape[0]), tensor.dtype.lanes);
    // points: 2, lanes: 1
    // Release the borrowed data before releasing the read and query.
    ovstage_release_group(stage, &group);
    waitOp(stage, ovstage_release_read(stage, read), "release read");
    waitOp(stage, ovstage_release_query(stage, query), "release query");
    path_dictionary_release_path_list_reference(dict, pathList);
}

Empty Values#

Where the interned representation applies, empty tokens, strings, path expressions, and either half of an asset pair use identifier 0. A text-backed exception such as UsdRenderVar.sourceName uses a zero-length STRING value. Empty array elements keep their position. An authored empty value is distinct from an absent property.

opaque has no value to store and is skipped without a diagnostic.

The authored empty token emptyName is present with identifier 0:

with paths.create_path_list_from_strings(["/World/Body"]) as selected:
    with stage.query_from_path_list(selected) as query:
        with stage.read_attributes(query, [paths.intern_token("emptyName")], OrdinalRange.latest(2)) as read:
            read.wait()
            for group in read.groups():
                with group:
                    print(int(group.array(0).ravel()[0]))
                    # 0
{
    const ovx_string_t selected[] = {literal_to_ovx_string("/World/Body")};
    ovx_primpath_list_t pathList{};
    checkOvx(dict, path_dictionary_create_path_list_from_strings(dict, selected, 1, &pathList), "paths");
    ovstage_query_handle_t query{};
    check(stage, ovstage_query_from_path_list(stage, pathList, &query), "query");
    const ovx_string_t name = literal_to_ovx_string("emptyName");
    ovx_token_t attribute{};
    checkOvx(dict, path_dictionary_create_tokens_from_strings(dict, &name, 1, &attribute), "attribute");
    ovstage_ordinal_range_t range{};
    range.end_ordinal = 2;
    ovstage_read_handle_t read{};
    waitOp(stage, ovstage_read_attributes(stage, query, &attribute, 1, range, &read), "read");
    ovstage_read_group_t group{};
    check(stage, ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group), "fetch");
    const DLTensor& tensor = group.data.tensors[0];
    const auto* values = reinterpret_cast<const uint64_t*>(
        static_cast<const uint8_t*>(tensor.data) + tensor.byte_offset);
    std::printf("%llu\n", static_cast<unsigned long long>(values[0]));
    // 0
    // Release the borrowed data before releasing the read and query.
    ovstage_release_group(stage, &group);
    waitOp(stage, ovstage_release_read(stage, read), "release read");
    waitOp(stage, ovstage_release_query(stage, query), "release query");
    path_dictionary_release_path_list_reference(dict, pathList);
}

An authored float[] emptyArray = [] also produces a read group. Its tensor has zero elements; an absent property would produce no group:

with paths.create_path_list_from_strings(["/World/Body"]) as selected:
    with stage.query_from_path_list(selected) as query:
        with stage.read_attributes(query, [paths.intern_token("emptyArray")], OrdinalRange.latest(2)) as read:
            read.wait()
            for group in read.groups():
                with group:
                    print(group.array(0).tolist())
                    # []
{
    const ovx_string_t selected[] = {literal_to_ovx_string("/World/Body")};
    ovx_primpath_list_t pathList{};
    checkOvx(dict, path_dictionary_create_path_list_from_strings(dict, selected, 1, &pathList), "paths");
    ovstage_query_handle_t query{};
    check(stage, ovstage_query_from_path_list(stage, pathList, &query), "query");
    const ovx_string_t name = literal_to_ovx_string("emptyArray");
    ovx_token_t attribute{};
    checkOvx(dict, path_dictionary_create_tokens_from_strings(dict, &name, 1, &attribute), "attribute");
    ovstage_ordinal_range_t range{};
    range.end_ordinal = 2;
    ovstage_read_handle_t read{};
    waitOp(stage, ovstage_read_attributes(stage, query, &attribute, 1, range, &read), "read");
    ovstage_read_group_t group{};
    check(stage, ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group), "fetch");
    const DLTensor& tensor = group.data.tensors[0];
    // The group is present even though its array has no elements.
    std::printf("elements: %lld\n", static_cast<long long>(tensor.shape[0]));
    // elements: 0
    // Release the borrowed data before releasing the read and query.
    ovstage_release_group(stage, &group);
    waitOp(stage, ovstage_release_read(stage, read), "release read");
    waitOp(stage, ovstage_release_query(stage, query), "release query");
    path_dictionary_release_path_list_reference(dict, pathList);
}

Properties That Do Not Follow Their USD Type#

The following schema properties use fixed runtime types so that population domains agree when sharing a stage:

Property (where it applies)

USD type

populated as

instead of

extent (any UsdGeomImageable)

float3[], two points

{float, 64, 6}, is_array=False, NONE

{float, 32, 3} array

normals (UsdGeomCurves, UsdGeomPoints)

normal3f[]

POINT

NORMAL

inputs:color (UsdLuxLightAPI), inputs:shadow:color (UsdLuxShadowAPI); inputs:shaping:focusTint (UsdLuxShapingAPI on a DistantLight, DiskLight, RectLight, SphereLight, CylinderLight or DomeLight)

color3f

VECTOR

COLOR

Scalar string attributes on UsdRenderSettings, UsdRenderProduct, or UsdRenderVar, including sourceName and schema defaults. Excludes xformOp* and primvars:*.

string

{uint, 8, 1}, is_array = true, STRING

interned {uint, 64, 1}

On UsdGeomCamera: sensorModelPluginName, sensorModelConfig, sensorModelSignals, and crossCameraReferenceName. Also includes string attributes declared by applied OmniSensor* schemas on cameras or OmniSensor-family prims, and by applied OmniLensDistortion* or PhysicalCameraResponsivityAPI schemas on cameras.

string

{uint, 8, 1}, is_array = true, STRING

interned {uint, 64, 1}

Scalar string attributes matching inputs:* or omni:simready:nonvisual:* on UsdShadeMaterial, UsdShadeNodeGraph, or UsdShadeShader. Also config:* on UsdShadeMaterial and info:*sourceCode on UsdShadeShader.

string

{uint, 8, 1}, is_array = true, STRING

interned {uint, 64, 1}

Scalar string primvars with a value, on any prim

string

{uint, 8, 1}, is_array = true, STRING

interned {uint, 64, 1}

semantic:<instance>:params:semanticType, semantic:<instance>:params:semanticData (applied SemanticsAPI:<instance>)

string

{uint, 64, 1}, TOKEN_ID

{uint, 64, 1}, NONE

omni:rtx:domeLight:size (OmniFiniteDomeLightAPI); nerf:crop:minBounds, nerf:crop:maxBounds, nerf:offset (OmniNerfVolumeAPI); omni:nurec:crop:minBounds, omni:nurec:crop:maxBounds, omni:nurec:offset (OmniNuRecVolumeAPI)

float3

VECTOR

NONE

extent and text stored as bytes change dtype or shape. The other exceptions change only the semantic. Each rule applies only to the schemas or prim types listed above. string[] attributes and primvars still store identifiers.

These schema-specific rules do not apply to arbitrary properties with the same name. When rendering populates a non-schema color3f, point3f, normal3f, vector3f, or texCoord3f property, its authored role is preserved through value edits. This does not depend on the property’s custom flag. Schema properties retain their required rendering representation; for example, a light’s inputs:color still uses VECTOR even if authored with custom = true.

If a value cannot use the required representation (for example, an authored token where a rule requires string bytes), population skips the value and logs a warning. The attribute keeps its required type and its last written value; this conversion mismatch does not fail the population operation.

Values Authored in Another Type Than Declared#

When an authored numeric value differs from its schema’s declared type, population tries to convert it to that type. Attributes without a schema declaration use their authored type, subject to the representation rules above.

Conversion requires matching shapes: scalar to scalar, array to array, or vectors with the same number of components. Supported types are:

  • Scalars and their arrays: bool, uchar, int, uint, int64, uint64, half, float, and double.

  • Vectors and their arrays: 2, 3, or 4 components of int, half, float, or double, including role types such as point3f and color3f.

Each component converts using USD’s numeric conversion rules:

Authored as, declared as

Result

integer or bool, integer or bool

the same value if it is in the declared type’s range (0 or 1 for bool); otherwise no conversion

floating point, integer or bool

the value truncated toward zero if that is in the declared type’s range; otherwise, and for an infinity or NaN, no conversion

any, floating point

the value rounded to the declared type. A value beyond the declared type’s range becomes an infinity, and a nonzero value too small for it becomes zero. Infinities and NaN convert unchanged.

Every array element and vector component must convert successfully. If conversion fails, population skips the whole value and logs a warning; the attribute keeps its declared type and its last written value. Shape mismatches and nonnumeric type mismatches involving token, string, or asset follow the same rule. The mismatch does not fail population, so consumers must not assume that a successful population operation updated every value.

This differs from re-authoring the property’s type: population replaces a column when its declared type or role changes, using FORCE attribute replacement. An unconvertible value does not trigger that replacement.

Read extent to see the fixed-size representation of its two bounds:

with paths.create_path_list_from_strings(["/World/Body"]) as selected:
    with stage.query_from_path_list(selected) as query:
        with stage.read_attributes(query, [paths.intern_token("extent")], OrdinalRange.latest(2)) as read:
            read.wait()
            for group in read.groups():
                with group:
                    print(group.array(0).dtype, group.array(0).reshape(-1, 6).tolist())
                    # float64 [[-1.0, -1.0, -1.0, 1.0, 1.0, 1.0]]
{
    const ovx_string_t selected[] = {literal_to_ovx_string("/World/Body")};
    ovx_primpath_list_t pathList{};
    checkOvx(dict, path_dictionary_create_path_list_from_strings(dict, selected, 1, &pathList), "paths");
    ovstage_query_handle_t query{};
    check(stage, ovstage_query_from_path_list(stage, pathList, &query), "query");
    const ovx_string_t name = literal_to_ovx_string("extent");
    ovx_token_t attribute{};
    checkOvx(dict, path_dictionary_create_tokens_from_strings(dict, &name, 1, &attribute), "attribute");
    ovstage_ordinal_range_t range{};
    range.end_ordinal = 2;
    ovstage_read_handle_t read{};
    waitOp(stage, ovstage_read_attributes(stage, query, &attribute, 1, range, &read), "read");
    ovstage_read_group_t group{};
    check(stage, ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group), "fetch");
    const DLTensor& tensor = group.data.tensors[0];
    // extent is one fixed-size value containing six float64 components.
    std::printf("bits: %u, lanes: %u, array: %d\n", tensor.dtype.bits, tensor.dtype.lanes, group.is_array);
    // bits: 64, lanes: 1, array: 0
    // Release the borrowed data before releasing the read and query.
    ovstage_release_group(stage, &group);
    waitOp(stage, ovstage_release_read(stage, read), "release read");
    waitOp(stage, ovstage_release_query(stage, query), "release query");
    path_dictionary_release_path_list_reference(dict, pathList);
}

Stage Metadata#

Population writes requested stage metadata to / as attributes named usd-metadata:<path>. Domains may request fields; descriptions request them through stage_metadata_paths. Unrequested fields are not populated.

A metadata path names a field and optional dictionary keys separated by ::

  • timeCodesPerSecond selects one field.

  • customLayerData:myTool:version selects a nested value.

  • customLayerData selects all values in that dictionary, each stored under its own full path.

Missing paths produce no value. Leading, trailing, or repeated : characters create empty components and are rejected.

The description above requests timeCodesPerSecond. Read the field from the root path. This scene uses 24 time codes per second:

with paths.create_path_list_from_strings(["/"]) as selected:
    with stage.query_from_path_list(selected) as query:
        with stage.read_attributes(query, [paths.intern_token("usd-metadata:timeCodesPerSecond")], OrdinalRange.latest(2)) as read:
            read.wait()
            for group in read.groups():
                with group:
                    print(float(group.array(0).ravel()[0]))
                    # 24.0
{
    const ovx_string_t selected[] = {literal_to_ovx_string("/")};
    ovx_primpath_list_t pathList{};
    checkOvx(dict, path_dictionary_create_path_list_from_strings(dict, selected, 1, &pathList), "paths");
    ovstage_query_handle_t query{};
    check(stage, ovstage_query_from_path_list(stage, pathList, &query), "query");
    const ovx_string_t name = literal_to_ovx_string("usd-metadata:timeCodesPerSecond");
    ovx_token_t attribute{};
    checkOvx(dict, path_dictionary_create_tokens_from_strings(dict, &name, 1, &attribute), "attribute");
    ovstage_ordinal_range_t range{};
    range.end_ordinal = 2;
    ovstage_read_handle_t read{};
    waitOp(stage, ovstage_read_attributes(stage, query, &attribute, 1, range, &read), "read");
    ovstage_read_group_t group{};
    check(stage, ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group), "fetch");
    const DLTensor& tensor = group.data.tensors[0];
    const auto* values = reinterpret_cast<const double*>(
        static_cast<const uint8_t*>(tensor.data) + tensor.byte_offset);
    std::printf("%.1f\n", values[0]);
    // 24.0
    // Release the borrowed data before releasing the read and query.
    ovstage_release_group(stage, &group);
    waitOp(stage, ovstage_release_read(stage, read), "release read");
    waitOp(stage, ovstage_release_query(stage, query), "release query");
    path_dictionary_release_path_list_reference(dict, pathList);
}

Prim and Property Metadata#

Selectors use prim_metadata_paths and property_metadata_paths to request metadata for selected prims and properties. They use the same path syntax:

  • Prim metadata becomes usd-metadata:<path>.

  • Property metadata becomes <property>:usd-metadata:<path>.

has_metadata() matches the presence of any requested path, including a value of False. It can be used in prim or property conditions. A dictionary field matches when any value beneath it resolves, including schema-provided metadata. Name a specific key, such as customData:myTool:export, when that is what you need to select.

The selector above requests the prim label and each selected property’s customData:unit. Read the populated temperature unit:

with paths.create_path_list_from_strings(["/World/Body"]) as selected:
    with stage.query_from_path_list(selected) as query:
        with stage.read_attributes(query, [paths.intern_token("temperature:usd-metadata:customData:unit")], OrdinalRange.latest(2)) as read:
            read.wait()
            for group in read.groups():
                with group:
                    print(paths.token_to_string(int(group.array(0).ravel()[0])))
                    # C
{
    const ovx_string_t selected[] = {literal_to_ovx_string("/World/Body")};
    ovx_primpath_list_t pathList{};
    checkOvx(dict, path_dictionary_create_path_list_from_strings(dict, selected, 1, &pathList), "paths");
    ovstage_query_handle_t query{};
    check(stage, ovstage_query_from_path_list(stage, pathList, &query), "query");
    const ovx_string_t name = literal_to_ovx_string("temperature:usd-metadata:customData:unit");
    ovx_token_t attribute{};
    checkOvx(dict, path_dictionary_create_tokens_from_strings(dict, &name, 1, &attribute), "attribute");
    ovstage_ordinal_range_t range{};
    range.end_ordinal = 2;
    ovstage_read_handle_t read{};
    waitOp(stage, ovstage_read_attributes(stage, query, &attribute, 1, range, &read), "read");
    ovstage_read_group_t group{};
    check(stage, ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group), "fetch");
    const DLTensor& tensor = group.data.tensors[0];
    const auto* values = reinterpret_cast<const uint64_t*>(
        static_cast<const uint8_t*>(tensor.data) + tensor.byte_offset);
    ovx_token_t token = values[0];
    ovx_string_t text{};
    checkOvx(dict, path_dictionary_get_strings_from_tokens(dict, &token, 1, &text), "resolve metadata");
    std::printf("%.*s\n", static_cast<int>(text.length), text.ptr);
    // C
    // Release the borrowed data before releasing the read and query.
    ovstage_release_group(stage, &group);
    waitOp(stage, ovstage_release_read(stage, read), "release read");
    waitOp(stage, ovstage_release_query(stage, query), "release query");
    path_dictionary_release_path_list_reference(dict, pathList);
}

The same selector requested customData:label on the prim itself. Read that value without the property-name prefix:

with paths.create_path_list_from_strings(["/World/Body"]) as selected:
    with stage.query_from_path_list(selected) as query:
        with stage.read_attributes(query, [paths.intern_token("usd-metadata:customData:label")], OrdinalRange.latest(2)) as read:
            read.wait()
            for group in read.groups():
                with group:
                    print(paths.token_to_string(int(group.array(0).ravel()[0])))
                    # robot
{
    const ovx_string_t selected[] = {literal_to_ovx_string("/World/Body")};
    ovx_primpath_list_t pathList{};
    checkOvx(dict, path_dictionary_create_path_list_from_strings(dict, selected, 1, &pathList), "paths");
    ovstage_query_handle_t query{};
    check(stage, ovstage_query_from_path_list(stage, pathList, &query), "query");
    const ovx_string_t name = literal_to_ovx_string("usd-metadata:customData:label");
    ovx_token_t attribute{};
    checkOvx(dict, path_dictionary_create_tokens_from_strings(dict, &name, 1, &attribute), "attribute");
    ovstage_ordinal_range_t range{};
    range.end_ordinal = 2;
    ovstage_read_handle_t read{};
    waitOp(stage, ovstage_read_attributes(stage, query, &attribute, 1, range, &read), "read");
    ovstage_read_group_t group{};
    check(stage, ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group), "fetch");
    const DLTensor& tensor = group.data.tensors[0];
    const auto* values = reinterpret_cast<const uint64_t*>(
        static_cast<const uint8_t*>(tensor.data) + tensor.byte_offset);
    ovx_token_t token = values[0];
    ovx_string_t text{};
    checkOvx(dict, path_dictionary_get_strings_from_tokens(dict, &token, 1, &text), "resolve metadata");
    std::printf("%.*s\n", static_cast<int>(text.length), text.ptr);
    // robot
    // Release the borrowed data before releasing the read and query.
    ovstage_release_group(stage, &group);
    waitOp(stage, ovstage_release_read(stage, read), "release read");
    waitOp(stage, ovstage_release_query(stage, query), "release query");
    path_dictionary_release_path_list_reference(dict, pathList);
}

Reserved Prim Attributes#

An ordinary populated prim with a USD type name carries usd-prim-type. A typeless prim is still populated but has no usd-prim-type; there is no synthetic type name. These metadata rules also apply when a selector requests no properties.

Outside RENDERING, usd-schemas contains the applied API names, or an empty set when none apply. HAS therefore matches even an empty set; use CONTAINS to select a particular API. RENDERING also adds the schemas and tags described below, including on geometry without authored APIs; it can omit usd-schemas when neither applied APIs nor rendering tags are present.

A multiple-apply schema records both its instance name and bare name. For example, CollectionAPI:parts also adds CollectionAPI. Typed export skips that accompanying bare name rather than treating it as an additional API instance.

For ordinary prims populated through selectors or PHYSICS, these names come from USD’s type token and composed applied-API list. Use those identifiers, rather than C++ or Python class names:

USD declaration

Runtime name

def Cube "Body"

usd-prim-type contains Cube.

apiSchemas = ["PhysicsRigidBodyAPI"]

usd-schemas includes PhysicsRigidBodyAPI.

apiSchemas = ["CollectionAPI:parts"]

usd-schemas includes CollectionAPI:parts and CollectionAPI.

For example, use Cube, not UsdGeomCube or UsdGeom.Cube, as the type name in a population predicate. The applied-API list is composed by USD, so it can include APIs supplied by schema definitions as well as those written in an apiSchemas statement. Do not rely on its runtime array order.

usd-ancestor-types lists the USD types the prim derives from, excluding its own type. These are schema ancestors, not parent prims: a Mesh includes PointBased, Gprim, Boundable, Xformable, Imageable, Typed, SchemaBase, and TfType::_Root. Use CONTAINS to select prims by a base type. Typeless and unknown types have no ancestor set in non-rendering population, even if fallbackPrimTypes provides a fallback. Live type edits update or remove the set with the prim type.

Outside authored PointInstancer descendants, instance roots use the following metadata in every population domain, with the RENDERING-only exception below:

  • usd-prim-type is XformInstance, or ScopeInstance when it is a Scope, whatever its authored type.

  • usd-schemas also carries GeomInstanceAPI, and GeomInstanceRootAPI when the instance root is outside a prototype. No USD schema applies these names.

  • usd-ancestor-types is Xformable alone.

With RENDERING alone, a Scope and a non-Scope instance sharing a prototype may both receive the instance type selected from the first instance encountered. PHYSICS and selector population classify each root separately, including when combined with RENDERING. Consumers must therefore not assume identical usd-prim-type values across these modes for such a shared prototype.

When PHYSICS or a selector populates an instanceable prim authored beneath a PointInstancer, it keeps its authored type and applied schemas, including alongside RENDERING. At that authored path it receives neither instance tag nor the rendering-derived tags listed below. It still links its prototype and remains discoverable through the Instancing Queries queries. Without RENDERING, it also receives its normal USD ancestor set. With RENDERING selected, population does not synthesize usd-ancestor-types at these authored paths; use their type or APIs rather than relying on an ancestor set. Rendering uses relocated prototype geometry, whose metadata is a separate representation.

An instance root’s authored properties are populated as for any other prim. A root populated by PHYSICS or a selector also carries its composed authored type as usd-metadata:typeName, even when the root itself matches no selector.

Every prim the PHYSICS domain populates also carries usd-metadata:typeName: its composed USD type name, as authored, including a type no loaded schema declares. This includes structural ancestors populated to reach physics content. On an instance root it is the authored type, not XformInstance or ScopeInstance. Live edits update it, and a prim that loses its type name loses the attribute. A prim with no type name carries none. A desc selector publishes the same attribute when it selects the typeName metadata path.

Wherever the PHYSICS domain or a selector publishes a PointInstancer’s prototypes relationship, population also publishes its authored targets, in authored order, as prototypes:usd-targets: the list protoIndices indexes, whichever domains are selected. Live edits update it, including an edit that clears the targets.

When the RENDERING domain is selected, the following hold for every prim it populates, including prims a selector or the PHYSICS domain also populates:

  • A PointInstancer’s prototypes lists the geometry RENDERING renders for its prototypes, at paths beneath the instancer, not the authored relationship’s targets. protoIndices does not index that list. The authored targets are in prototypes:usd-targets when PHYSICS or a selector also publishes the instancer’s prototypes. Its extent is the bounds of those prototypes, not the authored extent.

  • usd-schemas includes MaterialBindingAPI on supported geometry and DomeLight, even without an authored material binding. This is a recognized USD API and typed export applies it. Instance roots are excluded; material-binding subsets are included only under a non-instance Mesh.

  • usd-schemas also carries rendering tags that are not applied USD APIs:

    • PrimvarsAPI on a prim with a primvar that has a value, other than a string array, or with an authored normals attribute.

    • MeshSubdivision on a Mesh whose subdivision scheme, boundary or face-varying interpolation, or triangle subdivision rule differs from its default, or that has creases or corners, and on every TetMesh.

    • SkelBindingAPI_Geom on a Mesh outside a prototype, or on curves, with a primvars:skel:jointIndices attribute.

    • SkelBindingAPI_Skel on a non-instance Skeleton.

    • GeomPrototypeAPI on supported geometry inside a USD prototype, excluding the prototype root and instance roots.

    • Light-attribute tags such as LightVisibleInPrimaryRayAPI, LightGhostFactorAPI, LightDisableFogInteractionAPI, LightIsProjectorAPI, LightProjectorDirectionTextureAPI, LightEnableCausticsAPI, LightUsdLuxVersionAPI, DomeLightFiniteAPI, and LightShapingForwardAxisAPI, when their rendering attributes apply.

These tags are preserved when RENDERING is combined with selectors or PHYSICS. They are not added at authored paths beneath a PointInstancer.

Rendering population supplies the ancestor set when selected, including alongside selectors, except at the authored PointInstancer descendant paths described above. Its other current exceptions are:

  • Typeless prims carry Imageable.

  • Instance proxies carry only Xformable (instance-root rules are above).

  • GeomSubset and USD prototype prims carry no ancestor types.

  • Unknown types resolved to geometry through fallbackPrimTypes carry that fallback’s ancestors. With rendering alone the prim type is also the fallback; a selector can retain the source type name instead.

  • LODSelector, which has no USD schema, carries Imageable.

Writing usd-prim-type does not derive a new ancestor set. If the new type already appears in usd-ancestor-types, it is removed from that set because a prim’s own type is excluded. When changing a type directly, also write its matching ancestor set. Both this set and usd-schemas are replaced as a whole by a write. Typed export does not author usd-ancestor-types. Custom properties named after USD types or API schemas, such as Boundable on a Mesh, are unsupported.

USD scene-graph instance roots use XformInstance or ScopeInstance as described above. With RENDERING population (including ALL), their populated contents can also use types such as MeshInstance and CubeInstance: a Mesh beneath an instance root can become MeshInstance. You author ordinary USD prims with references and instanceable = true; population generates these runtime types. You do not author MeshInstance or XformInstance in USD.

A PointInstancer does not generate one such prim per point. Instanceable prims authored beneath it keep their authored metadata as described above; rendering’s relocated prototype geometry is a separate representation. See Instancing Queries for scene-graph instancing examples and queries.

Consequently, a runtime usd-prim-type value can differ from the source USD type. Population predicates inspect the source USD prim; runtime queries inspect the populated stage.

Read the populated type of /World/Body and resolve its token:

with paths.create_path_list_from_strings(["/World/Body"]) as selected:
    with stage.query_from_path_list(selected) as query:
        with stage.read_attributes(query, [paths.intern_token("usd-prim-type")], OrdinalRange.latest(2)) as read:
            read.wait()
            for group in read.groups():
                with group:
                    print(paths.token_to_string(int(group.array(0).ravel()[0])))
                    # Cube
{
    const ovx_string_t selected[] = {literal_to_ovx_string("/World/Body")};
    ovx_primpath_list_t pathList{};
    checkOvx(dict, path_dictionary_create_path_list_from_strings(dict, selected, 1, &pathList), "paths");
    ovstage_query_handle_t query{};
    check(stage, ovstage_query_from_path_list(stage, pathList, &query), "query");
    const ovx_string_t name = literal_to_ovx_string("usd-prim-type");
    ovx_token_t attribute{};
    checkOvx(dict, path_dictionary_create_tokens_from_strings(dict, &name, 1, &attribute), "attribute");
    ovstage_ordinal_range_t range{};
    range.end_ordinal = 2;
    ovstage_read_handle_t read{};
    waitOp(stage, ovstage_read_attributes(stage, query, &attribute, 1, range, &read), "read");
    ovstage_read_group_t group{};
    check(stage, ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group), "fetch");
    const DLTensor& tensor = group.data.tensors[0];
    const auto* values = reinterpret_cast<const uint64_t*>(
        static_cast<const uint8_t*>(tensor.data) + tensor.byte_offset);
    ovx_token_t token = values[0];
    ovx_string_t text{};
    checkOvx(dict, path_dictionary_get_strings_from_tokens(dict, &token, 1, &text), "resolve type");
    std::printf("%.*s\n", static_cast<int>(text.length), text.ptr);
    // Cube
    // Release the borrowed data before releasing the read and query.
    ovstage_release_group(stage, &group);
    waitOp(stage, ovstage_release_read(stage, read), "release read");
    waitOp(stage, ovstage_release_query(stage, query), "release query");
    path_dictionary_release_path_list_reference(dict, pathList);
}

Read usd-schemas to find the applied APIs. This scene applies PhysicsRigidBodyAPI to /World/Body:

with paths.create_path_list_from_strings(["/World/Body"]) as selected:
    with stage.query_from_path_list(selected) as query:
        with stage.read_attributes(query, [paths.intern_token("usd-schemas")], OrdinalRange.latest(2)) as read:
            read.wait()
            for group in read.groups():
                with group:
                    print([paths.token_to_string(int(token)) for token in group.array(0).ravel()])
                    # ['PhysicsRigidBodyAPI']
{
    const ovx_string_t selected[] = {literal_to_ovx_string("/World/Body")};
    ovx_primpath_list_t pathList{};
    checkOvx(dict, path_dictionary_create_path_list_from_strings(dict, selected, 1, &pathList), "paths");
    ovstage_query_handle_t query{};
    check(stage, ovstage_query_from_path_list(stage, pathList, &query), "query");
    const ovx_string_t name = literal_to_ovx_string("usd-schemas");
    ovx_token_t attribute{};
    checkOvx(dict, path_dictionary_create_tokens_from_strings(dict, &name, 1, &attribute), "attribute");
    ovstage_ordinal_range_t range{};
    range.end_ordinal = 2;
    ovstage_read_handle_t read{};
    waitOp(stage, ovstage_read_attributes(stage, query, &attribute, 1, range, &read), "read");
    ovstage_read_group_t group{};
    check(stage, ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group), "fetch");
    const DLTensor& tensor = group.data.tensors[0];
    const auto* values = reinterpret_cast<const uint64_t*>(
        static_cast<const uint8_t*>(tensor.data) + tensor.byte_offset);
    // One token per applied API schema, stored in a USD array.
    for (int64_t i = 0; i < tensor.shape[0]; ++i)
    {
        ovx_token_t token = values[i];
        ovx_string_t text{};
        checkOvx(dict, path_dictionary_get_strings_from_tokens(dict, &token, 1, &text), "resolve type");
        std::printf("%.*s\n", static_cast<int>(text.length), text.ptr);
        // PhysicsRigidBodyAPI
    }
    // Release the borrowed data before releasing the read and query.
    ovstage_release_group(stage, &group);
    waitOp(stage, ovstage_release_read(stage, read), "release read");
    waitOp(stage, ovstage_release_query(stage, query), "release query");
    path_dictionary_release_path_list_reference(dict, pathList);
}

Runtime-Owned Prims#

Rendering population keeps bookkeeping prims alongside the scene. They are not USD content and remain after reset_usd() or removal of a reference. Unfiltered queries count them too. Other population domains do not create these prims.

To enumerate scene content, filter by its root path or expected types; see Queries. Their paths, types, and count are implementation details. Do not write to them.

Path Conditions#

has_path() and is_under_path() accept absolute prim paths, including /. Property, target, and variant-selection paths fail at submission. Neither condition matches prims inside a native instance.

For an exact match, use has_path() as the selector’s prim condition:

# Select only /World/Body. is_under_path("/World") also includes its descendants.
body_condition = population.PrimPredicate.has_path("/World/Body")
const ovx_string_t bodyPath[] = {literal_to_ovx_string("/World/Body")};
// Match this exact prim; is_under_path would select a subtree instead.
auto bodyCondition = ovstage_population_prim_predicate_has_path(bodyPath, 1);

To select Cube prims below /World, combine a subtree condition with a type condition. This example also includes the PHYSICS domain: the selector adds size to the domain’s selection. It runs at ordinal 7, after the time-update examples below have used ordinals 3 through 6:

# Add size for Cube prims below /World to the predefined physics selection.
subtree_selector = population.Selector(
    prim_predicate=population.PrimPredicate.and_(
        population.PrimPredicate.is_under_path("/World"),
        population.PrimPredicate.has_type("Cube"),
    ),
    property_predicate=population.PropertyPredicate.has_name("size"),
)
population.open_usd_with_desc(
    stage, scene, ordinal=7, time=population.DEFAULT_TIME,
    descs=[population.Desc(domains=PopulationDomain.PHYSICS, selectors=[subtree_selector])],
)
stage.advance_write_floor(ordinal=7).wait()
// Add size for Cube prims below /World to the predefined physics selection.
const ovx_string_t roots[] = {literal_to_ovx_string("/World")};
const ovx_string_t types[] = {literal_to_ovx_string("Cube")};
const ovx_string_t subtreeProperties[] = {literal_to_ovx_string("size")};
const ovstage_population_prim_predicate_t conditions[] = {
    ovstage_population_prim_predicate_is_under_path(roots, 1),
    ovstage_population_prim_predicate_has_type(types, 1)};
ovstage_population_selector_t subtreeSelector{};
subtreeSelector.prim_predicate = ovstage_population_prim_predicate_and(conditions, 2);
subtreeSelector.property_predicate = ovstage_population_property_predicate_has_name(subtreeProperties, 1);
ovstage_population_desc_t subtreeDesc{};
subtreeDesc.domains = OVSTAGE_POPULATION_DOMAIN_PHYSICS;
subtreeDesc.selectors = &subtreeSelector;
subtreeDesc.selector_count = 1;
waitPop(stage, ovstage_population_open_usd_from_file_with_desc(
    stage, scenePath, 7, OVSTAGE_POPULATION_DEFAULT_TIME, &subtreeDesc, 1), "populate subtree");
watermark.ordinal = 7;
waitOp(stage, ovstage_advance_write_floor(stage, &watermark), "advance watermark");

Ancestors and Transforms#

The PHYSICS domain and explicit selectors also populate ancestors of selected prims so consumers can follow the hierarchy. An ancestor that no selector matches gets the reserved attributes above and, if USD-xformable, transform data. It does not receive other attributes, relationships, or metadata, except usd-metadata:typeName on ancestors populated by PHYSICS and on instance roots populated by PHYSICS or a selector.

For each USD-xformable prim selected this way, including ancestors, population writes:

  • omni:xform — composed local USD transform, stored as a row-major 4×4 float64 matrix with 16 lanes and semantic MATRIX.

  • omni:resetXformStack — a boolean indicating whether the prim ignores its parent’s transform.

When RENDERING is not selected, population also creates omni:fabric:worldMatrix if absent, initially using the local matrix. Later world-transform values come from ovstage.Stage.compute_hierarchy(); population does not overwrite them. A prim that stops being xformable loses this attribute. When RENDERING is selected, that domain manages it instead.

Treat the provisionally named world-matrix attribute as read-only output. Hierarchy computation updates existing rows but does not create them. Non-xformable prims receive none of these derived attributes, and being xformable does not by itself select a prim for population. See Transforms.

These names belong to population. A USD property named omni:xform, omni:resetXformStack, or omni:fabric:worldMatrix is never published, even when a selector names or matches it, on any prim.

Selecting /World/Body also populates the transform of its ancestor /World. Its authored translation is 10 along X:

with paths.create_path_list_from_strings(["/World"]) as selected:
    with stage.query_from_path_list(selected) as query:
        with stage.read_attributes(query, [paths.intern_token("omni:xform")], OrdinalRange.latest(2)) as read:
            read.wait()
            for group in read.groups():
                with group:
                    print(group.array(0).reshape(4, 4)[3, :3].tolist())
                    # [10.0, 0.0, 0.0]
{
    const ovx_string_t selected[] = {literal_to_ovx_string("/World")};
    ovx_primpath_list_t pathList{};
    checkOvx(dict, path_dictionary_create_path_list_from_strings(dict, selected, 1, &pathList), "paths");
    ovstage_query_handle_t query{};
    check(stage, ovstage_query_from_path_list(stage, pathList, &query), "query");
    const ovx_string_t name = literal_to_ovx_string("omni:xform");
    ovx_token_t attribute{};
    checkOvx(dict, path_dictionary_create_tokens_from_strings(dict, &name, 1, &attribute), "attribute");
    ovstage_ordinal_range_t range{};
    range.end_ordinal = 2;
    ovstage_read_handle_t read{};
    waitOp(stage, ovstage_read_attributes(stage, query, &attribute, 1, range, &read), "read");
    ovstage_read_group_t group{};
    check(stage, ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group), "fetch");
    const DLTensor& tensor = group.data.tensors[0];
    const auto* values = reinterpret_cast<const double*>(
        static_cast<const uint8_t*>(tensor.data) + tensor.byte_offset);
    // Row-major matrices store translation in the last row.
    std::printf("World translation: %.1f %.1f %.1f\n", values[12], values[13], values[14]);
    // World translation: 10.0 0.0 0.0
    // Release the borrowed data before releasing the read and query.
    ovstage_release_group(stage, &group);
    waitOp(stage, ovstage_release_read(stage, read), "release read");
    waitOp(stage, ovstage_release_query(stage, query), "release query");
    path_dictionary_release_path_list_reference(dict, pathList);
}

Rebuilding from the Current USD Source#

population.repopulate_usd_with_desc() rewrites selected attributes from the current USD source at the supplied ordinal. Use it to replace runtime edits with USD values. Pass time in seconds, or ovstage.population.DEFAULT_TIME for default opinions. Use population.repopulate_usd_with_desc_async() for asynchronous execution.

Repopulation keeps unselected prims and attributes, including runtime-created data. Delete them explicitly if they must be removed. The supplied descriptions also set the population scope for later updates.

The USD stage, root and session layers, edits, composition state, edit target, and reference handles stay valid.

For incremental updates, use population.apply_usd_changes() or population.update_from_usd_time(). These rewrite changed or time-sampled USD values, so they can leave runtime edits in place on other attributes.

Editing the USD Source#

Reference edits change the USD source first. Apply them to the runtime stage at a new ordinal:

The reference target decides what the next apply republishes. A not-yet-existing prim path republishes only the new prim and the content beneath it. An existing prim path republishes that prim and every prim beneath it, including prims the reference does not add or change; the absolute root republishes the whole stage. Every selected attribute of a republished prim is written at the apply ordinal even when its value is unchanged, so a read of that ordinal range reports it as changed, and the written value replaces any value written directly into the runtime stage since. To add content without republishing existing prims, target a not-yet-existing prim path.

Note

Clearing all USD values of a populated application attribute, with no fallback remaining, removes its runtime column on the next apply_usd_changes. With RENDERING enabled, renderer-retained columns are exceptions: schema attributes, primvars, properties of shader and node-graph prims, and authored properties of RenderSettings, RenderProduct, and RenderVar prims.

These examples update the USD source, apply its changes, and read the result:

# Reference a cube onto a new /World/EditCube in the USD source, then
# propagate the change into the runtime table with apply_usd_changes
# at a fresh ordinal (above the animation's floor). The runtime stage
# now sees a prim that existed only in USD.
usd_ordinal = 2 + FRAMES
population.add_usd_reference_from_string(stage, EDIT_CUBE_USDA, "/World/EditCube")
population.apply_usd_changes(stage, ordinal=usd_ordinal)
stage.advance_write_floor(ordinal=usd_ordinal).wait()

expanded_paths = paths.create_path_list_from_strings(
    ["/World", "/World/Plane", "/World/Torus", "/World/EditCube"])
expanded_query = stage.query_from_path_list(expanded_paths)
print("after USD edit, prim types:",
      ", ".join(_prim_types(stage, paths, expanded_query, prim_type, usd_ordinal)))
// Reference a cube onto a new /World/EditCube in the USD source, then
// propagate the edit into the runtime table with apply_usd_changes at a
// fresh ordinal above the animation's floor. add-reference carries no
// ordinal; apply_usd_changes does.
const ovstage_ordinal_t usdOrdinal = static_cast<ovstage_ordinal_t>(2 + kFrames);
const ovx_string_t editLayer = kEditCubeUsda;
// The optional out-handle is only needed for a later remove_usd_reference;
// this reference lives for the stage's lifetime, so pass NULL (as the
// Python sibling does by discarding the returned handle).
popEnq = ovstage_population_add_usd_reference_from_string(stage, editLayer,
                                                          literal_to_ovx_string("/World/EditCube"), nullptr);
waitPop(stage, popEnq, "add_usd_reference");
popEnq = ovstage_population_apply_usd_changes(stage, usdOrdinal);
waitPop(stage, popEnq, "apply_usd_changes");

ovstage_write_floor_desc_t usdFloor{};
usdFloor.ordinal = usdOrdinal;
usdFloor.scope = OVSTAGE_SCOPE_ALL;
enq = ovstage_advance_write_floor(stage, &usdFloor);
waitOp(stage, enq, "advance_write_floor");

const ovx_string_t expanded[] = { literal_to_ovx_string("/World"), literal_to_ovx_string("/World/Plane"),
                                  literal_to_ovx_string("/World/Torus"),
                                  literal_to_ovx_string("/World/EditCube") };
ovx_primpath_list_t expandedPaths = OVX_INVALID_PRIMPATH_LIST;
ovxResult = path_dictionary_create_path_list_from_strings(dict, expanded, 4, &expandedPaths);
checkOvx(dict, ovxResult, "create-path-list");
ovstage_query_handle_t expandedQuery = OVSTAGE_INVALID_QUERY_HANDLE;
status = ovstage_query_from_path_list(stage, expandedPaths, &expandedQuery);
check(stage, status, "query_from_path_list");
const std::vector<std::string> editedTypes = readPrimTypes(stage, dict, expandedQuery, primType, usdOrdinal);

// Example plumbing: print the type names on one line; the Cube is the propagated edit.
std::printf("after USD edit, prim types:");
for (const std::string& name : editedTypes)
    std::printf(" %s", name.c_str());
std::printf("\n");

Advancing Time#

Use ovstage.population.update_from_usd_time() to sample animated USD values at a new time and write them at a new ordinal. It uses the domains and selectors from the opening call, including selectors with domains=NONE. Edits to time samples are picked up without a separate apply_usd_changes().

The time argument is in seconds, converted using the stage’s timeCodesPerSecond. Pass population.DEFAULT_TIME for USD’s default time code; it is also the default for the basic open functions. The _with_desc functions require an explicit time.

DEFAULT_TIME is NaN, so test it with population.is_default_time() rather than ==. In C, use OVSTAGE_POPULATION_DEFAULT_TIME and ovstage_population_is_default_time().

Reapplying the same time rewrites in-scope time-sampled properties in every domain, including at default time. It is not a no-op.

Use apply_usd_changes() for structural edits and changes to properties that are not time samples. The RENDERING domain currently propagates these during time updates too; other domains do not. Do not rely on that difference.

The example temperature has samples of 20 at USD time code 0 and 30 at time code 24. Sample one second at ordinal 3, then read 30:

# One second is USD time code 24 in this scene. The ordinal is independent.
population.update_from_usd_time(stage, time=1.0, ordinal=3)
stage.advance_write_floor(ordinal=3).wait()
with paths.create_path_list_from_strings(["/World/Body"]) as selected:
    with stage.query_from_path_list(selected) as query:
        with stage.read_attributes(query, [paths.intern_token("temperature")], OrdinalRange.latest(3)) as read:
            read.wait()
            for group in read.groups():
                with group:
                    print(float(group.array(0).ravel()[0]))
                    # 30.0
// Seconds are converted using timeCodesPerSecond=24, independently of ordinal 3.
waitPop(stage, ovstage_population_apply_usd_time(stage, 3, 1.0), "sample one second");
watermark.ordinal = 3;
waitOp(stage, ovstage_advance_write_floor(stage, &watermark), "advance watermark");
{
    const ovx_string_t selected[] = {literal_to_ovx_string("/World/Body")};
    ovx_primpath_list_t pathList{};
    checkOvx(dict, path_dictionary_create_path_list_from_strings(dict, selected, 1, &pathList), "paths");
    ovstage_query_handle_t query{};
    check(stage, ovstage_query_from_path_list(stage, pathList, &query), "query");
    const ovx_string_t name = literal_to_ovx_string("temperature");
    ovx_token_t attribute{};
    checkOvx(dict, path_dictionary_create_tokens_from_strings(dict, &name, 1, &attribute), "attribute");
    ovstage_ordinal_range_t range{};
    range.end_ordinal = 3;
    ovstage_read_handle_t read{};
    waitOp(stage, ovstage_read_attributes(stage, query, &attribute, 1, range, &read), "read");
    ovstage_read_group_t group{};
    check(stage, ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group), "fetch");
    const DLTensor& tensor = group.data.tensors[0];
    const auto* values = reinterpret_cast<const float*>(
        static_cast<const uint8_t*>(tensor.data) + tensor.byte_offset);
    std::printf("%.1f\n", static_cast<double>(values[0]));
    // 30.0
    // Release the borrowed data before releasing the read and query.
    ovstage_release_group(stage, &group);
    waitOp(stage, ovstage_release_read(stage, read), "release read");
    waitOp(stage, ovstage_release_query(stage, query), "release query");
    path_dictionary_release_path_list_reference(dict, pathList);
}

Python ↔ C Name Mapping#

Several Python wrappers use shorter names than their C entry points:

Choosing Ordinals#

The application assigns ordinals to population operations and seals completed updates with advance_write_floor. Population never chooses, increments, or seals an ordinal for you. A tick may use several ordinals; seal through the final one once its updates complete. An operation at or below an applicable floor is a write-floor violation under every admission policy.

Python requires an explicit ordinal for open_usd (file/URL), open_usd_from_string, apply_usd_changes, and their _async variants. Pass it positionally or as ordinal=.... The former implicit value of 1 has been removed; calls that omit it raise TypeError. The _with_desc and update_from_usd_time helpers already require an explicit ordinal.

A newly created instance already has a write floor of 0, so the first population must use an ordinal of at least 1 — 0 is never a usable write ordinal, even though it is a perfectly valid ordinal to read at. An application whose frame counter starts at 0 should offset it rather than pass 0 through.

Use increasing ordinals for successive updates to the same data. For example, open at ordinal 1, propagate later USD edits with apply_usd_changes at 2, and update time-sampled properties with update_from_usd_time at 3. Wait for each operation before starting the next, then seal through ordinal 3. Use higher values when other writers or floors have already advanced the stage.

If you apply direct overrides after population, the same sequence applies: populate at 1, wait, write the overrides at 2, wait, then seal through 2. See Ordinal Admission Policies for the policy options and their configuration.

After using ordinals 1 through 3, start the next updates at 4 even though the frame counter starts at 0:

# Population has already used ordinals 1, 2, and 3 above.
for frame in range(3):
    ordinal = 4 + frame
    population.update_from_usd_time(stage, time=frame / 24.0, ordinal=ordinal)
    stage.advance_write_floor(ordinal=ordinal).wait()
    # Consumers can now read through this ordinal before the next update.
// Start after the three ordinals already used in this example.
for (int frame = 0; frame < 3; ++frame)
{
    const ovstage_ordinal_t ordinal = 4 + frame;
    waitPop(stage, ovstage_population_apply_usd_time(stage, ordinal, frame / 24.0), "sample frame");
    watermark.ordinal = ordinal;
    waitOp(stage, ovstage_advance_write_floor(stage, &watermark), "advance watermark");
    // Consumers can now read through this ordinal before the next update.
}

Repopulating a Stage#

Opening a new USD source adds or updates its data without removing values left by the previous source. Prims missing from the new scene remain, as do old attributes that the new scene no longer publishes.

To replace previously populated content:

  1. Call population.reset_usd(stage) to clear the USD source.

  2. Call population.apply_usd_changes(stage, ordinal=n) to remove its populated content. Prims created only by direct application writes remain.

  3. Call population.open_usd(stage, path, ordinal=n + 1) to load the replacement.

Wait for each step before starting the next. The blocking Python functions do this; with asynchronous calls or C, observe each operation’s completion. The following examples clear and reload a small Cube scene. CUBE_USDA in Python and kCubeUsda in C hold the scene text. Their test helpers count prims under /World to check removal:

# reset_usd clears the USD source; apply_usd_changes propagates the cleared
# state into the stage. The stage stays usable afterwards — repopulating from
# USD still works.
population.reset_usd(stage)
population.apply_usd_changes(stage, ordinal=2)
assert _prefix_count(stage, "/World") == 0  # cleared before repopulating
population.open_usd_from_string(stage, CUBE_USDA, ordinal=3, domains=PopulationDomain.RENDERING)
// reset_usd clears the USD source; apply_usd_changes propagates the cleared
// state. The stage stays usable afterwards — repopulating from USD still works.
ASSERT_TRUE(waitPopOk(stage_, ovstage_population_reset_usd(stage_), "reset_usd"));
ASSERT_TRUE(waitPopOk(stage_, ovstage_population_apply_usd_changes(stage_, 2), "apply_usd_changes"));
EXPECT_EQ(prefixCount("/World"), 0u);  // cleared before repopulating
ASSERT_TRUE(waitPopOk(stage_,
    ovstage_population_open_usd_from_string(stage_, str(kCubeUsda), 3, NAN, OVSTAGE_POPULATION_DOMAIN_RENDERING),
    "reopen"));

To clear the whole stage, including application-created prims, use an unfiltered stage.query(), wait, then call stage.delete_attributes(query, [], n). An empty attribute list deletes the selected prims. Wait for deletion and release the query before opening the new source. In C, the same operations are ovstage_query() with no filter and ovstage_delete_attributes() with no attribute names, followed by ovstage_release_query().

Use a fresh ordinal for deletion and another for population. Wait for deletion to complete before starting population.

Failure and the Ordinal#

A submission failure means the operation never ran. It leaves previously populated content unchanged, so the same ordinal can be retried if it still satisfies current watermarks, admission policy, and stage state. In C, this is the enqueue result’s status.

A failure while waiting may follow partial writes or removal of previous content. Retry population at a higher ordinal. Python raises OvstageError; in C, inspect ovstage_population_get_last_error() after the failed ovstage_population_wait_op(). The status code alone does not show how much work completed.

This example deliberately opens a missing file, distinguishes rejection from failure during execution, and retries with the valid scene:

ordinal = 1
try:
    operation = population.open_usd_async(
        stage, scene + ".missing", ordinal=ordinal, domains=PopulationDomain.PHYSICS
    )
except ovstage.OvstageError:
    # Rejected at submission: no writes ran, so this ordinal can be reused.
    retry_ordinal = ordinal
else:
    try:
        operation.wait()
    except ovstage.OvstageError:
        # Accepted but failed while running: allow for partial writes.
        retry_ordinal = ordinal + 1
    else:
        raise RuntimeError("Expected the missing file to fail")
# Fix the input and retry at the chosen ordinal.
population.open_usd(stage, scene, ordinal=retry_ordinal, domains=PopulationDomain.PHYSICS)
stage.advance_write_floor(ordinal=retry_ordinal).wait()
const std::string missing = scene + ".missing";
const ovstage_ordinal_t ordinal = 1;
ovstage_ordinal_t retryOrdinal = ordinal;
const auto operation = ovstage_population_open_usd_from_file(
    stage, {missing.c_str(), missing.size()}, ordinal,
    OVSTAGE_POPULATION_DEFAULT_TIME, OVSTAGE_POPULATION_DOMAIN_PHYSICS);
if (operation.status == OVSTAGE_OK)
{
    ovstage_population_op_wait_result_t result{};
    const auto status = ovstage_population_wait_op(
        stage, operation.op_index, OVSTAGE_TIMEOUT_INFINITE, &result);
    // A successful wait can still report a failed operation.
    if (status == OVSTAGE_OK && result.error_op_id_count == 0)
    {
        std::fprintf(stderr, "Expected the missing-file load to fail\n");
        ovstage_destroy_instance(stage);
        return EXIT_FAILURE;
    }
    retryOrdinal = ordinal + 1; // accepted: allow for partial writes
}
// Submission rejection leaves retryOrdinal unchanged: no writes ran.
// Read the diagnostic before the retry replaces it.
const ovx_string_t error = ovstage_population_get_last_error();
std::fprintf(stderr, "Population failed: %.*s\n", static_cast<int>(error.length), error.ptr);
// Population failed: <diagnostic for the missing file>
waitPop(stage, ovstage_population_open_usd_from_file(
    stage, scenePath, retryOrdinal, OVSTAGE_POPULATION_DEFAULT_TIME,
    OVSTAGE_POPULATION_DOMAIN_PHYSICS), "retry with valid file");
watermark.ordinal = retryOrdinal;
waitOp(stage, ovstage_advance_write_floor(stage, &watermark), "advance watermark");

Further Reading#