Vehicle examples#

A car has three useful selections. RIGID_BODY addresses chassis pose and velocity; VEHICLE addresses controls and transmission; VEHICLE_WHEEL addresses wheel attachments. The chassis and vehicle selectors name the same car prim. Wheel selection names the child attachments separately. ALL rigid bodies also includes unrelated bodies, and ALL wheels includes wheels from every vehicle.

The complete samples use the mixed-drive scene in tests/data/vehicle_read_write.usda in the source distribution and explicitly select its /World/Standard car. They write half accelerator, complete one step, then read chassis position, engine angular speed and wheel angular speeds. Accelerator is a persistent input; the two angular speeds are telemetry in radians per second.

C++ using the C API#

The complete C++ source, tests/c_samples/vehicle_read_write_cpp/main.cpp, includes task completion, typed tensor access, session cleanup, instance creation and stage attachment. Each query, open, fetch, commit and release receipt is checked and waited exactly once. An accepted release transfers ownership even if execution later fails. The sample uses CPU tensors and completes its fills before committing with zero CUDA synchronization handles.

Build its tests/c_samples/vehicle_read_write_cpp/CMakeLists.txt project against an installed ovphysx SDK and ovstage. The program follows; its shared task and stage helpers are defined in tests/c_samples/common/ovstage_sample.h.

// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

// @implements REQ-CAPI-VEHICLE-001
// @covers AC-1 AC-2 AC-4

#include "ovstage_sample.h"
#include <ovphysx/ovphysx_attributes.h>

#include <cstddef>
#include <cstdio>
#include <stdexcept>

namespace
{
void awaitTask(ovphysx_instance_handle_t instance, ovphysx_enqueue_result_t task)
{
    // The shared helper reports admission/task errors while their text is alive,
    // collects this receipt exactly once, and destroys its wait-result storage.
    if (ovphysx_sample_complete_task(instance, task).status != OVPHYSX_API_SUCCESS)
        throw std::runtime_error("Vehicle sample operation failed");
}

template <std::size_t N>
ovx_string_or_token_t attribute(const char (&name)[N])
{
    return { OVX_INVALID_TOKEN, { name, N - 1 } };
}

struct Sessions
{
    ovphysx_instance_handle_t instance;
    ovphysx_query_handle_t query = 0;
    ovphysx_read_handle_t read = 0;
    ovphysx_write_handle_t write = 0;

    template <class Release>
    void release(uint64_t& handle, Release function)
    {
        if (!handle)
            return;
        const ovphysx_enqueue_result_t task = function(instance, handle);
        // Accepted release transfers ownership even if its execution later fails.
        if (task.status == OVPHYSX_API_SUCCESS)
            handle = 0;
        awaitTask(instance, task);
    }

    void close()
    {
        release(read, ovphysx_release_read);
        release(write, ovphysx_release_write);
        release(query, ovphysx_release_query);
    }

    ~Sessions()
    {
        // Try every remaining cleanup, including when an earlier release failed.
        const auto cleanup = [&](uint64_t& handle, auto function) {
            try { release(handle, function); }
            catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); }
        };
        cleanup(read, ovphysx_release_read);
        cleanup(write, ovphysx_release_write);
        cleanup(query, ovphysx_release_query);
    }
};

const DLTensor& cpuFloats(const ovstage_data_t& data)
{
    if (data.tensor_count != 1 || !data.tensors || data.index_map)
        throw std::runtime_error("Expected one dense column");
    const DLTensor& tensor = data.tensors[0];
    if (tensor.device.device_type != kDLCPU || tensor.dtype.code != kDLFloat ||
        tensor.dtype.bits != 32 || tensor.ndim != 1 || !tensor.shape ||
        tensor.shape[0] < 0 || (tensor.strides && tensor.strides[0] != 1) ||
        (tensor.shape[0] && !tensor.data))
        throw std::runtime_error("Expected contiguous CPU float32 values");
    return tensor;
}

template <std::size_t N>
void printColumn(Sessions& sessions, const char (&name)[N])
{
    const ovx_string_or_token_t attr = attribute(name);
    awaitTask(sessions.instance, ovphysx_read(sessions.instance, sessions.query, &attr, 1,
                                             &sessions.read, nullptr));
    for (;;)
    {
        const ovstage_read_group_t* group = nullptr;
        awaitTask(sessions.instance, ovphysx_fetch_read_next(sessions.instance, sessions.read, &group));
        if (!group)
            break;
        const DLTensor& tensor = cpuFloats(group->data);
        const unsigned char* bytes = static_cast<const unsigned char*>(tensor.data);
        const float* values = bytes ? reinterpret_cast<const float*>(bytes + tensor.byte_offset) : nullptr;
        for (int64_t row = 0; row < tensor.shape[0]; ++row)
        {
            std::printf("%s row %lld:", name, static_cast<long long>(row));
            for (uint16_t lane = 0; lane < tensor.dtype.lanes; ++lane)
                std::printf(" %.3f", static_cast<double>(values[row * tensor.dtype.lanes + lane]));
            std::printf("\n");
        }
    }
    sessions.release(sessions.read, ovphysx_release_read);
}

void driveOneCar(ovphysx_instance_handle_t instance)
{
    Sessions chassis{ instance }, vehicle{ instance }, wheels{ instance };
    ovphysx_query_selection_t carSelection{};
    carSelection.pattern = OVPHYSX_LITERAL("/World/Standard");
    ovphysx_query_selection_t wheelSelection{};
    wheelSelection.pattern = OVPHYSX_LITERAL("/World/Standard/**");

    ovstage_query_result_t summary{};
    // Chassis pose/velocity and whole-vehicle controls use the same car prim.
    awaitTask(instance, ovphysx_query_selection(instance, OVPHYSX_QUERY_TARGET_RIGID_BODY,
                                                &carSelection, &chassis.query, &summary));
    if (summary.total_prim_count != 1)
        throw std::runtime_error("Expected one chassis");
    awaitTask(instance, ovphysx_query_selection(instance, OVPHYSX_QUERY_TARGET_VEHICLE,
                                                &carSelection, &vehicle.query, &summary));
    if (summary.total_prim_count != 1)
        throw std::runtime_error("Expected one vehicle");
    // This independent query selects that car's wheel attachment prims.
    awaitTask(instance, ovphysx_query_selection(instance, OVPHYSX_QUERY_TARGET_VEHICLE_WHEEL,
                                                &wheelSelection, &wheels.query, &summary));
    if (summary.total_prim_count != 4)
        throw std::runtime_error("Expected four wheels");

    const ovx_string_or_token_t accelerator = attribute(OVPHYSX_ATTR_ACCELERATOR);
    awaitTask(instance, ovphysx_write(instance, vehicle.query, &accelerator, 1,
                                      &vehicle.write, nullptr));
    for (;;)
    {
        ovphysx_fetched_write_group_t fetched{};
        awaitTask(instance, ovphysx_fetch_write_next(instance, vehicle.write, &fetched));
        if (!fetched.group)
            break;
        const DLTensor& tensor = cpuFloats(fetched.group->data);
        if (tensor.dtype.lanes != 1)
            throw std::runtime_error("Expected scalar accelerator values");
        unsigned char* bytes = static_cast<unsigned char*>(tensor.data);
        float* values = bytes ? reinterpret_cast<float*>(bytes + tensor.byte_offset) : nullptr;
        for (int64_t row = 0; row < tensor.shape[0]; ++row)
            values[row] = 0.5f;
        // Host fill is finished; no CUDA producer dependency is outstanding.
        awaitTask(instance, ovphysx_commit_group(instance, vehicle.write, fetched.group, {}));
    }
    vehicle.release(vehicle.write, ovphysx_release_write);
    awaitTask(instance, ovphysx_step(instance, 1.0f / 60.0f));

    printColumn(chassis, OVPHYSX_ATTR_POSITION);
    printColumn(vehicle, OVPHYSX_ATTR_ENGINE_ROTATION_SPEED);
    printColumn(wheels, OVPHYSX_ATTR_ROTATION_SPEED);
    wheels.close();
    vehicle.close();
    chassis.close();
}
} // namespace

int main()
{
    ovphysx_instance_handle_t instance = 0;
    ovphysx_sample_stage_attachment_t attachment{};
    ovphysx_config_value_t cpuMode{};
    cpuMode.type = OVPHYSX_CONFIG_VALUE_BOOL;
    cpuMode.value.bool_value = true;
    if (ovphysx_set_global_config(OVPHYSX_GLOBAL_CONFIG_CPU_ONLY, cpuMode).status != OVPHYSX_API_SUCCESS)
        return 1;
    if (ovphysx_initialize().status != OVPHYSX_API_SUCCESS)
        return 1;
    int result = 1;
    try
    {
        if (ovphysx_create_instance(&instance).status != OVPHYSX_API_SUCCESS)
            throw std::runtime_error("Failed to create instance");
        if (!ovphysx_sample_attach_usd_with_ovstage(
                instance, OVPHYSX_TEST_DATA "/vehicle_read_write.usda", &attachment))
            throw std::runtime_error("Failed to attach vehicle scene");
        driveOneCar(instance);
        result = 0;
    }
    catch (const std::exception& error)
    {
        std::fprintf(stderr, "%s\n", error.what());
    }
    if (instance)
    {
        if (!ovphysx_sample_destroy_stage(instance, &attachment))
            result = 1;
        if (ovphysx_destroy_instance(instance).status != OVPHYSX_API_SUCCESS)
            result = 1;
        else if (attachment.stage)
            ovstage_destroy_instance(attachment.stage);
    }
    if (ovphysx_shutdown().status != OVPHYSX_API_SUCCESS)
        result = 1;
    return result;
}

Python#

The complete Python source, tests/python_samples/vehicle_read_write.py, includes attachment and cleanup, and reuses the adjacent sample’s attach_scene helper. Run it with the ovphysx and ovstage Python packages installed. print_column resolves row identities through the query’s attached Stage dictionary and uses a NumPy view of the CPU tensor for printing before releasing the read session.

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Select one car's chassis, controls, and wheels through independent queries."""

# @implements REQ-CAPI-VEHICLE-001
# @covers AC-1 AC-2 AC-4

from pathlib import Path

from ovphysx import PhysX, set_global_config
from ovphysx.types import GlobalConfigKey, SimObjectType
from session_write import attach_scene


def print_column(physx, query, attribute):
    with physx.read(query, [attribute]).wait() as result:
        while (group := result.fetch_next().wait()) is not None:
            paths = query.dictionary.get_path_strings(group.prim_list)
            paths = paths[group.prim_offset:group.prim_offset + group.prim_count]
            values = group.tensors[0].numpy()
            for path, value in zip(paths, values):
                print(f"{path} {attribute}: {value}")


def drive_one_car(physx):
    # The chassis and vehicle use the same prim; wheels use its descendants.
    with physx.query(SimObjectType.RIGID_BODY, pattern="/World/Standard").wait() as chassis, \
         physx.query(SimObjectType.VEHICLE, pattern="/World/Standard").wait() as vehicle, \
         physx.query(SimObjectType.VEHICLE_WHEEL, pattern="/World/Standard/**").wait() as wheels:
        counts = tuple(q.metadata.total_prim_count for q in (chassis, vehicle, wheels))
        if counts != (1, 1, 4):
            raise RuntimeError(f"Expected one chassis, one vehicle and four wheels, got {counts}")
        with physx.write(vehicle, ["accelerator"]).wait() as session:
            while (group := session.fetch_next().wait()) is not None:
                group.tensors[0].fill_(0.5)  # Complete CPU fill before commit.
                session.commit(group).wait()
        physx.step_sync(1.0 / 60.0)
        print_column(physx, chassis, "position")
        print_column(physx, vehicle, "engineRotationSpeed")
        print_column(physx, wheels, "rotationSpeed")
    # Collect the releases, including read storage after print_column drops its aliases.
    physx.wait_all()


def main():
    set_global_config(GlobalConfigKey.CPU_ONLY, True)
    physx = PhysX()
    stage = None
    try:
        scene = Path(__file__).resolve().parent.parent / "data" / "vehicle_read_write.usda"
        stage = attach_scene(physx, scene)
        drive_one_car(physx)
    finally:
        if stage is not None:
            physx.wait_task(physx.detach_ovstage())
            stage.destroy()
        physx.destroy()


if __name__ == "__main__":
    main()

Neither sample authors runtime control changes back into USD. See readable vehicle data and writable vehicle data for drive applicability, column types, validation and reset ordering.