Attribute Reads and Writes#

Note

Python examples query, read, and write through ovstage. The renderer read/write wrappers and their destination-buffer and CUDA forms are deprecated compatibility APIs. Refer to skills/update-0_3-0_4-python/SKILL.md.

Ovstage reads and writes runtime stage attributes using DLPack tensors. The dtype and shape must match the USD attribute schema. Scalar attributes contain one value per prim. Array attributes contain variable-length values such as mesh points or relationships.

Tensor Layout#

Ovstage and C attribute tensors use DLDataType::lanes for multi-component values. NumPy backing arrays and DLPack consumers expose lane components as trailing dimensions:

USD value

Python shape

C shape and dtype

int for N prims

(N,) int32

shape=[N], {kDLInt, 32, 1}

point3f for N prims

(N, 3) float32

shape=[N], {kDLFloat, 32, 3}

matrix4d for N prims

(N, 16) float64

shape=[N], {kDLFloat, 64, 16}

4x4 transform semantic for N prims

(N, 4, 4) float64

shape=[N], {kDLFloat, 64, 16}

point3f[] with M elements

(M, 3) float32

shape=[M], {kDLFloat, 32, 3}

Reading Attributes#

read = stage.read_attributes(query, [attribute], ovstage.OrdinalRange.latest(2))
read.wait()
group = read.fetch_next()
tensor = group.dlpack(0)
values = np.from_dlpack(tensor).copy()
stage.release_group(group)
read.release().wait()
with stage.read_attributes(query, [points], ovstage.OrdinalRange.latest(1)) as read:
    group = read.fetch_next()
    values = np.from_dlpack(group.dlpack(0)).copy()
    stage.release_group(group)
// Enqueue a read for a schema-known attribute over one prim, at latest(2).
// The read handle is reserved synchronously — pass it to fetch_read_next
// once the returned op_index completes.
ovstage_ordinal_range_t range{};
range.has_start_ordinal = false;
range.end_ordinal = 2;

ovstage_read_handle_t read_handle = OVSTAGE_INVALID_READ_HANDLE;
ovstage_enqueue_result_t eq = ovstage_read_attributes(
    stage_, query_handle, &attr_token, 1, range, &read_handle);
ASSERT_EQ(eq.status, OVSTAGE_OK) << format_ovstage_last_error();
docs_wait_ovstage_no_errors(stage_, eq.op_index);

// Fetch the first (and, for a single-prim scalar read, only) group. The
// group's tensor is a DLPack view into the sealed attribute storage —
// valid until release_group.
ovstage_read_group_t group{};
ASSERT_EQ(ovstage_fetch_read_next(stage_, read_handle, OVSTAGE_TIMEOUT_INFINITE, &group),
          OVSTAGE_OK)
    << format_ovstage_last_error();
ASSERT_GT(group.data.tensor_count, 0u);
uint32_t value = *static_cast<uint32_t const*>(group.data.tensors[0].data);

// Release the fetched storage (synchronous) then the read handle (async;
// the release is per-handle-ordered so any in-flight fetch drains first).
ovstage_release_group(stage_, &group);
ovstage_release_read(stage_, read_handle);
// Array attributes are variable-length per prim. Read semantics are the
// same as for scalars — enqueue, wait, fetch — but the DLTensor's shape
// reflects the per-prim element count; for `points` (float3 array) the
// dtype.lanes carries the tuple width and the leading shape dim carries
// the element (point) count.
ovstage_ordinal_range_t range{};
range.has_start_ordinal = false;
range.end_ordinal = 1;

ovstage_read_handle_t read_handle = OVSTAGE_INVALID_READ_HANDLE;
ovstage_enqueue_result_t eq = ovstage_read_attributes(
    stage_, query_handle, &attr_token, 1, range, &read_handle);
ASSERT_EQ(eq.status, OVSTAGE_OK) << format_ovstage_last_error();
docs_wait_ovstage_no_errors(stage_, eq.op_index);

ovstage_read_group_t group{};
ASSERT_EQ(ovstage_fetch_read_next(stage_, read_handle, OVSTAGE_TIMEOUT_INFINITE, &group),
          OVSTAGE_OK)
    << format_ovstage_last_error();
ASSERT_GT(group.data.tensor_count, 0u);
ASSERT_TRUE(group.is_array);

DLTensor const& t = group.data.tensors[0];
// ovrtx-test-base-geometry.usda authors 4 float3 points on the Plane.
ASSERT_EQ(t.dtype.code, kDLFloat);
ASSERT_EQ(t.dtype.bits, 32u);
ASSERT_EQ(t.dtype.lanes, 3u);
ASSERT_EQ(t.ndim, 1);
ASSERT_EQ(t.shape[0], 4);
int64_t element_count = t.shape[0] * t.dtype.lanes;

ovstage_release_group(stage_, &group);
ovstage_release_read(stage_, read_handle);

The deprecated renderer read wrappers can write directly into caller-provided CPU or CUDA DLPack destinations:

# Pre-allocate the destination. The read writes directly into `dest`; the
# returned tensor is a handle to the same memory — both aliases are valid.
# The dtype must match how the runtime stores the attribute.
dest = np.empty((1,), dtype=np.uint32)
renderer.read_attribute(
    attribute_name="omni:rtx:rtpt:maxBounces",
    prim_paths=["/Render/Camera"],
    dest=dest,
)
# `dest` now holds the attribute value.
# Allocate a CUDA destination through Warp (any DLPack-compatible CUDA
# allocator works). The read writes directly into GPU memory; pass a CUDA
# stream handle so the read is ordered on the caller's stream.
dest = wp.empty(1, dtype=wp.uint32, device="cuda:0")
stream = wp.Stream(device=dest.device)
renderer.read_attribute(
    attribute_name="omni:rtx:rtpt:maxBounces",
    prim_paths=["/Render/Camera"],
    dest=dest,
    cuda_stream=stream.cuda_stream,
)
wp.synchronize_stream(stream)

Writing Attributes#

# point3f[] is a variable-length array of 3-component float vectors.
# Store values in a 2-D ndarray, then expose M logical elements with three
# lanes each to ovstage.
points = np.array(
    [
        [-50.0, 0.0, -50.0],
        [50.0, 0.0, -50.0],
        [-50.0, 0.0, 50.0],
        [50.0, 0.0, 50.0],
    ],
    dtype=np.float32,
)  # shape=(4, 3)
with ovstage.PathDictionary(stage) as paths:
    path_list = paths.create_path_list_from_strings(["/World/Plane"])
    with stage.query_from_path_list(path_list) as query:
        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()
        with stage.read_attributes(query, [attribute], ovstage.OrdinalRange.latest(2)) as read:
            group = read.fetch_next()
            values = np.from_dlpack(group.dlpack(0)).copy()
            stage.release_group(group)
        assert values.shape == (4, 3)
    paths.destroy_path_list(path_list)
attribute = paths.intern_token("omni:docTokens")
token_ids = np.array([paths.intern_token("sensor"), paths.intern_token("validated")], dtype=np.uint64)
stage.write_attribute(
    query,
    attribute,
    ordinal=2,
    tensors=token_ids,
    is_array=True,
    semantic=ovstage.AttributeSemantic.TOKEN_ID,
).wait()
stage.advance_write_floor(2, ovstage.Scope.ALL).wait()
// 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);

Compatibility Data Access#

Synchronous writes copy data before the call returns. Asynchronous writes can access the caller’s memory later during stream execution, so the source tensor must remain alive until the operation completes. String data supports only synchronous access.

The deprecated Python wrappers expose this through DataAccess.SYNC and DataAccess.ASYNC. C uses the access mode argument to ovrtx_write_attribute().

Type Notes#

  • Pass is_array=True to ovstage writes for USD array attributes and relationships.

  • Ovstage writes use AttributeSemantic to preserve authored interpretation. Deprecated ovrtx reads use raw storage layout and OVRTX_SEMANTIC_NONE.

  • Quaternion tensor order is (i, j, k, real) even though USDA authors values as (real, i, j, k).

  • string attributes are represented as UTF-8 byte arrays. String arrays are not supported; use token[] for string-like arrays.

  • Python ovstage code interns token and relationship values through ovstage.PathDictionary.

  • Ovstage asset values use byte rows with AttributeSemantic.ASSET_STRING. Deprecated C compatibility writes represent scalar assets as token pairs.

C Convenience Helpers#

For path, token, and transform attributes, prefer helpers in <ovrtx/ovrtx_attributes.h> where available. For token strings:

ovx_string_t prim = ovx_str("/World/Plane");
ovx_string_t purpose = ovx_str("guide");
ovrtx_enqueue_result_t eq =
    ovrtx_set_token_attributes(renderer_, &prim, 1, ovx_str("purpose"), &purpose);
ASSERT_API_SUCCESS(eq.status);
docs_wait_no_errors(renderer_, eq.op_index);

Troubleshooting#

  • Match the runtime dtype, not the Python or C default numeric type.

  • Ovstage array writes use lane-aware DLTensors and is_array=True.

  • PrimMode.UPSERT creates absent prims and updates existing prims; PrimMode.INSERT is create-only.

  • In C, binding descriptors borrow path storage. Keep the strings and arrays alive until the operation that uses the descriptor has completed.

  • Generic authored USD attributes require customLayerData.populateAllAuthoredAttributes = true on the root layer.