C API Practical Patterns#

The C API reference lists functions and types. This page covers the common usage patterns that make those functions safe to combine in applications.

Version and Configuration#

Build the renderer configuration from ovrtx_config_entry_t values, then pass the array to ovrtx_create_renderer().

uint32_t major = 0;
uint32_t minor = 0;
uint32_t patch = 0;
ovrtx_get_version(&major, &minor, &patch);

ovrtx_config_entry_t entries[] = {
    ovrtx_config_entry_log_level(ovx_str("info")),
    ovrtx_config_entry_sync_mode(true),
};
ovrtx_config_t config{entries, 2};

ovrtx_renderer_t* configured_renderer = nullptr;
ovrtx_result_t create_result = ovrtx_create_renderer(&config, &configured_renderer);
ASSERT_API_SUCCESS(create_result.status);
ASSERT_NE(configured_renderer, nullptr);
ovrtx_destroy_renderer(configured_renderer);

String Handling#

ovrtx C strings use ovx_string_t: a ptr and explicit length. The strings are null-terminated where practical, but C and C++ code should use the length field for printing, comparison, and copying.

The minimal C example’s error helper demonstrates safe conversion to std::string_view and length-aware printing:

template <typename ResultT>
static bool check_and_print_error(ResultT const& result,
                                  std::string_view operation) {
    if (result.status == OVRTX_API_ERROR) {
        ovx_string_t error = ovrtx_get_last_error();
        if (error.ptr && error.length > 0) {
            std::cerr << "ovrtx " << operation << " failed: "
                      << std::string_view(error.ptr, error.length) << std::endl;
        } else {
            std::cerr << "ovrtx " << operation << " failed" << std::endl;
        }
        return true;
    }
    return false;
}

Error Lifetimes#

Immediate API failures report details through ovrtx_get_last_error(). Consume or copy the returned string before the next API call on the same thread.

const void* vtable = nullptr;
ovrtx_result_t result = ovrtx_query_extension("ovrtx.docs.missing_extension", &vtable);
ASSERT_EQ(result.status, OVRTX_API_ERROR);

ovx_string_t error = ovrtx_get_last_error();
std::string message(error.ptr, error.length);

Asynchronous failures are reported when waiting. The wait result contains op ids that failed, and each id can be queried with ovrtx_get_last_op_error(). Those wait-result arrays and strings are transient and invalidated by the next wait on the same thread.

// Wait for the op. The new 0.3.0 shape of ovrtx_op_wait_result_t carries:
//   - error_op_ids[0..num_error_ops) : ids that errored since the last wait
//   - lowest_pending_op_id           : 0 if everything is resolved
// Both error_op_ids and the strings returned by ovrtx_get_last_op_error()
// are thread-local and are invalidated by the next wait on this thread.
ovrtx_op_wait_result_t wait_result{};
ovrtx_result_t r = ovrtx_wait_op(renderer_, eq.op_index, ovrtx_timeout_infinite, &wait_result);
ASSERT_API_SUCCESS(r.status);

ASSERT_GT(wait_result.num_error_ops, 0u) << "expected the failing op to be reported";

for (size_t i = 0; i < wait_result.num_error_ops; ++i) {
    ovx_string_t err = ovrtx_get_last_op_error(wait_result.error_op_ids[i]);
    // Prefer the explicit length field over the null terminator.
    ASSERT_GT(err.length, 0u);
    printf("op %llu error: %.*s\n",
           (unsigned long long)wait_result.error_op_ids[i],
           (int)err.length, err.ptr);
}

// Nothing is still in flight → lowest_pending_op_id is 0.
EXPECT_EQ(wait_result.lowest_pending_op_id, 0u);

No explicit error-release call is needed.

// In 0.3.0 the per-thread error data is transient: it lives until the next
// ovrtx_wait_op() on the same thread, which implicitly recycles it. There is
// no ovrtx_release_errors() call to make anymore.
std::string bogus = get_docs_test_data_dir() + "/another-missing-file.usda";

ovrtx_enqueue_result_t eq = ovrtx_open_usd_from_file(renderer_, {bogus.c_str(), bogus.size()});
ASSERT_API_SUCCESS(eq.status);

ovrtx_op_wait_result_t wr1{};
ASSERT_API_SUCCESS(ovrtx_wait_op(renderer_, eq.op_index, ovrtx_timeout_infinite, &wr1).status);
ASSERT_GT(wr1.num_error_ops, 0u);

// Do another wait on the same thread — this invalidates wr1's error_op_ids.
// No explicit release step is required or supported.
ovrtx_enqueue_result_t eq2 = ovrtx_reset_stage(renderer_);
ASSERT_API_SUCCESS(eq2.status);
ovrtx_op_wait_result_t wr2{};
ASSERT_API_SUCCESS(ovrtx_wait_op(renderer_, eq2.op_index, ovrtx_timeout_infinite, &wr2).status);
ASSERT_NO_OP_ERRORS(wr2);

Status Queries#

Status queries are snapshots for pending or recently completed operations. Use them for progress indicators and diagnostics. Release each successful status query with ovrtx_release_op_status().

ovrtx_op_status_t status{};
ASSERT_API_SUCCESS(ovrtx_query_op_status(renderer_, eq.op_index, &status).status);
ASSERT_EQ(status.op_id, eq.op_index);
ASSERT_TRUE(status.state == OVRTX_EVENT_PENDING ||
            status.state == OVRTX_EVENT_COMPLETED);
ASSERT_TRUE(status.progress < 0.0 || (status.progress >= 0.0 && status.progress <= 1.0));
for (size_t i = 0; i < status.counter_count; ++i) {
    ASSERT_NE(status.counters[i].name.ptr, nullptr);
    ASSERT_GT(status.counters[i].name.length, 0u);
    if (status.counters[i].total != 0u) {
        ASSERT_LE(status.counters[i].current, status.counters[i].total);
    }
}
ASSERT_API_SUCCESS(ovrtx_release_op_status(renderer_, &status).status);

Logging Callback#

The log callback is process-global. The channel filter is a comma-separated list of channel_prefix=level rules; the longest matching channel prefix wins. Accepted levels include verbose, debug, info, warn, warning, error, and fatal.

// First pass: receive all messages (NULL channel filter).
g_message_count.store(0);
ovrtx_result_t r = ovrtx_set_log_callback(OVRTX_LOG_INFO,
                                          nullptr, // NULL = all channels
                                          &count_messages,
                                          &g_message_count);
ASSERT_API_SUCCESS(r.status);

// Reset first so the renderer is in a clean state.
ovrtx_enqueue_result_t eq = ovrtx_reset_stage(renderer_);
ASSERT_API_SUCCESS(eq.status);
ovrtx_op_wait_result_t wait_result;
ASSERT_API_SUCCESS(ovrtx_wait_op(renderer_, eq.op_index, ovrtx_timeout_infinite, &wait_result).status);

do_open_usd();

// Ensure pending log messages have been delivered through the callback.
ovrtx_timeout_t flush_timeout{5'000'000'000ull};
ovrtx_flush_log(flush_timeout);

int observed_any_channel = g_message_count.load();

// Second pass: install a high default threshold and an explicit low
// threshold for a channel prefix that cannot match any real channel.
g_message_count.store(0);
std::string bogus = "this.channel.does.not.exist.42=info";
ovx_string_t filter{bogus.c_str(), bogus.size()};
r = ovrtx_set_log_callback(OVRTX_LOG_FATAL, &filter, &count_messages, &g_message_count);
ASSERT_API_SUCCESS(r.status);

// Re-run the same work and flush — the callback should not fire.
eq = ovrtx_reset_stage(renderer_);
ASSERT_API_SUCCESS(eq.status);
ASSERT_API_SUCCESS(ovrtx_wait_op(renderer_, eq.op_index, ovrtx_timeout_infinite, &wait_result).status);

do_open_usd();
ovrtx_flush_log(flush_timeout);

int observed_bogus_filter = g_message_count.load();

// Disable the callback before the renderer is torn down.
ovrtx_set_log_callback(OVRTX_LOG_INFO, nullptr, nullptr, nullptr);

Resource Cleanup#