Transforms#

Two attributes describe a prim’s transform:

  • omni:xform is always the prim’s local transform.

  • omni:resetXformStack controls whether the parent’s world transform participates in computing the prim’s world transform.

Set omni:resetXformStack=True when supplying object-to-world transforms explicitly for each prim, such as those returned by a rigid-body physics simulation. Each prim then uses its supplied transform directly, without also applying its parent’s transform.

Set omni:resetXformStack=False when a prim should move with its parent. For example, place a car’s wheels under its body and give each wheel a transform relative to the body. Moving the body moves the whole car, while updating each wheel’s local transform lets it rotate without losing its position relative to the body.

The examples below run in order on a USD-populated hierarchy. query selects /World/Car/Wheel; its parent, /World/Car, has a translation of 10 along X. Both initially inherit their parent transforms. The runnable transforms example selects every Xform during population, which creates the transform and derived-output rows used here. paths is the stage’s PathDictionary (dict in C). The C helpers waitOp(), check(), and checkOvx() wait for operations and check stage or dictionary errors.

Transform Data Layout#

omni:xform is a fixed-size, non-array attribute with:

  • float64 values with 16 DLPack lanes;

  • AttributeSemantic.MATRIX;

  • one row-major 4x4 matrix per queried prim.

ovstage follows the USD row-vector matrix convention. Translation occupies matrix elements 12, 13, and 14 (the first three elements of the last row).

omni:resetXformStack is a fixed-size, non-array scalar with DLPack dtype DLDataType(code=DLDataTypeCode.kDLBool, bits=8, lanes=1). Use boolean values; an integer column is not valid even if it has the same byte width.

These snippets prepare a matrix translating the wheel by 4 along X and a boolean reset value. Python supplies a compact (1, 4, 4) array; C uses one 16-lane tensor row:

import pathlib
import numpy as np
import ovstage
from ovstage import AttributeSemantic, HierarchyComputationModel, OrdinalRange, population

# One row-major matrix for the wheel; translation is in the last row.
matrices = np.eye(4, dtype=np.float64)[None, :, :]
matrices[0, 3, :3] = [4, 0, 0]
# A boolean per prim controls whether the parent transform is inherited.
reset = np.array([False], dtype=np.bool_)
// One row-major matrix for the wheel; translation is in elements 12..14.
double matrix[] = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 4, 0, 0, 1 };
int64_t shape[] = { 1 };
DLTensor matrixTensor{ matrix, { kDLCPU, 0 }, 1, { kDLFloat, 64, 16 }, shape, nullptr, 0 };
bool reset = false;
DLTensor resetTensor{ &reset, { kDLCPU, 0 }, 1, { kDLBool, 8, 1 }, shape, nullptr, 0 };

Writing Local or World Transforms#

Use these combinations when writing a transform:

Intent

omni:resetXformStack

Result

Author a local transform

False

omni:xform composes with the parent’s world transform.

Set a world transform

True

Parent composition stops at this prim, so its effective world transform is exactly omni:xform.

With reset enabled, the prim ignores its parent transform. Its children still inherit its transform. omni:xform remains the local matrix.

The reset value persists until another write or an xform-related USD synchronization replaces it. While it remains True, later transform updates need only write omni:xform. When changing both attributes, write them at the same ordinal and complete both before advancing the watermark or computing world transforms. ovstage.Stage.write_attributes() groups the writes in one operation, but does not make them transactional.

These snippets write the matrix and reset value prepared above. With reset set to false, the wheel keeps its position relative to the car:

# Both attributes describe the same update, at ordinal 2.
stage.write_attribute(query, "omni:xform", ordinal=2, tensors=matrices,
                      is_array=False, semantic=AttributeSemantic.MATRIX).wait()
stage.write_attribute(query, "omni:resetXformStack", ordinal=2,
                      tensors=reset, is_array=False).wait()
# False keeps the wheel relative to the car. True would make it world-relative.
// Both attributes describe the same update, at ordinal 2.
ovstage_write_data_t matrixWrite{};
matrixWrite.tensors = &matrixTensor;
matrixWrite.tensor_count = 1;
matrixWrite.is_array = false;
matrixWrite.semantic = OVSTAGE_SEMANTIC_MATRIX;
waitOp(stage, ovstage_write_attribute(stage, query, { 0, literal_to_ovx_string("omni:xform") },
    2, matrixWrite, OVSTAGE_PRIM_MODE_UPSERT), "write transform");
ovstage_write_data_t resetWrite{};
resetWrite.tensors = &resetTensor;
resetWrite.tensor_count = 1;
resetWrite.is_array = false;
waitOp(stage, ovstage_write_attribute(stage, query, { 0, literal_to_ovx_string("omni:resetXformStack") },
    2, resetWrite, OVSTAGE_PRIM_MODE_UPSERT), "write reset");
// false keeps the wheel relative to the car; true would make it world-relative.

Deriving World Transforms#

Call ovstage.Stage.compute_hierarchy() to compute world transforms. Choose a model returned by ovstage.Stage.get_hierarchy_computation_models().

The ovstage.HierarchyComputationModel values choose where and how hierarchy data is updated:

Model

Behavior

CPU_INCREMENTAL

Updates affected hierarchy data on the CPU.

GPU_INCREMENTAL

Updates affected hierarchy data on the GPU.

GPU_GLOBAL

Recomputes hierarchy data globally on the GPU.

DEFAULT_CPU currently aliases CPU_INCREMENTAL; DEFAULT_GPU aliases GPU_GLOBAL. RUNTIME_DEFAULT uses the process default captured when the stage was created, which defaults to CPU_INCREMENTAL. It does not select a GPU model merely because the input buffers are on the GPU.

Check get_hierarchy_computation_models() for availability, then pass the chosen model as model= to compute_hierarchy(). The CUDA examples below select GPU_GLOBAL explicitly and wait for computation to complete.

In the current release, CPU incremental hierarchy computation supports USD-populated transform hierarchies. A hierarchy created entirely through client writes requires a supported GPU computation model and pre-existing derived-output rows. Refer to the authoring-hierarchy pair in Examples for that workflow. Its clone snippet customizes environment roots at ordinal 3 but seals only ordinal 2. Ordinal 3 remains open until the later output-row initialization and hierarchy computation finish; the example then seals 3 before reading those results.

To compute transforms for one update:

  1. Write omni:xform and, when needed, omni:resetXformStack at ordinal N.

  2. Wait for the writes to complete.

  3. Call stage.compute_hierarchy(input_ordinal=N, output_ordinal=N, model=model). This call blocks. If using compute_hierarchy_async(), wait for its operation before advancing the watermark.

  4. Advance the watermark to N and wait before reading the results.

The input and output ordinals do not select historical transforms. Computation uses current values. Do not overlap writes from a later update with the computation. Reads and attached consumers can also trigger runtime-default propagation, so do not use a read to check whether transforms are stale. Call compute_hierarchy() when your application needs to control when they are updated.

These snippets select the supported CPU model, compute the wheel’s world transform, and then advance the watermark:

# CPU incremental computation supports this USD-populated hierarchy.
model = HierarchyComputationModel.CPU_INCREMENTAL
if model not in [item.model_id for item in stage.get_hierarchy_computation_models()]:
    raise RuntimeError("CPU incremental hierarchy computation is unavailable")
stage.compute_hierarchy(input_ordinal=2, output_ordinal=2, model=model)
stage.advance_write_floor(ordinal=2).wait()
# Car x=10 plus wheel local x=4 gives wheel world x=14.
// CPU incremental computation supports this USD-populated hierarchy.
const ovstage_hierarchy_computation_model_desc_t* models = nullptr;
size_t count = 0;
check(stage, ovstage_get_hierarchy_computation_models(stage, &models, &count), "get models");
const auto model = OVSTAGE_HIERARCHY_COMPUTATION_MODEL_CPU_INCREMENTAL;
bool supported = false;
for (size_t i = 0; i < count; ++i)
    supported = supported || models[i].model_id == model;
if (!supported)
{
    std::fprintf(stderr, "CPU incremental hierarchy computation is unavailable\n");
    std::exit(EXIT_FAILURE);
}
ovstage_write_floor_desc_t watermark{};
watermark.ordinal = 2;
watermark.scope = OVSTAGE_SCOPE_ALL;
waitOp(stage, ovstage_compute_hierarchy(stage, model, 2, 2), "compute hierarchy");
waitOp(stage, ovstage_advance_write_floor(stage, &watermark), "advance watermark");
// Car x=10 plus wheel local x=4 gives wheel world x=14.

How This Differs from Editing USD#

The inheritance semantics match USD’s reset-xform-stack behavior, but the representation and data ownership differ:

  • USD encodes a reset in xformOpOrder as !resetXformStack!.

  • ovstage exposes the mirrored runtime state as an independently writable omni:resetXformStack runtime attribute.

When population includes a prim that supports USD transforms, it initializes omni:xform and omni:resetXformStack from USD. See Population — Ancestors and Transforms for how prims are selected. An application can then change either value in ovstage without editing the USD source. For example, setting the runtime reset solely to publish world-space simulation poses can make ovstage’s reset value differ from the reset authored in USD. Do not assume the runtime value still describes the source layer after such a write.

These snippets change only the runtime reset to true. The source USD still inherits the parent transform, while the runtime wheel now uses its supplied matrix as its world transform:

# Override the runtime reset without editing the source USD's xformOpOrder.
stage.write_attribute(query, "omni:resetXformStack", ordinal=3,
                      tensors=np.array([True], dtype=np.bool_), is_array=False).wait()
stage.compute_hierarchy(input_ordinal=3, output_ordinal=3, model=model)
stage.advance_write_floor(ordinal=3).wait()
# The unchanged matrix now puts the wheel at world x=4, ignoring the car.
// Override the runtime reset without editing the source USD's xformOpOrder.
reset = true; // The earlier write has completed, so its input can be changed.
waitOp(stage, ovstage_write_attribute(stage, query, { 0, literal_to_ovx_string("omni:resetXformStack") },
    3, resetWrite, OVSTAGE_PRIM_MODE_UPSERT), "override reset");
watermark.ordinal = 3;
waitOp(stage, ovstage_compute_hierarchy(stage, model, 3, 3), "compute hierarchy");
waitOp(stage, ovstage_advance_write_floor(stage, &watermark), "advance watermark");
// The unchanged matrix now puts the wheel at world x=4, ignoring the car.

Reading World Transforms#

Read omni:fabric:worldMatrix to get a prim’s computed object-to-world matrix. It combines the prim’s omni:xform with its ancestors’ transforms, stopping wherever omni:resetXformStack is true. Treat it as read-only output. To change a pose directly in the runtime stage, write omni:xform and omni:resetXformStack, then compute the hierarchy.

There are two separate requirements: the output attribute must exist, and hierarchy computation must have finished for the update you want to read. An existing attribute alone does not mean its value is up to date.

When Does the Attribute Exist?#

Being transformable in USD does not, by itself, guarantee that a prim has omni:fabric:worldMatrix in the runtime stage. Population must include the prim, and the population path must create this output:

How the prim enters the runtime stage

World-matrix output

Population without RENDERING, including PHYSICS or explicit selectors

Population creates the attribute for each in-scope USD-transformable prim. Its initial value is the local matrix; compute the hierarchy before using it as a world matrix.

Population with RENDERING, including ALL

The rendering domain owns omni:fabric:worldMatrix on ordinary Xform and geometry prims. Generated instance prims, such as XformInstance and MeshInstance, can store a compact world transform instead. Check for omni:fabric:worldMatrix on these instance prims before reading it.

Prims created entirely through runtime writes

Writing omni:xform does not automatically create the world-matrix attribute. Initialize placeholder output rows once during hierarchy setup, then let computation fill them. See the authoring-hierarchy example in Examples.

compute_hierarchy() updates existing output rows; it does not create missing ones. Repeating the computation will not fix a missing attribute. Check the population selection and hierarchy setup instead. See Ancestors and Transforms for selection rules and Queries for checking whether an attribute exists on a prim.

When Is the Value Ready to Read?#

USD changes applied through rendering population: when RENDERING is enabled (including ALL), population updates world transforms as it processes USD changes. After editing USD, call population.apply_usd_changes() and wait for completion if using its asynchronous form. Then advance the watermark to the update’s ordinal and wait before reading. You do not need a separate compute_hierarchy() call for those USD changes. Editing USD alone does not update the runtime stage; population must process the changes first. It updates affected transforms, rather than necessarily recomputing every matrix for every USD edit.

Direct runtime transform writes, or population without rendering: use explicit hierarchy computation before reading. For direct runtime writes, this applies even if the stage was originally populated with RENDERING. Follow this order:

  1. Finish population or writes to the transform inputs.

  2. Call compute_hierarchy() with that ordinal for both input_ordinal and output_ordinal. If using compute_hierarchy_async(), wait for it.

  3. Advance the watermark to that update’s ordinal and wait.

  4. Read omni:fabric:worldMatrix with OrdinalRange.latest(ordinal). Finish reading before writing the next update.

Use a supported computation model as described in Deriving World Transforms.

Note

The current implementation uses the latest committed inputs; input_ordinal does not select a historical snapshot. Neither ordinal preserves old matrices. Updated world transforms participate in change reads at output_ordinal, even if that ordinal is already sealed. Consumers that already read that ordinal will miss these updates, so compute first and then advance the watermark.

Advancing the watermark for omni:fabric:worldMatrix also recomputes world transforms when their inputs have changed, using the runtime default model, and makes those changes available at the advance ordinal. Compute explicitly first to choose the model.

For the car and wheel above, the car is at X=10 and the wheel’s local matrix has X=4:

Wheel’s omni:resetXformStack

Computed wheel world X

False

14: the wheel inherits the car’s translation.

True

4: the wheel ignores its parent’s translation.

The following snippets run after the reset override and computation at ordinal 3, so they print Wheel world x: 4.0. They fetch every result group and map each prim to its matrix row. An absent output row is not an identity matrix and must not be treated as one.

import pathlib
import numpy as np
import ovstage
from ovstage import AttributeSemantic, HierarchyComputationModel, OrdinalRange, population

# Read the derived output only after computation; never write it directly.
world = paths.intern_token("omni:fabric:worldMatrix")
found = False
with stage.read_attributes(query, [world], OrdinalRange.latest(3)) as read:
    read.wait()
    for group in read.groups():
        with group:
            world_matrices = group.array(0).reshape(-1, 4, 4)
            for i in range(group.prim_count):
                row = group.data_row_index(i) if group.has_data_index_map else i
                print("Wheel world x:", float(world_matrices[row, 3, 0]))
                # Wheel world x: 4.0
                found = True
if not found:
    print("No derived world-matrix row is available.")
    # No derived world-matrix row is available.
// Read the derived output only after computation; never write it directly.
const ovx_string_t name = literal_to_ovx_string("omni:fabric:worldMatrix");
ovx_token_t world = OVX_INVALID_TOKEN;
checkOvx(dict, path_dictionary_create_tokens_from_strings(dict, &name, 1, &world), "intern world matrix");
ovstage_ordinal_range_t range{};
range.end_ordinal = 3;
ovstage_read_handle_t read = OVSTAGE_INVALID_READ_HANDLE;
waitOp(stage, ovstage_read_attributes(stage, query, &world, 1, range, &read), "read world matrix");
bool found = false;
for (;;)
{
    ovstage_read_group_t group{};
    const auto status = ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group);
    if (status == OVSTAGE_ERROR_END_OF_ITERATION)
        break;
    check(stage, status, "fetch group");
    const DLTensor& tensor = group.data.tensors[0];
    const auto* values = reinterpret_cast<const double*>(
        static_cast<const uint8_t*>(tensor.data) + tensor.byte_offset);
    for (uint32_t i = 0; i < group.prims.count; ++i)
    {
        const uint32_t row = group.data.index_map ? group.data.index_map[i] : i;
        std::printf("Wheel world x: %.1f\n", values[row * 16 + 12]);
        // Wheel world x: 4.0
        found = true;
    }
    check(stage, ovstage_release_group(stage, &group), "release group");
}
waitOp(stage, ovstage_release_read(stage, read), "release read");
if (!found)
{
    std::printf("No derived world-matrix row is available.\n");
    // No derived world-matrix row is available.
}

The attribute stores one row-major 4x4 float64 matrix per prim, with the same matrix convention as omni:xform. The Python snippet reshapes the returned values to (row_count, 4, 4); the C tensor has the same shape with scalar lanes and compact row-major strides. These unplaced reads return CPU buffers, even if hierarchy computation ran on the GPU. Use Experimental Device Placement for a CUDA read. See Reading Attributes for result-group ownership and row mapping.

Writing Transforms From CUDA Buffers#

Supply CUDA buffers directly to write_attribute() to update transforms without copying their values back to the CPU. Use one contiguous, row-major float64 4×4 matrix per prim in query order, with is_array=False and AttributeSemantic.MATRIX. Python accepts a CUDA DLPack producer with shape (prim_count, 4, 4) or (prim_count, 16); C describes the device pointer with a DLTensor whose device is kDLCUDA.

For object-to-world matrices, set omni:resetXformStack to true for each prim. The examples below also supply these boolean flags from a CUDA buffer. If reset is already set correctly, subsequent updates need only write omni:xform.

Record a CUDA event after producing both buffers and pass it with the writes. Keep the buffers and event alive, and do not overwrite the buffers until the writes complete. Waiting on an operation synchronizes completion; it does not copy the transform values to the CPU.

To keep the write payloads on the GPU, use dense inputs in query order, without index_map or mask. The current sparse fixed-size write path can copy inputs through CPU memory. Do not call read_attributes() or map the values without placement in this workflow: unplaced reads and maps return CPU buffers. The experimental Experimental Device Placement can instead select CUDA buffers. If world transforms must also be computed on the GPU, select a supported GPU hierarchy computation model rather than CPU_INCREMENTAL. GPU writes alone do not determine where a downstream consumer performs its work.

These helpers accept buffers already produced on the GPU and compute the hierarchy using GPU_GLOBAL. Use a populated hierarchy with existing derived-output rows, as in the earlier example. prim_count must match the query’s prim count, and ordinal must be above the watermark. The Python ready_event is the integer handle of the recorded CUDA event; the C helper accepts a cudaEvent_t. Neither helper reads the values back:

from ovstage import AttributeSemantic, DLDataType, DLDataTypeCode, HierarchyComputationModel, make_dltensor


def write_gpu_transforms(stage, query, matrices, reset_flags, prim_count, ordinal, ready_event):
    """Write CUDA-resident matrices and reset flags after the producer event."""
    # Choose an available GPU model before changing the stage.
    model = HierarchyComputationModel.GPU_GLOBAL
    if model not in [item.model_id for item in stage.get_hierarchy_computation_models()]:
        raise RuntimeError("GPU global hierarchy computation is unavailable")

    # matrices is a contiguous CUDA DLPack producer with float64 values and
    # shape (prim_count, 4, 4) or (prim_count, 16). No buffer copy is needed.
    matrix_tensor = make_dltensor(matrices,
        dtype=DLDataType(code=DLDataTypeCode.kDLFloat, bits=64, lanes=16),
        shape=[prim_count])
    # reset_flags is a CUDA boolean array: True means explicit world transforms.
    reset_tensor = make_dltensor(reset_flags,
        dtype=DLDataType(code=DLDataTypeCode.kDLBool, bits=8, lanes=1),
        shape=[prim_count])

    # ready_event is the integer handle of a CUDA event recorded after both
    # buffers were produced. Waits synchronize work; they do not read values back.
    stage.write_attribute(query, "omni:xform", ordinal=ordinal,
        tensors=matrix_tensor, is_array=False, semantic=AttributeSemantic.MATRIX,
        cuda_event=ready_event).wait()
    stage.write_attribute(query, "omni:resetXformStack", ordinal=ordinal,
        tensors=reset_tensor, is_array=False, cuda_event=ready_event).wait()
    # Keep both buffers and the event alive and unchanged until these writes finish.
    # Compute from the completed update on the GPU; no payload readback.
    stage.compute_hierarchy(input_ordinal=ordinal, output_ordinal=ordinal, model=model)
    stage.advance_write_floor(ordinal=ordinal).wait()
// Write caller-owned CUDA buffers after the producer event, with no CPU readback.
void writeGpuTransforms(ovstage_instance_t* stage, ovstage_query_handle_t query,
                       double* deviceMatrices, bool* deviceResetFlags,
                       int64_t primCount, int deviceId, ovstage_ordinal_t ordinal,
                       cudaEvent_t readyEvent)
{
    // Choose an available GPU model before changing the stage.
    const ovstage_hierarchy_computation_model_desc_t* models = nullptr;
    size_t modelCount = 0;
    check(stage, ovstage_get_hierarchy_computation_models(stage, &models, &modelCount), "get models");
    const auto model = OVSTAGE_HIERARCHY_COMPUTATION_MODEL_GPU_GLOBAL;
    bool supported = false;
    for (size_t i = 0; i < modelCount; ++i)
        supported = supported || models[i].model_id == model;
    if (!supported)
    {
        std::fprintf(stderr, "GPU global hierarchy computation is unavailable\n");
        std::exit(EXIT_FAILURE);
    }

    // Dense query order: one row-major 4x4 matrix and one bool per prim.
    int64_t shape[] = { primCount };
    DLTensor matrices{ deviceMatrices, { kDLCUDA, deviceId }, 1,
        { kDLFloat, 64, 16 }, shape, nullptr, 0 };
    DLTensor resetFlags{ deviceResetFlags, { kDLCUDA, deviceId }, 1,
        { kDLBool, 8, 1 }, shape, nullptr, 0 };

    // Record readyEvent on the producing stream after filling both buffers.
    ovstage_write_data_t write{};
    write.tensors = &matrices;
    write.tensor_count = 1;
    write.is_array = false;
    write.semantic = OVSTAGE_SEMANTIC_MATRIX;
    write.cuda_sync.wait_event = reinterpret_cast<uintptr_t>(readyEvent);
    waitOp(stage, ovstage_write_attribute(stage, query,
        { 0, literal_to_ovx_string("omni:xform") }, ordinal, write,
        OVSTAGE_PRIM_MODE_UPSERT), "write GPU transforms");

    write.tensors = &resetFlags;
    write.semantic = OVSTAGE_SEMANTIC_NONE;
    waitOp(stage, ovstage_write_attribute(stage, query,
        { 0, literal_to_ovx_string("omni:resetXformStack") }, ordinal, write,
        OVSTAGE_PRIM_MODE_UPSERT), "write GPU reset flags");
    // Both writes are complete: the caller can now reuse the buffers and event.
    ovstage_write_floor_desc_t watermark{};
    watermark.ordinal = ordinal;
    watermark.scope = OVSTAGE_SCOPE_ALL;
    // Wait for GPU hierarchy computation before a consumer uses the result.
    waitOp(stage, ovstage_compute_hierarchy(stage, model, ordinal, ordinal), "compute GPU hierarchy");
    waitOp(stage, ovstage_advance_write_floor(stage, &watermark), "advance watermark");
}

Further Reading#