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 selected unsealed state fails with
OVSTAGE_ERROR_WRITE_FLOOR_VIOLATION. Advance the write floor first (step 6).A fixed range rejects a later change to the same selected key. If a selected
(attribute, path)also has a retained change after the range end, the read fails withOVSTAGE_ERROR_OUT_OF_RANGEwhether or not that later change is sealed. Widen or rebase the range, repoll from a newer cursor, or request current state when that is what the consumer wants. Advancing the floor alone does not resolve this error.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. Fixed ranges select
the keys that changed, not historical payloads, and return
OVSTAGE_ERROR_OUT_OF_RANGE when a selected key changed again after the
range end.
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.