Hello World – Populate ovstage and Step#

This tutorial shows the smallest end-to-end ovphysx workflow: create an instance, populate an ovstage Stage from USD, attach it, step simulation, and clean up resources. You can use this flow as the starting point for larger integrations.

Prerequisites#

  • A USD stage to populate. This tutorial uses stages that ship with every package: links_chain_sample.usda for Python and simple_physics_scene.usda for C. They are under ovphysx/samples/data/ in the wheel, <sdk-root>/samples/data/ in the C/C++ SDK, and tests/data/ in a repository checkout. No hand-authoring is needed.

  • GPU simulation (optional): prebuilt packages require a CUDA-capable NVIDIA driver compatible with CUDA 12.8, but no CUDA Toolkit installation. Refer to the Quickstart prerequisites for the versioned corresponding-driver table and source-build requirements.

Code Language#

Python#

Install the package first:

pip install ovphysx

This complete sample creates a PhysX instance, populates an ovstage Stage from links_chain_sample.usda, runs one synchronous step, and releases the stage and the instance in lifetime-safe order:

import ovphysx
from ovphysx import PhysX
from pathlib import Path

print("Using ovphysx version: ", ovphysx.__version__)

_physx_schemas_registered = False


def attach_scene(physx, usd_path):
    import ovstage

    if not ovstage.population.available():
        raise RuntimeError("ovstage population bridge is unavailable")

    # ovphysx ships its PhysX USD schemas as codeless resources and does not register
    # them itself. Register them with ovstage once, before the first population
    # call in the process. The Newton USD schema (pip package newton-usd-schemas)
    # is registered alongside so authored newton:* attributes reach the parser.
    global _physx_schemas_registered
    if not _physx_schemas_registered:
        ovstage.population.register_usd_schemas(
            [str(ovphysx.codeless_schema_root()), str(ovphysx.newton_schema_root())]
        )
        _physx_schemas_registered = True
    stage = ovstage.Stage("ovphysx-hello-world")
    ordinal = 1
    try:
        ovstage.population.open_usd(stage, str(usd_path), ordinal=ordinal, domains=ovstage.PopulationDomain.PHYSICS)
        stage.advance_write_floor(ordinal=ordinal).wait()
        physx.attach_ovstage(stage, read_ordinal=ordinal)
        print("Loaded scene through ovstage")
        return stage
    except Exception:
        stage.destroy()
        raise

# Prefer package data so a copied sample works. Fall back to the checked-in
# sample's adjacent data directory when package data is absent.
usd_path = Path(ovphysx.__file__).resolve().parent / "samples" / "data" / "links_chain_sample.usda"
if not usd_path.is_file():
    usd_path = Path(__file__).resolve().parent.parent / "data" / "links_chain_sample.usda"
if not usd_path.is_file():
    raise FileNotFoundError(f"ovphysx sample data is missing: {usd_path}")

# Initialize PhysX
physx = PhysX()
stage = attach_scene(physx, usd_path)

try:
    # Run a simulation step
    dt = 1.0 / 60.0
    physx.step_sync(dt)
    print("Simulation step completed successfully")
finally:
    if stage is not None:
        physx.detach_ovstage()
        stage.destroy()
    physx.destroy()
    print("Cleanup complete")

The attach_scene helper registers with ovstage the codeless PhysX USD schemas that ovphysx ships (ovstage.population.register_usd_schemas() with ovphysx.codeless_schema_root()) and the separately installed Newton USD schema (pip install newton-usd-schemas, located with ovphysx.newton_schema_root()) before it creates and populates the stage; ovphysx never registers them itself, and the registration must precede the first population call in the process. The samples’ pyproject.toml declares newton-usd-schemas, so uv run resolves it; install it yourself before running a sample from a plain pip install ovphysx environment.

C#

Download the ovphysx SDK and matching native ovstage archive as described in the SDK Quickstart, and extract them as separate package roots.

CMakeLists.txt

Every C sample uses find_package(ovphysx) and links against ovphysx::ovphysx. Here is the CMakeLists.txt for hello_world_c:

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

cmake_minimum_required(VERSION 3.16)
project(HelloWorldC C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

find_package(ovphysx REQUIRED)

add_executable(hello_world_c main.c)

# Force the C language so the sample never compiles as C++.
set_source_files_properties(main.c PROPERTIES LANGUAGE C)
set_target_properties(hello_world_c PROPERTIES
    C_STANDARD 11
    C_STANDARD_REQUIRED ON
)


# Compiler-specific flags that enforce strict C compilation.
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
    # An implicit declaration error catches any C++-only symbol leaking into the C sample.
    target_compile_options(hello_world_c PRIVATE 
        -Werror=implicit-function-declaration
        -pedantic
    )
endif()

if(MSVC)
    # /TC makes MSVC compile every source as C.
    target_compile_options(hello_world_c PRIVATE /TC)
endif()

target_link_libraries(hello_world_c PRIVATE ovphysx::ovphysx ovphysx::ovstage)
target_include_directories(hello_world_c PRIVATE "${CMAKE_CURRENT_LIST_DIR}/../common")
get_filename_component(OVPHYSX_TEST_DATA_DIR "${CMAKE_CURRENT_LIST_DIR}/../../data" ABSOLUTE)
target_compile_definitions(hello_world_c PRIVATE
    OVPHYSX_TEST_DATA="${OVPHYSX_TEST_DATA_DIR}"
)
if(WIN32)
    ovphysx_copy_runtime_dlls(hello_world_c)
endif()

Build by pointing CMAKE_PREFIX_PATH at both package roots (refer to SDK Quickstart for details).

Source

The C sample performs the same sequence against simple_physics_scene.usda:

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

static int run(void)
{
  // Create a PhysX instance with the default arguments.
  ovphysx_create_args create_args = OVPHYSX_CREATE_ARGS_DEFAULT;
  ovphysx_handle_t handle = 0;
  
  ovphysx_result_t result = ovphysx_create_instance(&create_args, &handle);
  if (result.status != OVPHYSX_API_SUCCESS) {
    fprintf(stderr, "Failed to create PhysX instance\n");
  ovphysx_shutdown();
    return 1;
  }

  // Populate an ovstage instance from the USD file and attach it to ovphysx.
  ovphysx_sample_stage_attachment_t stage_attachment = {0};
  if (!ovphysx_sample_attach_usd_with_ovstage(
          handle, OVPHYSX_TEST_DATA "/simple_physics_scene.usda", &stage_attachment)) {
    fprintf(stderr, "Failed to attach ovstage scene\n");
    ovphysx_destroy_instance(handle);
  ovphysx_shutdown();
    return 1;
  }

  // Enqueue one simulation step. Stepping is asynchronous.
  ovphysx_enqueue_result_t step_result = ovphysx_step(handle, 0.016f);
  if (step_result.status != OVPHYSX_API_SUCCESS) {
    fprintf(stderr, "Failed to step simulation\n");
    ovphysx_sample_destroy_stage(handle, &stage_attachment);
    ovphysx_destroy_instance(handle);
  ovphysx_shutdown();
    return 1;
  }

  // Wait for the step to complete and check it reported no errors.
  ovphysx_op_wait_result_t step_wait_result = {0};
  ovphysx_result_t step_wait_status = ovphysx_wait_op(
      handle, step_result.op_index, OVPHYSX_TIMEOUT_INFINITE, &step_wait_result);
  int step_ok = (step_wait_status.status == OVPHYSX_API_SUCCESS && step_wait_result.num_errors == 0);
  ovphysx_destroy_wait_result(&step_wait_result);
  if (!step_ok) {
    fprintf(stderr, "Simulation step failed\n");
    ovphysx_sample_destroy_stage(handle, &stage_attachment);
    ovphysx_destroy_instance(handle);
  ovphysx_shutdown();
    return 1;
  }

  printf("Simulation step completed successfully\n");

  ovphysx_sample_destroy_stage(handle, &stage_attachment);
  ovphysx_destroy_instance(handle);
  ovphysx_shutdown();
  printf("Cleanup complete\n");

  return 0;
}

int main(void) {
    ovphysx_result_t init_r = ovphysx_initialize();
    if (init_r.status != OVPHYSX_API_SUCCESS) {
        fprintf(stderr, "ovphysx_initialize() failed\n");
        return 1;
    }
  int rc = run();
  return rc;
}

The ovphysx_sample_attach_usd_with_ovstage() helper from samples/c_samples/common/ovstage_sample.h registers the codeless PhysX USD schemas with ovstage first, passing the root from ovphysx_get_codeless_schema_root() to ovstage_population_register_usd_schemas(), and only then creates and populates the stage.

Result#

After this tutorial, you can step the simulation from both Python and C and release all resources cleanly.