Basic Workflow: Attach ovstage and Step
The smallest end-to-end ovphysx workflow.
When to Use
Use this skill when a caller needs the smallest working ovphysx program, an installation smoke test, or a Python-to-C API mapping for instance creation, ovstage attachment, stepping, and cleanup.
Instructions
- Read the referenced sample for the caller's language before changing code.
- Keep the create, populate ovstage, attach, step, and destroy order shown here unless the caller authors later ovstage edits that require an update drain.
- Use Shell to run the Python sample or compile the C sample after adapting the paths.
Python
pip install ovphysx
from ovphysx import PhysX
import ovstage
physx = PhysX()
stage = ovstage.Stage("ovphysx-basic")
try:
ovstage.population.open_usd(stage, "scene.usda", ordinal=1, domains=ovstage.PopulationDomain.PHYSICS)
stage.advance_write_floor(ordinal=1).wait()
physx.attach_ovstage(stage, read_ordinal=1)
physx.step_sync(1.0 / 60.0)
finally:
physx.detach_ovstage()
stage.destroy()
physx.release()
step_sync() steps and waits in one call, so step errors surface immediately and
teardown cannot race the step. Use bare async step() + an explicit
wait_op() / wait_all() only when overlapping CPU work with the running step.
The physics-only domains mask above is fine for this skill's non-instanced
sample USD. For arbitrary content prefer ALL (see
docs/ovstage_integration.md, "Population domains") -- PHYSICS alone can
silently omit colliders under native USD scene-graph instances.
Later ovstage edits
When a running test changes the USD population after attach, use this exact handoff:
import ovstage
def drain_population_change(stage, physx, ordinal):
ovstage.population.apply_usd_changes(stage, ordinal=ordinal)
stage.advance_write_floor(ordinal=ordinal).wait()
physx.update_from_ovstage(ordinal, ordinal)
apply_usd_changes() waits for population work but does not seal the ordinal.
Do not drain the initial read_ordinal again; attach_ovstage() already parsed
it. Before a structural add or remove, destroy cached tensor bindings when
practical, then create replacements after the update completes. Do not use a
pre-update binding's membership as the test oracle.
Full sample:
samples/python_samples/hello_world.py(wheel)- Source checkout:
tests/python_samples/hello_world.py
C
The C workflow follows one ordering; keep it as-is:
ovphysx_initialize()(process-wide; no Python analogue).ovphysx_create_instance()withOVPHYSX_CREATE_ARGS_DEFAULT.- Populate an ovstage from USD, wait for the population op, advance and wait
for the write floor, then
ovphysx_attach_ovstage(handle, stage, read_ordinal). ovphysx_step_sync()— steps and waits in one call, so step errors are reported and teardown cannot race the step.- Detach and destroy the stage, then
ovphysx_destroy_instance(). ovphysx_shutdown().
This program uses only public ovphysx / ovstage API, so it builds against the SDK as shown under "Build the C sample" below:
#include <ovphysx/ovphysx.h>
#include <ovstage/ovstage.h>
#include <ovstage/ovstage_population.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
if (ovphysx_initialize().status != OVPHYSX_API_SUCCESS) {
fprintf(stderr, "Failed to initialize ovphysx\n");
return 1;
}
ovphysx_create_args create_args = OVPHYSX_CREATE_ARGS_DEFAULT;
ovphysx_handle_t handle = 0;
if (ovphysx_create_instance(&create_args, &handle).status != OVPHYSX_API_SUCCESS) {
fprintf(stderr, "Failed to create instance\n");
ovphysx_shutdown();
return 1;
}
// Create an ovstage and populate it from USD (public ovstage API).
ovstage_instance_desc_t stage_desc;
memset(&stage_desc, 0, sizeof(stage_desc));
stage_desc.name = "ovphysx-basic";
ovstage_instance_t* stage = NULL;
if (ovstage_create_instance(&stage_desc, &stage) != OVSTAGE_OK) {
fprintf(stderr, "Failed to create ovstage\n");
ovphysx_destroy_instance(handle);
ovphysx_shutdown();
return 1;
}
const char* usd_path = "scene.usda";
ovx_string_t path;
path.ptr = usd_path;
path.length = strnlen(usd_path, 4096);
const uint64_t ordinal = 1;
ovstage_population_enqueue_result_t open = ovstage_population_open_usd_from_file(
stage, path, ordinal, 0.0, OVSTAGE_POPULATION_DOMAIN_PHYSICS);
ovstage_population_op_wait_result_t open_wait;
memset(&open_wait, 0, sizeof(open_wait));
if (open.status != OVSTAGE_OK ||
ovstage_population_wait_op(stage, open.op_index, OVSTAGE_TIMEOUT_INFINITE, &open_wait) != OVSTAGE_OK) {
fprintf(stderr, "Failed to populate ovstage\n");
ovstage_destroy_instance(stage);
ovphysx_destroy_instance(handle);
ovphysx_shutdown();
return 1;
}
ovstage_write_floor_desc_t floor_desc;
memset(&floor_desc, 0, sizeof(floor_desc));
floor_desc.ordinal = ordinal;
floor_desc.scope = OVSTAGE_SCOPE_ALL;
ovstage_enqueue_result_t floor = ovstage_advance_write_floor(stage, &floor_desc);
ovstage_op_wait_result_t floor_wait;
memset(&floor_wait, 0, sizeof(floor_wait));
if (floor.status != OVSTAGE_OK ||
ovstage_wait_op(stage, floor.op_index, OVSTAGE_TIMEOUT_INFINITE, &floor_wait) != OVSTAGE_OK ||
floor_wait.error_op_id_count != 0) {
fprintf(stderr, "Failed to seal ovstage ordinal\n");
ovstage_destroy_instance(stage);
ovphysx_destroy_instance(handle);
ovphysx_shutdown();
return 1;
}
if (ovphysx_attach_ovstage(handle, stage, ordinal).status != OVPHYSX_API_SUCCESS) {
fprintf(stderr, "Failed to attach ovstage\n");
ovstage_destroy_instance(stage);
ovphysx_destroy_instance(handle);
ovphysx_shutdown();
return 1;
}
// step_sync steps and waits; a nonzero status means the step failed.
if (ovphysx_step_sync(handle, 1.0f / 60.0f).status != OVPHYSX_API_SUCCESS) {
fprintf(stderr, "Simulation step failed\n");
ovphysx_detach_ovstage(handle);
ovstage_destroy_instance(stage);
ovphysx_destroy_instance(handle);
ovphysx_shutdown();
return 1;
}
ovphysx_detach_ovstage(handle);
ovstage_destroy_instance(stage);
ovphysx_destroy_instance(handle);
ovphysx_shutdown();
return 0;
}
If you step with the async ovphysx_step() instead of ovphysx_step_sync(),
you must then call ovphysx_wait_op(handle, step_result.op_index, ...) and check
num_errors before teardown — the enqueue status alone only means accepted,
not completed.
Full sample:
samples/c_samples/hello_world_c/main.c(SDK)- Source checkout:
tests/c_samples/hello_world_c/main.c
The tested sample keeps its main() short by factoring the populate/attach and
stage teardown into local helpers (ovphysx_sample_attach_usd_with_ovstage() /
ovphysx_sample_destroy_stage() in tests/c_samples/common/ovstage_sample.h).
Those helpers are sample scaffolding, not public API — the sequence above is what
they wrap.
Build the C sample with CMake:
find_package(ovphysx REQUIRED)
add_executable(my_app main.c)
target_link_libraries(my_app PRIVATE ovphysx::ovphysx ovphysx::ovstage)
if(WIN32) # optionally copy ovphysx dlls into bin folder
ovphysx_copy_runtime_dlls(my_app)
endif()
Configure and build:
cmake -B build \
-DCMAKE_PREFIX_PATH="/path/to/ovphysx;/path/to/ovstage"
cmake --build build
For prebuilt SDKs, download OVPhysX and the matching native OVStage archive from their GitHub Releases pages and keep the extracted roots separate. Source builds fetch OVStage automatically.
Key APIs
| Python | C |
|---|---|
| (none — implicit) | ovphysx_initialize() / ovphysx_shutdown() |
PhysX() |
ovphysx_create_instance() |
physx.attach_ovstage(stage) |
ovphysx_attach_ovstage() |
physx.step_sync(dt) |
ovphysx_step_sync() |
physx.step(dt) |
ovphysx_step() |
physx.release() |
ovphysx_destroy_instance() |
The C API brackets everything with ovphysx_initialize() / ovphysx_shutdown();
Python manages that lifecycle internally, so there is no Python analogue. Use
physx.update_from_ovstage(from_ordinal, to_ordinal) /
ovphysx_update_from_ovstage() only to drain later ovstage edits after the
initial attach (not needed for this minimal workflow).
References
- Docs:
docs/tutorials/hello_world.md - Python sample:
samples/python_samples/hello_world.py(wheel; source:tests/python_samples/hello_world.py) - C sample:
samples/c_samples/hello_world_c/main.c(SDK; source:tests/c_samples/hello_world_c/main.c) - C header:
include/ovphysx/ovphysx.h