PhysX Interop – Direct PhysX SDK Access#

This tutorial shows how to get a raw PhysX SDK pointer from ovphysx and call PhysX methods directly. The sample moves a kinematic rigid body using PxRigidDynamic::setKinematicTarget(), then reads it back through ovphysx’s session read API (ovphysx_read) to confirm the runtime observed the move.

Prerequisites#

  • Complete the Hello World tutorial.

  • Familiarity with the ovphysx C API (ovphysx_get_physx_ptr()).

  • A C++17 compiler.

Setup#

The ovphysx SDK ships PhysX headers under include/physx/. Include them in your project through the ovphysx_PHYSX_INCLUDE_DIR CMake variable (set automatically by find_package(ovphysx)). No PhysX library linking is needed.

CMakeLists.txt#

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

cmake_minimum_required(VERSION 3.16)
project(PhysxInteropCpp CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(ovphysx REQUIRED)

add_executable(physx_interop_cpp main.cpp)

target_link_libraries(physx_interop_cpp PRIVATE ovphysx::ovphysx ovphysx::ovstage)
target_include_directories(physx_interop_cpp PRIVATE "${CMAKE_CURRENT_LIST_DIR}/../common")

# PhysX SDK headers shipped with the ovphysx SDK (headers only, no library linking needed)
target_include_directories(physx_interop_cpp PRIVATE ${ovphysx_PHYSX_INCLUDE_DIR})

get_filename_component(OVPHYSX_TEST_DATA_DIR "${CMAKE_CURRENT_LIST_DIR}/../../data" ABSOLUTE)
target_compile_definitions(physx_interop_cpp PRIVATE
    OVPHYSX_TEST_DATA="${OVPHYSX_TEST_DATA_DIR}"
)
if(WIN32)
    ovphysx_copy_runtime_dlls(physx_interop_cpp)
endif()

Note: ovphysx_PHYSX_INCLUDE_DIR is set automatically by find_package(ovphysx).

Source#

#include "ovphysx/ovphysx.h"
#include "ovphysx/ovphysx_types.h"
#include "ovstage_sample.h"
#include <ovx/path_dictionary/path_dictionary.h>  // path_dictionary_* prim/token/string resolution

// PhysX SDK headers, shipped with the ovphysx SDK under include/physx/.
#include "PxRigidDynamic.h"
#include "foundation/PxTransform.h"

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>

static bool check_result(ovphysx_result_t result, const char* context) {
    if (result.status != OVPHYSX_API_SUCCESS) {
        ovphysx_string_t err = ovphysx_get_last_error();
        fprintf(stderr, "ERROR in %s: ", context);
        if (err.ptr && err.length > 0) {
            fprintf(stderr, "%.*s\n", (int)err.length, err.ptr);
        } else {
            fprintf(stderr, "status=%d\n", (int)result.status);
        }
        return false;
    }
    return true;
}

static bool wait_op(ovphysx_handle_t handle, ovphysx_op_index_t op_index, const char* context) {
    ovphysx_op_wait_result_t wait_result = {};
    ovphysx_result_t result = ovphysx_wait_op(handle, op_index, 10ULL * 1000 * 1000 * 1000, &wait_result);

    bool ok = (result.status == OVPHYSX_API_SUCCESS && wait_result.num_errors == 0);
    if (!ok) {
        fprintf(stderr, "ERROR in %s: %s\n", context,
                wait_result.num_errors > 0 ? "async operation failed" : "wait failed");
    }
    ovphysx_destroy_wait_result(&wait_result);
    return ok;
}

static ovphysx_sample_stage_attachment_t g_stage_attachment{};

static int destroy_instance_and_shutdown(ovphysx_handle_t handle) {
    ovphysx_sample_destroy_stage(handle, &g_stage_attachment);
    ovphysx_destroy_instance(handle);
    ovphysx_shutdown();
    return 1;
}

// Return the index of the read group's row whose prim path equals `target_path`, or -1 if absent.
// A fixed group stacks every prim into tensors[0] with row i owned by prims.list entry i, so this
// index also selects that prim's tensor row. The shared dictionary only resolves component tokens,
// so a full path is rebuilt by joining each prim's component strings with '/'. All handles and
// strings it returns are dictionary-owned and must not be freed. The group's own borrow keeps
// prims.list valid here, so no extra reference is taken.
static int find_prim_row(path_dictionary_instance_t* dict, const ovstage_read_group_t* rg,
                         const char* target_path) {
    const uint32_t n = rg->prims.count;
    std::vector<ovx_primpath_t> paths(n);
    size_t got = 0;
    if (path_dictionary_get_paths_from_path_list(
            dict, (ovx_primpath_list_t)rg->prims.list, rg->prims.offset, n, paths.data(), &got)
                .status != OVX_API_SUCCESS ||
        got != n)
        return -1;

    for (uint32_t i = 0; i < n; ++i) {
        ovx_token_t token_buffer[64];
        ovx_token_t* tokens_per_path[1] = { nullptr };
        size_t num_tokens[1] = { 0 };
        size_t processed = 0;
        if (path_dictionary_get_tokens_from_paths(dict, &paths[i], 1, token_buffer, 64,
                                                  tokens_per_path, num_tokens, &processed)
                    .status != OVX_API_SUCCESS ||
            processed != 1)
            continue;

        std::vector<ovx_string_t> comps(num_tokens[0]);
        if (path_dictionary_get_strings_from_tokens(dict, tokens_per_path[0], num_tokens[0], comps.data())
                .status != OVX_API_SUCCESS)
            continue;

        std::string full;
        for (size_t c = 0; c < num_tokens[0]; ++c) {
            full.push_back('/');
            full.append(comps[c].ptr, comps[c].length);
        }
        if (full == target_path)
            return (int)i;
    }
    return -1;
}

static int run(void)
{
    printf("=== ovphysx PhysX Interop (C++ API) ===\n");

    ovphysx_result_t result = ovphysx_initialize();
    if (!check_result(result, "initialize"))
        return 1;

    // 1. Create instance
    ovphysx_handle_t handle = 0;
    ovphysx_create_args args = OVPHYSX_CREATE_ARGS_DEFAULT;

    result = ovphysx_create_instance(&args, &handle);
    if (!check_result(result, "create_instance")) {
        ovphysx_shutdown();
        return 1;
    }

    printf("Instance created.\n");

    // 2. Populate ovstage from USD and attach it
    if (!ovphysx_sample_attach_usd_with_ovstage(
            handle, OVPHYSX_TEST_DATA "/simple_physics_scene.usda", &g_stage_attachment)) {
        return destroy_instance_and_shutdown(handle);
    }

    printf("USD scene loaded.\n");

    // 3. Step once to initialize physics
    ovphysx_enqueue_result_t step_result = ovphysx_step(handle, 1.0f / 60.0f);
    if (step_result.status != OVPHYSX_API_SUCCESS) {
        fprintf(stderr, "Failed to enqueue step\n");
        return destroy_instance_and_shutdown(handle);
    }
    if (!wait_op(handle, step_result.op_index, "initial step")) {
        return destroy_instance_and_shutdown(handle);
    }

    printf("Initial simulation step completed.\n");

    // 4. Get the PhysX pointer for the kinematic cube.
    //    OVPHYSX_PHYSX_TYPE_ACTOR returns either PxRigidDynamic* or PxRigidStatic*,
    //    so cast to PxRigidActor* first, then validate the concrete type.
    void* actor_ptr = nullptr;
    result = ovphysx_get_physx_ptr(
        handle, OVPHYSX_LITERAL("/World/KinematicCube"), OVPHYSX_PHYSX_TYPE_ACTOR, &actor_ptr);
    if (!check_result(result, "get_physx_ptr")) {
        return destroy_instance_and_shutdown(handle);
    }

    physx::PxRigidActor* rigid_actor = static_cast<physx::PxRigidActor*>(actor_ptr);
    physx::PxRigidDynamic* actor = rigid_actor->is<physx::PxRigidDynamic>();
    if (!actor) {
        fprintf(stderr, "ERROR: /World/KinematicCube is not a PxRigidDynamic\n");
        return destroy_instance_and_shutdown(handle);
    }
    printf("Got PxRigidDynamic* for /World/KinematicCube\n");

    // 5. Set kinematic target to move the cube from (0,2,0) to (3,2,0)
    physx::PxTransform target(physx::PxVec3(3.0f, 2.0f, 0.0f));
    actor->setKinematicTarget(target);
    printf("Set kinematic target to (3, 2, 0)\n");

    // 6. Step again so PhysX moves the kinematic body to the target
    step_result = ovphysx_step(handle, 1.0f / 60.0f);
    if (step_result.status != OVPHYSX_API_SUCCESS) {
        fprintf(stderr, "Failed to enqueue step\n");
        return destroy_instance_and_shutdown(handle);
    }
    if (!wait_op(handle, step_result.op_index, "target step")) {
        return destroy_instance_and_shutdown(handle);
    }

    // 7. Read the pose back through ovphysx, which is the point of the interop round-trip. The
    //    actor was driven with the raw PhysX pointer. Reading rigid-body positions with the session
    //    read API (ovphysx_query + ovphysx_read) confirms the ovphysx runtime observed the move.
    //    The kinematic cube is the only body driven to (3, 2, 0). The dynamic ones fall away.
    ovphysx_query_handle_t read_query = 0;
    result = ovphysx_query(handle, OVPHYSX_OBJECT_RIGID_BODY, OVPHYSX_SCOPE_ALL, &read_query);
    if (!check_result(result, "query") || read_query == 0) {
        return destroy_instance_and_shutdown(handle);
    }
    const ovx_string_or_token_t pos_attr = { 0, { OVPHYSX_ATTR_POSITION, sizeof(OVPHYSX_ATTR_POSITION) - 1 } };
    ovphysx_read_handle_t read_session = 0;
    result = ovphysx_read(handle, read_query, &pos_attr, 1, &read_session);
    if (!check_result(result, "read")) {
        return destroy_instance_and_shutdown(handle);
    }

    // The shared source dictionary that interned the group prim lists. It resolves each row's prim
    // path so that /World/KinematicCube specifically is verified, not merely some body that drifted
    // near the target. Matching on x/y alone would false-pass on a future body at the same x/y or
    // a target-z regression.
    void* dict_void = nullptr;
    if (!check_result(ovphysx_query_shared_dictionary(handle, read_query, &dict_void),
                      "query_shared_dictionary") ||
        !dict_void) {
        return destroy_instance_and_shutdown(handle);
    }
    path_dictionary_instance_t* dict = static_cast<path_dictionary_instance_t*>(dict_void);

    const float tolerance = 0.1f;
    bool found_cube = false;
    float obs_x = 0.0f, obs_y = 0.0f, obs_z = 0.0f;
    for (;;) {
        const ovstage_read_group_t* rg = nullptr;
        const ovphysx_result_t r = ovphysx_fetch_read_next(handle, read_session, &rg);
        if (r.status == OVPHYSX_API_END_OF_ITERATION)
            break;  // exhausted. The only non-failure exit
        if (r.status != OVPHYSX_API_SUCCESS) {
            fprintf(stderr, "ovphysx_fetch_read_next(RIGID_BODY) failed (status %d)\n", (int)r.status);
            return destroy_instance_and_shutdown(handle);
        }
        // A fixed rigid-body position group stacks every body into tensors[0] ([N,3]) with row i ==
        // prim i, so resolve prims.list and pick the /World/KinematicCube row exactly.
        if (!found_cube && !rg->is_delete && rg->data.tensor_count == 1 && rg->data.tensors &&
            rg->prims.list != 0) {
            const DLTensor& t = rg->data.tensors[0];
            if (t.data && t.device.device_type == kDLCPU && t.dtype.lanes == 3) {
                const int cube_row = find_prim_row(dict, rg, "/World/KinematicCube");
                if (cube_row >= 0 && cube_row < t.shape[0]) {
                    const float* p = static_cast<const float*>(t.data);
                    obs_x = p[cube_row * 3 + 0];
                    obs_y = p[cube_row * 3 + 1];
                    obs_z = p[cube_row * 3 + 2];
                    found_cube = true;
                }
            }
        }
        ovphysx_release_group(handle, read_session, rg->read_group_id);
    }
    ovphysx_release_read(handle, read_session);
    ovphysx_release_query(handle, read_query);

    if (!found_cube) {
        fprintf(stderr, "FAILED: /World/KinematicCube not present in the ovphysx_read read-back.\n");
        return destroy_instance_and_shutdown(handle);
    }
    // Check the full kinematic target driven through the raw PhysX pointer, (3, 2, 0) on every axis.
    if (!(obs_x > 3.0f - tolerance && obs_x < 3.0f + tolerance &&
          obs_y > 2.0f - tolerance && obs_y < 2.0f + tolerance &&
          obs_z > 0.0f - tolerance && obs_z < 0.0f + tolerance)) {
        fprintf(stderr, "FAILED: /World/KinematicCube read back at (%.3f, %.3f, %.3f), expected (3, 2, 0).\n",
                obs_x, obs_y, obs_z);
        return destroy_instance_and_shutdown(handle);
    }
    printf("SUCCESS: ovphysx_read observed /World/KinematicCube at (%.3f, %.3f, %.3f) -- "
           "the runtime saw the move made through the raw PhysX pointer.\n", obs_x, obs_y, obs_z);

    // 8. Cleanup
    ovphysx_sample_destroy_stage(handle, &g_stage_attachment);
    ovphysx_destroy_instance(handle);
    ovphysx_shutdown();
    printf("Cleanup complete\n");

    return 0;
}

int main(void) {
    int rc = run();
    return rc;
}

How It Works#

Before you work through these steps, confirm the three Prerequisites: the Hello World tutorial is complete, a C++17 compiler is available, and find_package(ovphysx) has set ovphysx_PHYSX_INCLUDE_DIR so the shipped PhysX headers resolve. The sample then proceeds as follows:

  1. Create instance and load USD — standard ovphysx workflow.

  2. Step once — initializes the PhysX scene so rigid body actors exist.

  3. Get pointer — ovphysx_get_physx_ptr() returns a void* for the actor’s physics-object path. The process-global PxPhysics object instead uses OVPHYSX_PHYSX_TYPE_PHYSICS with a zero-length selector ({ NULL, 0 } or { "", 0 }).

  4. Cast and validate — OVPHYSX_PHYSX_TYPE_ACTOR can return either a PxRigidDynamic* or PxRigidStatic*, so cast to PxRigidActor* first, then use is<PxRigidDynamic>() to validate the concrete type before calling setKinematicTarget().

  5. Step again — PhysX moves the kinematic body to the target pose.

  6. Verify — read rigid-body positions back through ovphysx (ovphysx_query + ovphysx_read) to confirm the runtime observed the PhysX-pointer-driven move.

Object Type Compared to PhysX Pointer#

ovphysx_get_physx_ptr() and ovphysx_get_object_type() answer different questions on the same path. The pointer API returns a live PhysX SDK object; the object-type API returns a TensorAPI classification without casting. The two taxonomies pair up: OVPHYSX_PHYSX_TYPE_JOINT with OVPHYSX_OBJECT_TYPE_JOINT, OVPHYSX_PHYSX_TYPE_CUSTOM_JOINT with OVPHYSX_OBJECT_TYPE_CUSTOM_JOINT, and OVPHYSX_PHYSX_TYPE_LINK_JOINT with OVPHYSX_OBJECT_TYPE_ARTICULATION_JOINT. Refer to ovphysx_object_type_t in the C API Reference for the full taxonomy and INVALID semantics.

Pointer Lifecycle#

  • Treat pointers as invalid after stage reset or detachment, or instance destruction.

  • ovphysx_step() does not invalidate pointers.

  • Do not call release() on returned pointers – ovphysx owns them.

  • Objects explicitly created through PxPhysics follow PhysX SDK ownership rules.

  • Use the PhysX SDK headers shipped for the same ovphysx build.

Disabling Simulation#

Do not toggle PxActorFlag::eDISABLE_SIMULATION on a pointer from ovphysx_get_physx_ptr(). Use the ovstage disableSimulation attribute (ovphysx_write() / OVPHYSX_ATTR_DISABLE_SIMULATION) instead. Refer to PhysX Pointer Interop in the developer guide.

Thread Safety#

PhysX APIs on returned pointers must only be called between simulation steps — after wait_op() completes for the preceding step and before the next ovphysx_step() call. Refer to the developer guide for details.

Result#

The sample succeeds when its final read-back line reports the kinematic body at the target position:

SUCCESS: ovphysx_read observed /World/KinematicCube at (3.000, 2.000, 0.000) -- the runtime saw the move made through the raw PhysX pointer.
Cleanup complete

A FAILED: line instead of SUCCESS: means the read-back position did not match the target the direct PhysX setKinematicTarget() call requested, and the process exits non-zero.