Runtime Loop#
This guide drives an ovstage scene headlessly — no renderer attached: load a USD
scene into the runtime table through population, read prims
back to confirm, update the live stage, and re-read. The application owns the
ordinal lifecycle throughout: populate at one ordinal, then write updates at
higher ordinals, sealing each tick with advance_write_floor.
Note
This walkthrough uses the population capability. The runtime-loop examples
are pre-release and provisional (“Draft — API in flux”); the
transform-write recipe below uses the omni:xform attribute and
MATRIX semantic and is subject to change.
Setup#
import pathlib
import sys
import numpy as np
import ovstage
from ovstage import (AttributeSemantic, DLDataType, DLDataTypeCode, OrdinalRange, OvstageError,
PopulationDomain, make_dltensor, population)
#include <ovstage/ovstage.h>
#include <ovstage/ovstage_population.h>
#include <ovx/path_dictionary/path_dictionary.h>
#include <ovx/path_dictionary/path_dictionary_utils.h>
#include <dlpack/dlpack.h>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <string>
#include <vector>
Two Update Paths#
After a scene is live there are two ways to change it:
Write into the runtime table at a new ordinal — a direct edit with no USD round-trip (for example, animate a prim’s
omni:xformtransform over frames).Edit the USD source with
add_usd_referenceand propagate it through withapply_usd_changes.
1. Populate#
Load the scene at ordinal 1 and seal it:
# 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_code=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");
2. Read Back to Confirm#
Prove the populate landed by reading the reserved usd-prim-type metadata over
the scene query and resolving each token to its type name:
# Read the reserved usd-prim-type metadata ovstage auto-maintains for
# every populated prim.
print("populated prim types:", ", ".join(_prim_types(stage, paths, scene_query, prim_type, 1)))
// Read the reserved usd-prim-type column over a query and return the resolved
// type names as-is, in stage order -- proof that the prims populated.
// usd-prim-type is one token per prim (is_array=false); the column crosses as
// uint64 token ids resolved back to strings through the path dictionary.
static std::vector<std::string> readPrimTypes(ovstage_instance_t* stage, path_dictionary_instance_t* dict,
ovstage_query_handle_t query, ovx_token_t primType,
ovstage_ordinal_t endOrdinal)
{
ovstage_ordinal_range_t range{};
range.end_ordinal = endOrdinal;
range.has_start_ordinal = false;
ovstage_read_handle_t read = OVSTAGE_INVALID_READ_HANDLE;
ovstage_enqueue_result_t enq = ovstage_read_attributes(stage, query, &primType, 1, range, &read);
waitOp(stage, enq, "read_attributes");
std::vector<std::string> typeNames;
for (;;)
{
ovstage_read_group_t group{};
const ovstage_api_status_t fetched = ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group);
if (fetched == OVSTAGE_ERROR_END_OF_ITERATION)
break; // the normal end of the group stream
check(stage, fetched, "fetch_read_next");
if (group.data.tensor_count == 1 && group.data.tensors[0].data && group.data.tensors[0].ndim > 0)
{
const uint64_t* ids = static_cast<const uint64_t*>(group.data.tensors[0].data);
for (int64_t i = 0; i < group.data.tensors[0].shape[0]; ++i)
{
ovx_token_t token = static_cast<ovx_token_t>(ids[i]);
ovx_string_t name{};
ovx_api_result_t ovxResult = path_dictionary_get_strings_from_tokens(dict, &token, 1, &name);
checkOvx(dict, ovxResult, "resolve-type");
typeNames.push_back(std::string(name.ptr ? name.ptr : "", name.length));
}
}
ovstage_release_group(stage, &group);
}
enq = ovstage_release_read(stage, read);
waitOp(stage, enq, "release_read");
return typeNames;
}
Note
This example reads back the transform it wrote itself. Reading a populated
implementation-defined derived scene transform (omni:fabric:localMatrix /
omni:fabric:worldMatrix) back through
read_attributes is not part of this validated flow.
3. Update the Table#
Animate a prim by writing omni:xform (a 4×4 matrix, MATRIX semantic)
straight into the table over successive ordinals, advancing the floor each step:
# Write the Torus transform into the ovstage table over 24 frames (one
# ordinal per frame), no USD round-trip. omni:xform is a 4x4 matrix
# column -> semantic=MATRIX.
for frame in range(FRAMES):
ordinal = 2 + frame
tx = 100.0 * frame / (FRAMES - 1) # 0 -> 100 across the animation
stage.write_attribute(
torus_query, xform, ordinal=ordinal,
tensors=_translate_matrix(tx, 25.0, 0.0),
is_array=False, semantic=AttributeSemantic.MATRIX,
).wait()
stage.advance_write_floor(ordinal=ordinal).wait()
# Read back the final frame's transform (our own written column).
# The read is a context manager -- block exit releases its handle even
# when a guard raises -- and the fetched group is released in a finally.
with stage.read_attributes(torus_query, [xform], OrdinalRange.latest(2 + FRAMES - 1)) as read:
read.wait()
group = read.fetch_next()
if group is None: # fetch_next() returns None when the read yields no group
raise SystemExit("no data group returned for the final-frame omni:xform read")
try:
m = np.asarray(group.array(0)).ravel() # row-major 4x4
if m.size != 16: # single-path query -> exactly one 16-lane matrix element
raise SystemExit(f"unexpected omni:xform read layout: {m.size} values, expected 16")
print("final Torus xform translation (row [3][0:3]):", m[12:15]) # -> [100. 25. 0.]
finally:
stage.release_group(group)
// Write the Torus transform over 24 frames (one sealed ordinal per frame),
// no USD round-trip. omni:xform is a 4x4 double matrix (row-vector
// convention: translation in elements [12..14]) -> semantic = MATRIX.
// The current implementation stores it as ONE 16-lane element per prim, so the tensor is dtype
// {kDLFloat, 64, 16} / shape={1} -- NOT a 4x4 of lanes=1.
double matrix[16] = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 };
const int64_t matShape[] = { 1 };
const int64_t matStrides[] = { 1 };
DLTensor matTensor{};
matTensor.data = matrix;
matTensor.device = { kDLCPU, 0 };
matTensor.ndim = 1;
matTensor.dtype = { kDLFloat, 64, 16 };
matTensor.shape = const_cast<int64_t*>(matShape);
matTensor.strides = const_cast<int64_t*>(matStrides);
ovstage_write_data_t xformWrite = writeData(&matTensor, OVSTAGE_SEMANTIC_MATRIX);
matrix[13] = 25.0; // the scene is Y-up: keep the Torus's authored y offset
for (int frame = 0; frame < kFrames; ++frame)
{
const ovstage_ordinal_t ordinal = static_cast<ovstage_ordinal_t>(2 + frame);
matrix[12] = 100.0 * frame / (kFrames - 1); // slide along +X: tx 0 -> 100
enq = ovstage_write_attribute(stage, torusQuery, { xform, {} }, ordinal, xformWrite, OVSTAGE_PRIM_MODE_UPSERT);
waitOp(stage, enq, "write_attribute");
ovstage_write_floor_desc_t frameFloor{};
frameFloor.ordinal = ordinal;
frameFloor.scope = OVSTAGE_SCOPE_ALL;
enq = ovstage_advance_write_floor(stage, &frameFloor);
waitOp(stage, enq, "advance_write_floor");
}
// Read back the final frame's transform (our own written column).
ovstage_ordinal_range_t range{};
range.end_ordinal = static_cast<ovstage_ordinal_t>(2 + kFrames - 1);
range.has_start_ordinal = false;
ovstage_read_handle_t read = OVSTAGE_INVALID_READ_HANDLE;
enq = ovstage_read_attributes(stage, torusQuery, &xform, 1, range, &read);
waitOp(stage, enq, "read_attributes");
ovstage_read_group_t group{};
status = ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group);
check(stage, status, "fetch_read_next");
// Single-path query -> exactly one 16-lane matrix element.
if (group.data.tensor_count != 1 || !group.data.tensors[0].data || group.data.tensors[0].ndim != 1 ||
group.data.tensors[0].shape[0] != 1 || group.data.tensors[0].dtype.lanes != 16)
{
std::fprintf(stderr, "unexpected omni:xform read layout\n");
return EXIT_FAILURE;
}
const double* m = static_cast<const double*>(group.data.tensors[0].data);
std::printf("final Torus xform translation (row [3][0:3]): %.1f %.1f %.1f\n", m[12], m[13], m[14]);
ovstage_release_group(stage, &group);
enq = ovstage_release_read(stage, read);
waitOp(stage, enq, "release_read");
4. Edit the USD Source#
Add a reference onto a prim, propagate it into the runtime table at a fresh ordinal, and re-read the prim types:
# 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");
Expected Output#
populated prim types: Xform, Mesh, Mesh
final Torus xform translation (row [3][0:3]): [100. 25. 0.]
after USD edit, prim types: Xform, Mesh, Mesh, Cube
populated prim types: Xform Mesh Mesh
final Torus xform translation (row [3][0:3]): 100.0 25.0 0.0
after USD edit, prim types: Xform Mesh Mesh Cube
Where to Go Next#
Population (USD → ovstage) — the population API in detail.
Writing Attributes — the table-write path and attribute semantics.
Reading Attributes — reading metadata and columns back.