Attribute Bindings#

Note

Python examples replace persistent bindings with reusable ovstage queries. Persistent renderer bindings remain deprecated compatibility APIs. Refer to skills/update-0_3-0_4-python/SKILL.md.

For repeated Python writes or maps, retain one ovstage query for the target prims and reuse it at successive ordinals. C compatibility code can retain a binding descriptor for the same purpose.

Use regular writes from Attribute Reads and Writes for one-shot edits. Use Attribute Mapping when the hot path needs zero-copy writes into ovrtx-owned buffers.

Create a Reusable Target and Write#

query = stage.query_from_path_list(path_list)
attribute = paths.intern_token("omni:xform")
matrix_dtype = ovstage.numpy_to_dldatatype(matrix.dtype, lanes=16)
matrix_tensor = ovstage.make_dltensor(matrix, dtype=matrix_dtype, shape=[1], ndim=1)
stage.write_attribute(query, attribute, ordinal=2, tensors=matrix_tensor, is_array=False).wait()
stage.advance_write_floor(2, ovstage.Scope.ALL).wait()
// The ovstage "binding" is the pair (query handle, interned attribute
// token). The query identifies the target prims; the token identifies the
// attribute. Both are reserved synchronously and reusable across many
// writes/reads until released.
path_dictionary_instance_t* pd = ovstage_get_path_dictionary(stage_);
ovx_string_t prim_path = ovx_str("/World/Plane");
ovx_primpath_list_t path_list{};
ASSERT_EQ(path_dictionary_create_path_list_from_strings(pd, &prim_path, 1, &path_list).status,
          OVX_API_SUCCESS);

ovstage_query_handle_t query_handle = OVSTAGE_INVALID_QUERY_HANDLE;
ASSERT_EQ(ovstage_query_from_path_list(stage_, path_list, &query_handle), OVSTAGE_OK)
    << format_ovstage_last_error();

ovx_string_t attr_str = ovx_str("omni:xform");
ovx_token_t attr_token{};
ASSERT_EQ(path_dictionary_create_tokens_from_strings(pd, &attr_str, 1, &attr_token).status,
          OVX_API_SUCCESS);
// Write through the (query, token) binding. omni:xform is a per-prim 4x4
// double matrix — shape=[1], lanes=16, with OVSTAGE_SEMANTIC_MATRIX.
// Translation lives in the last row (USD row-vector convention).
double matrix[16] = {
    1.0,  0.0,  0.0, 0.0,
    0.0,  1.0,  0.0, 0.0,
    0.0,  0.0,  1.0, 0.0,
    14.0, 0.0,  0.0, 1.0,
};
int64_t write_shape[1] = {1};
DLTensor write_tensor{};
write_tensor.data = matrix;
write_tensor.device = {kDLCPU, 0};
write_tensor.ndim = 1;
write_tensor.dtype = {kDLFloat, 64, 16};
write_tensor.shape = write_shape;

ovstage_write_data_t write_data{};
write_data.tensors = &write_tensor;
write_data.tensor_count = 1;
write_data.is_array = false;
write_data.semantic = OVSTAGE_SEMANTIC_MATRIX;

ovx_string_or_token_t attr_ref{};
attr_ref.token = attr_token;

ovstage_enqueue_result_t wq = ovstage_write_attribute(
    stage_, query_handle, attr_ref, /*ordinal=*/2, write_data, OVSTAGE_PRIM_MODE_UPSERT);
ASSERT_EQ(wq.status, OVSTAGE_OK) << format_ovstage_last_error();
docs_wait_ovstage_no_errors(stage_, wq.op_index);
docs_ovstage_advance_write_floor(stage_, 2);
// Release the query handle (per-handle-ordered: waits for in-flight
// reads/writes to complete) and drop the path-list refcount.
ovstage_release_query(stage_, query_handle);
path_dictionary_release_path_list_reference(pd, path_list);

Async Queries and Writes#

Ovstage query and write handles can be waited explicitly for non-blocking update pipelines.

plane_filter = ovstage.Filter([ovstage.Predicate("usd-path", ovstage.FilterOp.IN, ["/World/Plane"])])
query = stage.query(filter=plane_filter)
query.wait()
attribute = paths.intern_token("omni:xform")
matrix_dtype = ovstage.numpy_to_dldatatype(matrix.dtype, lanes=16)
matrix_tensor = ovstage.make_dltensor(matrix, dtype=matrix_dtype, shape=[1], ndim=1)
write_op = stage.write_attribute(query, attribute, ordinal=2, tensors=matrix_tensor, is_array=False)
write_op.wait()
stage.advance_write_floor(2, ovstage.Scope.ALL).wait()

Array Attributes#

Use is_array=True for variable-length USD array attributes such as mesh points.

attribute = paths.intern_token("points")
point_dtype = ovstage.numpy_to_dldatatype(points.dtype, lanes=3)
point_tensor = ovstage.make_dltensor(points, dtype=point_dtype, shape=[4], ndim=1)
stage.write_attribute(query, attribute, ordinal=2, tensors=point_tensor, is_array=True).wait()
stage.advance_write_floor(2, ovstage.Scope.ALL).wait()

Mapping Through a Query#

The same ovstage query can be reused for repeated map/unmap cycles.

query = stage.query_from_path_list(path_list)
attribute = paths.intern_token("omni:xform")
with stage.map_attribute(query, attribute, ordinal=2) as mapping:
    mapping.wait()
    group = mapping.fetch_next()
    matrices = np.from_dlpack(group.dlpack(0)).reshape(1, 4, 4)
    matrices[0, 3, 0] = 8.0
stage.advance_write_floor(2, ovstage.Scope.ALL).wait()

Lifetime Rules#

  • Release reusable ovstage queries when the hot path is done.

  • In C, keep strings and descriptor arrays alive until binding creation has completed.

  • OVRTX_BINDING_FLAG_OPTIMIZE is intended for frequent high-volume writes.