Application Flow#
ovstage is a data stage, not a renderer. A program creates an instance,
identifies prims and attributes through the instance-owned
path dictionary, writes attribute columns at
explicit ordinals, seals them by advancing the write floor, and reads them
back. Every step rides on the same asynchronous submit/observe model: enqueues
are synchronous and return an op_index immediately, while the actual work
runs later and is observed with ovstage_wait_op or a fetch_* call.
Two cross-cutting rules govern every stage:
Asynchronous enqueue/observe — an enqueue returns an
op_indeximmediately; nothing has executed until you wait or fetch. Refer to Asynchronous Submit/Observe Model.Ordinals and the write floor — writes carry an
ordinal; advancing the write floor seals everything at or below it. Reads target sealed data at or below the write floor; queries resolve against the latest committed state.
Canonical Lifecycle#
The minimal program follows this order:
ovstage_create_instance— create the stage.ovstage_get_path_dictionary— obtain the instance-owned dictionary.Intern a token / build a prim-path list.
ovstage_query_from_path_list— open a query over those prims.ovstage_write_attribute— write a column at anordinal.ovstage_advance_write_floor— seal the ordinal so it becomes readable.ovstage_read_attributes/ovstage_fetch_read_next— read the column back.Release in reverse:
ovstage_release_group→ovstage_release_read→ release the path list →ovstage_destroy_instance.
The path dictionary is instance-owned and must never be freed by the caller.
The write → seal → read core, in both languages:
# Write one float per prim into the "temperature" column at ordinal 1,
# seal it by advancing the write floor to 1, then read it back. Tensor
# data crosses as a numpy array (CPU) via DLPack; async ops return an
# Operation whose .wait() raises OvstageError on failure. The read is
# a context manager -- block exit releases its handle even when an
# error interrupts -- and the fetched group is released in a finally.
stage.write_attribute(
query, attr, ordinal=1, tensors=np.array([1.0, 2.0, 3.0], np.float32), is_array=False
).wait()
stage.advance_write_floor(ordinal=1).wait()
with stage.read_attributes(query, [attr], OrdinalRange.latest(1)) as read:
read.wait()
group = read.fetch_next()
if group is None:
raise SystemExit("read returned no group at ordinal 1")
try:
# group.array(i) is a zero-copy numpy view of tensor i (CPU).
print("read back ordinal", group.ordinal, group.array(0)) # -> [1. 2. 3.]
finally:
stage.release_group(group)
// Write one float per prim into the "temperature" column (UPSERT creates
// the prims on first write), seal it by advancing the write floor to
// ordinal 1, then read the column back.
float values[] = { 1.0f, 2.0f, 3.0f };
int64_t shape[] = { 3 };
int64_t strides[] = { 1 };
DLTensor tensor{};
tensor.data = values;
tensor.device = { kDLCPU, 0 };
tensor.ndim = 1;
tensor.dtype = { kDLFloat, 32, 1 }; // {code, bits, lanes}
tensor.shape = shape;
tensor.strides = strides;
ovstage_write_data_t write{};
write.tensors = &tensor;
write.tensor_count = 1;
write.is_array = false;
ovstage_enqueue_result_t enq =
ovstage_write_attribute(stage, query, attrArg, /*ordinal*/ 1, write, OVSTAGE_PRIM_MODE_UPSERT);
waitOp(stage, enq, "write_attribute");
ovstage_write_floor_desc_t writeFloor{};
writeFloor.ordinal = 1;
writeFloor.scope = OVSTAGE_SCOPE_ALL;
enq = ovstage_advance_write_floor(stage, &writeFloor);
waitOp(stage, enq, "advance_write_floor");
ovstage_ordinal_range_t range{};
range.end_ordinal = 1;
range.has_start_ordinal = false;
ovstage_read_handle_t read = OVSTAGE_INVALID_READ_HANDLE;
enq = ovstage_read_attributes(stage, query, &attr, 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");
if (group.data.tensor_count != 1 || !group.data.tensors[0].data)
{
std::fprintf(stderr, "unexpected read layout\n");
return EXIT_FAILURE;
}
const float* out = static_cast<const float*>(group.data.tensors[0].data);
std::printf("read back ordinal %llu: %.1f %.1f %.1f\n", (unsigned long long)group.ordinal, out[0], out[1], out[2]);
ovstage_release_group(stage, &group); // the tensor data is only valid until the group is released
Python vs. C#
Concern |
Python |
C |
|---|---|---|
Instance Lifetime |
Managed by the |
|
Enqueue |
Methods return handle objects ( |
Functions return |
Observe |
|
|
Attribute Data |
DLPack-compatible arrays (NumPy, etc.). |
|
Errors |
Raise |
Return status codes; inspect with the diagnostics accessors. |
Common Failure Modes#
Reading an unsealed ordinal returns nothing. Advance the write floor first (step 6).
Writing at or below the write floor is rejected with
OVSTAGE_ERROR_WRITE_FLOOR_VIOLATION. Choose an ordinal above the current floor. (Advancing the floor backwards is not an error — it clamps.)
Note
This build retains only the latest committed state. The C API surface includes ordinal and retention concepts, but do not design flows that read back older ordinals.
Where to Go Next#
Asynchronous Submit/Observe Model — enqueue vs. execution, op ids, waits, and timeouts.
Path Dictionary — interning tokens, paths, and path lists.
Writing Attributes / Reading Attributes — the write and read paths in detail.
Error Handling and Diagnostics — status codes, per-op errors, and Python exceptions.
Population (USD → ovstage) — ingesting USD content into the stage.