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_index immediately; 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:

  1. ovstage_create_instance — create the stage.

  2. ovstage_get_path_dictionary — obtain the instance-owned dictionary.

  3. Intern a token / build a prim-path list.

  4. ovstage_query_from_path_list — open a query over those prims.

  5. ovstage_write_attributewrite a column at an ordinal.

  6. ovstage_advance_write_floor — seal the ordinal so it becomes readable.

  7. ovstage_read_attributes / ovstage_fetch_read_nextread the column back.

  8. Release in reverse: ovstage_release_groupovstage_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 Stage context manager.

ovstage_create_instance / ovstage_destroy_instance.

Enqueue

Methods return handle objects (Query, Read, Map).

Functions return ovstage_enqueue_result_t (status + op_index).

Observe

.wait(timeout=...) on the handle.

ovstage_wait_op / ovstage_fetch_*.

Attribute Data

DLPack-compatible arrays (NumPy, etc.).

DLTensor payloads (refer to DLPack Tensor Exchange).

Errors

Raise ovstage.OvstageError / ovstage.OvxError.

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#