Developer Guide#
Use this guide when you integrate, build, and operate ovphysx in your own application. You learn how to build the SDK, run samples, and apply core runtime rules for synchronization, threading, and resource ownership.
Relationship and Consumption Model#
ovphysx provides a stable C API and Python bindings for ovstage-driven PhysX simulation. Use ovphysx when you need USD-based physics simulation outside of Omniverse Kit with a small, self-contained SDK.
Python:
pip install ovphysxandfrom ovphysx import PhysXC/C++: download the SDK from the GitHub Releases page, include headers from
include/ovphysx/, and link againstovphysx::ovphysxthrough CMake
CPU note (x86_64): pre-built ovphysx binaries require AVX. Missing AVX is
detected at ovphysx_initialize() with a clear error. See
System Requirements in the overview for
compatibility details and how to verify AVX on Linux x86_64.
This guide focuses on the runtime and integration surface. For authoring the USD physics content that ovphysx simulates and the concepts behind it (scenes, colliders, rigid bodies, joints, articulations, deformables, particles), refer to the Simulation Setup pages starting with Physics Scene, and the tuning Guides.
Samples and Tutorials#
The ovphysx samples are runnable references for SDK and wheel usage, designed to run in a clean environment matching how end users consume the wheel or SDK.
Note: In the source repository, samples live under
tests/c_samples/andtests/python_samples/. In the SDK package, these are installed tosamples/c_samples/andsamples/python_samples/respectively.
Python Samples (tests/python_samples/):
Sample |
Tutorial |
Feature |
|---|---|---|
|
Load USD + step (minimal workflow) |
|
|
Read/write simulation data through tensor bindings |
|
|
Read contact forces through sensor/filter bindings |
|
|
Build lightweight view wrappers on TensorBindingsAPI (advanced) |
|
|
Record physics internals to .ovd files |
|
|
Closed loop: author control into ovstage, drain it explicitly, step, read every output type back (ADR-0007) |
Extra Python Samples (tests/python_samples_extra/):
Sample |
Tutorial |
Feature |
|---|---|---|
|
Visualize rigid body simulation with Rerun |
C/C++ Samples (tests/c_samples/):
Sample |
Tutorial |
Feature |
|---|---|---|
|
Minimal C hello world |
|
|
CPU tensor read/write |
|
|
GPU tensor read/write with CUDA |
|
|
Contact force reading |
|
|
Record physics internals to .ovd files |
|
|
Direct PhysX SDK pointer access |
|
|
Closed loop: author control into ovstage, drain it explicitly, step, read every output type back (ADR-0007) |
For SDK setup, refer to SDK Quickstart.
For tensor binding shape/read/write semantics, refer to Tensor Bindings.
Canonical enum-level definitions are in include/ovphysx/ovphysx_types.h (ovphysx_tensor_type_t).
Execution Model#
ovphysx uses a stream-ordered execution model:
Calls are enqueued in submission order and observe prior writes without extra sync.
Asynchronous calls return an
op_index; wait on it before consuming results outside the stream.Synchronous calls complete before returning.
clone()retains the operation-index-shaped API and returns an already-completeop_index; waiting on it is valid and returns immediately.
Concretely, step() / ovphysx_step() is asynchronous and returns an
op_index, while step_sync() / ovphysx_step_sync() steps and waits in one
call (and step_n_sync() batches N synchronous steps). After an async step(),
in-stream ovphysx operations (tensor read() / write()) see the result without
extra synchronization; only out-of-stream consumers (external GPU work, logging,
rendering, network I/O) need an explicit wait_op() / wait_all().
Use ovphysx_wait_op() (or PhysX.wait_op() in Python) to:
synchronize before reading or modifying tensors on CPU/GPU if they’re currently accessed by asynchronous operations inside ovphysx
ensure correctness before external side-effects (logging, rendering, network I/O)
Configuration#
The SDK uses a typed config system for known settings, plus a raw Carbonite-setting override for arbitrary paths. Config entries are built
with convenience functions from ovphysx_config.h and passed at instance creation or set at runtime.
C#
#include "ovphysx/ovphysx_config.h"
// Before instance creation
ovphysx_create_args args = OVPHYSX_CREATE_ARGS_DEFAULT;
ovphysx_config_entry_t entries[] = {
ovphysx_config_entry_disable_contact_processing(true),
ovphysx_config_entry_num_threads(4),
ovphysx_config_entry_carbonite(
OVPHYSX_LITERAL("/physics/updateToUsd"),
OVPHYSX_LITERAL("false")),
};
args.config_entries = entries;
args.config_entry_count = 3;
ovphysx_initialize();
ovphysx_create_instance(&args, &handle);
// At runtime
ovphysx_set_global_config(ovphysx_config_entry_num_threads(8));
// Typed getter
int32_t threads;
ovphysx_get_global_config_int32(OVPHYSX_CONFIG_NUM_THREADS, &threads);
ovphysx_destroy_instance(handle);
ovphysx_shutdown();
Python#
from ovphysx import PhysX, PhysXConfig, ConfigBool, ConfigInt32
# At initialization
physx = PhysX(config=PhysXConfig(
disable_contact_processing=True,
num_threads=4,
carbonite_overrides={"/physics/updateToUsd": False},
))
# Typed getter
threads = physx.get_config_int32(ConfigInt32.NUM_THREADS)
enabled = physx.get_config_bool(ConfigBool.DISABLE_CONTACT_PROCESSING)
The carbonite_overrides dict accepts arbitrary Carbonite setting paths for
settings not yet covered by the typed fields. Value types are auto-detected from the
string representation. Using a carbonite_overrides key that targets a path already
covered by a typed field raises ValueError.
Note:
carbonite_overridesis write-only. There is no getter for arbitrary Carbonite paths; use the typed getters (get_config_bool,get_config_int32, etc.) for known settings.
Config is process-global (Carbonite-backed). All instances in the same process share the same config state.
Cooked-Collider Cache (UJITSO)#
Collision “cooking” converts a mesh into the representation PhysX simulates (convex hull, triangle mesh, SDF, or convex decomposition). It is CPU-expensive, so ovphysx caches the cooked result on disk and reuses it on the next run through UJITSO, a local content-addressed cache. The cache key includes the mesh geometry, the cooking parameters, the cooking type, and a cooker/PhysX version token, so a changed mesh, changed parameters, or a different PhysX build automatically re-cooks instead of returning a stale shape.
The cache directory is application-provided: ovphysx does not read environment variables and does not choose a location on your behalf. Set it to enable cross-run persistence:
import ovstage
from ovphysx import PhysX, PhysXConfig
physx = PhysX(config=PhysXConfig(cooked_collider_cache_dir="/path/to/your/cache"))
In C, set the OVPHYSX_CONFIG_COOKED_COLLIDER_CACHE_DIRECTORY string config key. The first run with
a given cache cooks and stores the result; later runs with an unchanged scene are served from the
cache without re-cooking.
Cooked colliders are not reused across runs when:
No
cooked_collider_cache_diris configured: cooking still runs, but nothing is persisted, so it re-cooks every run.ovphysx cannot create or write the configured directory: cooked colliders are not persisted across runs (a warning is logged when the directory cannot be created).
The mesh, the cooking parameters/type, or the PhysX build changed: the key differs, so the shape is re-cooked (this is correct, not a fault).
Collision cooking has been disabled (it is enabled by default).
ovphysx uses UJITSO as a local, in-process cache. (UJITSO can also operate as a distributed cache; ovphysx does not use that.) The cache is per-machine, shared only through the directory you configure, and it grows without automatic eviction – delete that directory to reclaim space. If collision cooking is enabled but the required UJITSO cache plugins are not loaded, instance creation fails fast with a clear error rather than silently cooking uncached.
Process-Global Lifecycle#
C callers can use ovphysx_initialize() to initialize process-global lifecycle
state, and ovphysx_shutdown() to shut it down. Only one initialize can be
active at a time; a second initialize before shutdown returns
OVPHYSX_API_ERROR. C callers must call ovphysx_initialize() before
ovphysx_create_instance().
ovphysx_shutdown() does not destroy live handles and does not balance
ovphysx_create_instance(). Call ovphysx_destroy_instance() for every handle
returned by ovphysx_create_instance().
Multi-Instance Support#
The SDK exposes a per-instance handle (ovphysx_handle_t) so callers can manage lifetime, error queues, and tensor/contact bindings on a per-instance basis. However, the underlying simulation state is process-global: only one ovstage-backed USD scene can be attached at a time across all instances in a process. Attaching a stage on one instance detaches whatever stage was previously attached.
Concretely, this means:
Multiple
ovphysx_handle_tvalues can coexist, but they share one simulation backend and one attached stage.Config, the hard CPU-only policy, and the attached stage are process-global. Different instances cannot use different config values or stages in the same process; outside hard CPU-only mode, CPU/GPU dynamics are authored per scene.
For isolated stages or parallel simulations, run each in a separate subprocess.
Multiple UsdPhysicsScene prims within the same loaded stage are supported. ovphysx_step() advances all scenes together; per-scene independent stepping is not currently exposed through the ovphysx C API.
In-process destroy/create cycles are supported: the Carbonite framework,
dependency plugins, static omni.physx runtime, and OmniClient remain resident
across destroy/create pairs and until process exit. Hard CPU-only mode is the
exception: as soon as ovphysx_set_cpu_mode(true) succeeds, it is sticky and
cannot be reverted in that process. Outside hard
CPU-only mode, CPU/GPU dynamics remain authored per scene in USD rather than
selected by ovphysx_create_instance(). Call ovphysx_shutdown() once when
the current process-lifecycle scope is done; it clears the
ovphysx_initialize() token but does not tear down the resident static runtime.
Operation Indices and Polling#
op_index values are single-use. A wait processes unconsumed indices in order
through the requested index: every completed or failed index it reaches is
consumed and must not be used again. A timeout leaves the first still-pending
index and later indices unconsumed. An index completed by internal stream
synchronization may be acknowledged once with wait_op; that acknowledgement
consumes it. Polling is supported by passing timeout_ns = 0 to wait_op.
Threading#
A single instance is not thread-safe. Serialize access externally.
Do not wait on the same
op_indexfrom multiple threads.Because all instances share one simulation backend and one attached stage, calling simulation APIs concurrently from different instances is not safe either. Serialize across instances as well.
Python: concurrent step() and tensor I/O#
Warning: the GIL does not protect ovphysx calls. The Python API reaches native code through ctypes. The GIL is released during those calls, so two Python threads can execute ovphysx C code at the same time. Calling
step()/step_sync()on one thread while another thread callsTensorBinding.read()/write()(or any other simulation API on the same instance) is a data race. Results range from torn reads to segfaults. This pattern is common in multi-threaded RL frameworks that dispatch simulation steps and observation collection to separate thread pools.
Serialize all simulation and tensor I/O on a single instance (for example with
a threading.Lock). If different threads participate, complete the step first
and only read or write tensors after that step is finished:
import threading
lock = threading.Lock()
def sim_thread(physx, dt):
for _ in range(1000):
with lock:
physx.step_sync(dt) # mutation completes before lock is released
def read_thread(physx, binding, buf):
for _ in range(1000):
with lock:
binding.read(buf) # no concurrent step() on another thread
t1 = threading.Thread(target=sim_thread, args=(physx, dt))
t2 = threading.Thread(target=read_thread, args=(physx, binding, buf))
t1.start()
t2.start()
When using asynchronous step(), the same rule applies: do not call
binding.read() / binding.write() concurrently with an in-flight step.
Either wait first (wait_op() / wait_all() / step_sync()), or keep all
step and tensor calls on one thread. In-stream tensor reads submitted after
step() on the same thread are stream-ordered and safe without an explicit
wait; cross-thread use always requires external serialization.
Tensor Binding Diagnostics#
Pattern bindings can intentionally match zero prims, so expected TensorAPI no-match diagnostics are quieted on the simulation view used to create that binding.
Explicit
prim_pathskeep the default error-level no-match diagnostics for typo detection.To detect partial misses programmatically, compare requested explicit paths with Python
binding.prim_pathsor Covphysx_tensor_binding_get_prim_paths()after binding creation.
Ownership and Lifetimes#
Tensor bindings, contact bindings, and attribute bindings own internal resources. They are automatically destroyed when the parent instance is destroyed through
ovphysx_destroy_instance(). Explicit per-binding destruction (ovphysx_destroy_tensor_binding,ovphysx_destroy_contact_binding) is available for releasing resources earlier.In Python, use
with physx.create_tensor_binding("/World/robot*", tensor_type=TensorType.RIGID_BODY_POSE) as binding:,with physx.create_contact_binding(["/World/sensor*"]) as binding:, or callbinding.destroy()explicitly. If garbage collection cleans up aTensorBindingorContactBindingfirst, ovphysx emitsResourceWarning. Python suppresses this warning category by default; run with-W defaultor setPYTHONWARNINGS=defaultto show it.Create tensor and contact bindings once outside simulation loops and reuse them across steps. Creating a new binding every step allocates new native resources and is slower than reusing the existing binding.
Tensor and contact bindings are views of the currently realized physics objects.
step()andstep_sync()do not invalidate them, but application-owned stage lifecycle changes do. Beforereset_stage(), before removing USD data that contains bound objects, or before replacing/reparsing the stage so bound objects are destroyed and recreated, destroy cached bindings when practical. If a stale binding survives one of those lifecycle operations, only destroy it; do not read or write through it. Create replacement bindings after the operation has completed. Do not keep cached bindings across topology changes and probe them defensively; the application knows when it changed the stage.On failure, call
ovphysx_get_last_error()on the same thread to retrieve the error string (valid until the next ovphysx call on that thread).PhysX pointers from
ovphysx_get_physx_ptr()are owned by ovphysx — do not callrelease()on them. See PhysX Pointer Interop for lifetime details.Contact report buffers from
ovphysx_get_contact_report()(and the default Pythonget_contact_report()) are zero-copy views into internal buffers — valid only until the nextstep()/step_sync()call. Accessing them after the next step is undefined behavior (silent corruption or segfault). Python: passcopy=Truetophysx.get_contact_report(copy=True)to receive Python-owned lists of dicts that are safe to retain across simulation steps. Recommended for RL training loops or any code path that holds contact data across a step.PhysX instance lifecycle (Python): use
with PhysX() as physx:or callphysx.release()explicitly to guarantee deterministic cleanup of native resources. If an instance is garbage-collected during the run (dropped mid-program rather than held to interpreter shutdown),__del__emits aResourceWarningbefore callingrelease()itself. The warning is silent by default (filtered out unlesspython -W default::ResourceWarningor a test suite captures it) and mirrors Python file-object semantics. The Python API does not register a process-exit cleanup hook, so applications must not rely on interpreter shutdown to release live instances.
Dependency Management#
The native SDK bundles its complete runtime. The Python distribution splits the
matched runtime across the ovphysx wheel and its exact ovstage wheel dependency:
Runtime dependencies are loaded from the packaged plugins directory next to the library (
_install/plugins/in the SDK,ovphysx/plugins/in the wheel).The ovphysx wheel includes the physics runtime plus the OVStage-provided OmniClient and connection libraries needed for PhysX-first startup.
The exact
ovstagewheel supplies OVStage, the resolver and its registry, and the namespaced USD runtime; the ovphysx wheel omits duplicate resolver/USD singleton binaries.Python exposes only TensorBindingsAPI; the legacy
ovphysx.tensorscompatibility layer is no longer shipped.No additional dependency fetch step is required for normal package usage.
Auto-detects library location through
getLibraryDirectory()Pre-loads shared libraries with
RTLD_GLOBALfor plugin symbol resolutionOffline-capable once the SDK or both declared wheels are installed
For source builds, namespaced monolithic USD is the only supported layout. The classic USD build switch is not present.
Error Handling#
For C:
Check
result.statuson every call.On failure, call
ovphysx_get_last_error()on the same thread to retrieve the error string (valid until the next ovphysx call on that thread).For
ovphysx_wait_op(), iterate overerror_op_indicesand callovphysx_get_last_op_error()per failed op index, then free the result withovphysx_destroy_wait_result().
For Python:
Runtime errors are raised on failed calls.
Use try/finally or context managers to ensure bindings are destroyed.
Pass
raise_if_empty=Truetocreate_tensor_binding()when a zero-count binding should be treated as a configuration error.For optional or broad queries, keep the default
raise_if_empty=Falseand checkbinding.countbefore reading.
GPU Warmup and Determinism#
Reads from GPU-backed DirectGPU bindings require a warmup step that initializes DirectGPU buffers.
This is done automatically on the first tensor operation.
If deterministic initial state matters, call ovphysx_warmup_gpu() explicitly after USD load and before the first tensor read.
Ordering constraint: all
clone()calls must happen before GPU warmup. Callingclone()afterwarmup_gpu()or after the firststep()/step_sync()/step_n_sync()would reallocate the DirectGPU buffers and silently corrupt already-initialized solver state, so the C runtime rejects this ordering withOVPHYSX_API_INVALID_ARGUMENT, surfaced in Python asRuntimeError. If you need to clone after warmup, callreset_stage()first to clear the warmup state.
GPU dynamics and DirectGPU TensorAPI are separate choices.
Process-level CPU-only mode must be set before creating any instance. To prevent any CUDA driver contact (e.g. on a CPU-only deployment):
from ovphysx import PhysX
PhysX.set_cpu_mode(True) # before the first PhysX() for the no-CUDA-touch guarantee
physx = PhysX()
This forces all PhysX scenes in the process to use CPU dynamics, overriding
any physxScene:enableGPUDynamics=true in the USD stage. The mode is sticky
for the process lifetime as soon as enabling it succeeds. For per-scene CPU
control (mixed CPU/GPU process without this flag), author each scene explicitly:
physxScene:enableGPUDynamics=false and physxScene:broadphaseType="MBP".
GPU dynamics (whether the PhysX simulation pipeline runs on GPU) is
otherwise controlled by authoring physxScene:enableGPUDynamics=true in the
USD stage – ovphysx does not read or write this setting itself.
High-throughput CUDA TensorBinding and ContactBinding views additionally require opting into DirectGPU before instance creation:
from ovphysx import PhysX, PhysXConfig
physx = PhysX(
config=PhysXConfig(
carbonite_overrides={
"/physics/suppressReadback": True,
},
),
)
Use this for IsaacLab-style tensor workloads that read state or contact tensors every step. Leave DirectGPU off for scenes that need contact modification, such as surface velocity or custom contact callbacks.
Scene Cloning#
Two paths duplicate a scene subtree for large-scale parallel simulation:
Direct ovphysx API. After an ovstage-backed scene has been attached and drained, callers may use
ovphysx_clone()(C),PhysX::clone()(C++), orPhysX.clone()(Python). Pass a source path, a list of target paths, and an optional flat array of per-target parent transforms(px, py, pz, qx, qy, qz, qw)(position + imaginary-first quaternion;NULLco-locates every clone on the source). Each transform positions a copy’s parent; every cloned body keeps its pose relative to the source’s parent, so an at-origin source lands each body exactly at its transform. Replication executes inline. C and Python return an already-complete op index (ovphysx_wait_opremains valid and returns immediately), while the C++ wrapper reports it through the optionaloutOpIndexout-param. Cloning is driven by the PhysX replicator: each target becomes a real physics object — articulations are reconstructed, not flattened — addressable by its target path. It happens in the simulation’s internal representation only; no USD prims are authored at the target paths. When one logical environment is assembled from several clone calls (a heterogeneous ClonePlan: first every environment’s robot, then every environment’s object), pass the optional per-targetenv_idswith the same ids in every call — objects that share an id share a runtime environment and keep colliding with each other while staying isolated from other environments. Withoutenv_idseach call numbers its copies afresh, so same-environment objects cloned by different calls would never collide. Seetests/python_samples/clone.pyandtests/c_samples/clone_c/main.c.Build the scene through ovstage. Apps that own the ovstage
Stagecan duplicate the source subtree withovstage.Stage.clone()(Python) orovstage_clone()(C), then drain the clone delta withupdate_from_ovstage(). This creates the corresponding physics objects, but in ovphysx 0.5 TensorBindingsAPI does not discover objects created by an ovstage clone delta. Use this path only when tensor bindings do not need to include the clones. Complete TensorBindingsAPI support is scheduled for ovstage after 0.5.
For tensor-based multi-environment workloads in 0.5, populate and attach the
source Stage, drain any later committed source edits, and use the direct
clone() API. Complete direct cloning before warmup_gpu() or the first
simulation step. Direct cloning invalidates existing tensor and contact
bindings; destroy and recreate them before use. Do not also drain a duplicate
ovstage clone delta for the same target paths; the direct clones are runtime-only
physics copies and are the copies TensorBindingsAPI should target in this
workflow.
Neither cloning nor step() implicitly drains later ovstage edits. Applications
must seal those edit ordinals and call ovphysx_update_from_ovstage() before
cloning or stepping.
Remote USD Loading#
ovstage population accepts any URI that the Omniverse Client Library supports:
Local paths:
/path/to/scene.usd(as before)Omniverse Nucleus:
omniverse://server/path/scene.usdS3 (HTTPS):
https://my-bucket.s3.us-east-1.amazonaws.com/path/scene.usdAzure Blob:
https://account.blob.core.windows.net/container/scene.usd
The client library is loaded automatically when an ovphysx instance is created. No additional setup is needed for Nucleus URIs when the server allows anonymous access.
Note: Use HTTPS S3 URLs (virtual-hosted style), not
s3://URIs. The OmniClient library resolves S3 assets through HTTPS and adds AWS authentication automatically when credentials are configured.
Configuring credentials#
Remote-asset credentials live on the Omniverse Client Library (OmniClient) — the asset resolver ovstage population uses to fetch S3 / Azure assets — not on ovphysx. The application owns population, so it configures credentials directly on OmniClient before populating ovstage from a private URL. ovphysx does not wrap this (it only consumes an already-populated Stage). The examples below use placeholder strings; do not commit real credentials in source code.
Python: use the
omni.clientS3 / Azure configuration API (e.g.omni.client.set_s3_configuration(...)/set_azure_sas_token(...)).C / C++: call OmniClient directly —
omniClientSetS3Configuration2(...)/omniClientSetAzureSASToken(...).
import ovphysx
import ovstage
# Configure credentials on the asset layer your app uses, BEFORE population, e.g.:
# import omni.client
# Configure the access key, secret, and region with omni.client's S3 API.
physx = ovphysx.PhysX()
stage = ovstage.Stage("remote-scene")
ovstage.population.open_usd(
stage,
"https://my-bucket.s3.us-east-1.amazonaws.com/scenes/robot.usd",
ordinal=1,
domains=ovstage.PopulationDomain.PHYSICS,
)
physx.attach_ovstage(stage, read_ordinal=1)
Notes#
OmniClient credentials are process-global — configure them before ovstage population of the remote URL.
OmniClient must be loaded in the process. Creating an ovphysx instance loads it from the exact matched OVStage runtime and pins it for process lifetime. A client already loaded by OVStage is reused only when its version matches; ovphysx fails closed if an incompatible foreign client is already present. An app that populates ovstage before/without ovphysx relies on OVStage’s USD runtime to bring up the resolver.
carb.omniclient.pluginis not part of the static ovphysx bootstrap path.
Scene Queries#
ovphysx provides three scene query functions covering raycast, sweep, and overlap:
Function |
Purpose |
Geometry input |
|---|---|---|
|
Cast a ray |
Origin + direction |
|
Move a shape along a direction |
Geometry desc + direction |
|
Test shape overlap at a position |
Geometry desc |
Each function accepts a mode parameter:
CLOSEST– return the single closest hit (0 or 1 result). Valid forraycastandsweeponly; overlap has no direction, so the Python API raisesValueErrorforoverlap(mode=CLOSEST). The C API treatsCLOSESTasALLfor overlap.ANY– return whether any hit exists (0 or 1, hit fields zeroed)ALL– return all hits
Geometry types for sweep/overlap:
SPHERE– radius + center positionBOX– half-extents + position + orientation quaternionSHAPE– any UsdGeomGPrim by prim path (meshes use convex approximation internally)
Hit results are stored in an internal buffer owned by the ovphysx instance, valid
until the next scene query call on the same instance. C and C++ callers holding the
raw pointer from ovphysx_raycast() / ovphysx_sweep() / ovphysx_overlap() must
copy any hits they need before issuing another query.
The Python API copies hit fields into Python-owned dicts before returning, so
retaining results from earlier raycast() / sweep() / overlap() calls across
subsequent queries is safe.
Python Example#
import ovphysx
from ovphysx import SceneQueryMode, SceneQueryGeometryType
# Raycast downward
hits = physx.raycast(
origin=[0, 10, 0],
direction=[0, -1, 0],
distance=100.0,
mode=SceneQueryMode.CLOSEST,
)
if hits:
print(f"Hit at distance {hits[0]['distance']}")
# Sweep a sphere
hits = physx.sweep(
geometry_type=SceneQueryGeometryType.SPHERE,
direction=[1, 0, 0],
distance=50.0,
radius=0.5,
position=[0, 1, 0],
)
# Overlap test
hits = physx.overlap(
geometry_type=SceneQueryGeometryType.BOX,
half_extent=[1, 1, 1],
position=[0, 0, 0],
rotation=[0, 0, 0, 1],
)
Path Encoding#
Hit results contain collision, rigid_body, and material fields as uint64-encoded
SdfPaths matching ovphysx’s internal path encoding. Consumers that need to compare
hit paths against known prims should use the same encoding.
PhysX Pointer Interop#
For advanced use cases that go beyond the TensorBindingsAPI – such as custom joint manipulation or direct body property access – ovphysx can return raw PhysX SDK object pointers by USD prim path.
void* ptr = NULL;
ovphysx_result_t r = ovphysx_get_physx_ptr(
handle, OVPHYSX_LITERAL("/World/physicsScene"), OVPHYSX_PHYSX_TYPE_SCENE, &ptr);
// Cast: physx::PxScene* scene = static_cast<physx::PxScene*>(ptr);
The ovphysx_physx_type_t enum specifies which PhysX type to look up
(scene, actor, articulation link, joint, shape, material, etc.).
The C API returns void*; the caller casts to the appropriate PhysX SDK type.
Casting requires the PhysX SDK C++ headers (e.g. PxScene.h,
PxRigidDynamic.h). The ovphysx SDK ships these headers under
include/physx/; find_package(ovphysx) sets ovphysx_PHYSX_INCLUDE_DIR
to point there. No PhysX library linking is needed.
See the PhysX Interop tutorial for a
complete sample that uses setKinematicTarget() on a PxRigidDynamic*.
The C++ experimental API provides a type-safe overload that deduces the enum from the pointer type, preventing mismatches at compile time:
physx::PxScene* scene = nullptr;
physx.getPhysXPtr("/World/physicsScene", scene); // enum auto-deduced
PhysX pointer interop is exposed only through the C and C++ APIs. The Python binding does not expose raw PhysX pointers; use the TensorBindingsAPI for Python workflows instead.
Pointer Lifetime#
Returned pointers are valid from acquisition until
ovphysx_reset_stage() or instance destruction. Calls to ovphysx_step() do
not invalidate existing pointers. Do not call
release() on returned pointers — ovphysx owns them.
Thread Safety#
PhysX APIs on returned pointers must only be called between simulation
steps — specifically after wait_op() completes for the preceding step
and before the next ovphysx_step() call. Calling PhysX APIs while a
step is in-flight is a data race.
Object-Change Notifications#
To manage cached pointers across the events that invalidate them, subscribe
to physics-object change notifications. The C API exposes
ovphysx_subscribe_object_changes() / ovphysx_unsubscribe_object_changes();
the experimental C++ wrapper returns an RAII
ovphysx::ObjectChangeSubscription. Subscribers receive three optional
callbacks:
on_object_created— fires AFTER the object exists. Safe to callovphysx_get_physx_ptr()from a deferred handler.on_object_destroyed— fires BEFORE the object is destroyed. Drop any cached pointer at this point; do not callrelease()on it.on_all_objects_destroyed— fires BEFORE a bulk teardown (e.g.ovphysx_reset_stage()). Flush the entire pointer cache; no per-object destruction events follow.
Subscriptions are process-global: a single subscription receives events
from every ovphysx instance in the process. There is no per-instance filter,
and callback payloads do not include a stage identifier — only prim_path.
Today ovphysx is single-attached-stage-per-process, so treat these callbacks
as one-stage-at-a-time and do not rely on them for cross-stage bookkeeping.
ovphysx_object_change_callbacks_t cbs = {0};
cbs.on_object_destroyed = my_on_destroyed;
cbs.on_all_objects_destroyed = my_on_all_destroyed;
cbs.user_data = my_cache;
ovphysx_subscription_id_t sub = OVPHYSX_INVALID_SUBSCRIPTION_ID;
ovphysx_subscribe_object_changes(&cbs, &sub);
ovphysx_unsubscribe_object_changes(sub);
PointerCache* cache_ptr = &cache;
auto sub = ovphysx::subscribeObjectChanges(ovphysx::ObjectChangeCallbacks{
.onDestroyed = [cache_ptr](std::string_view path, ovphysx_physx_type_t) { cache_ptr->drop(path); },
.onAllDestroyed = [cache_ptr]() { cache_ptr->clear(); },
});
// sub destructor unsubscribes automatically.
Threading: callbacks may fire from internal worker threads during
ovphysx_step(), ovphysx_step_sync(), ovphysx_clone(), or
ovphysx_reset_stage(). Do not call other ovphysx APIs from inside a
callback — defer follow-up work to after the next wait_op() /
wait_all() returns, or after the synchronous call (ovphysx_step_sync(),
ovphysx_reset_stage()) returns.
Known limitation: ovphysx_clone() does not currently emit
on_object_created notifications. Pointer caches that need to track cloned
objects must be refreshed after the synchronous clone call returns.
ovphysx_reset_stage() reliably
emits on_all_objects_destroyed and is the supported path for bulk
pointer invalidation.
Contact Data#
ovphysx provides two complementary APIs for reading contact information. Choose the one that fits your use case:
Contact Binding |
Contact Report |
|
|---|---|---|
Use when |
You need aggregate force tensors for RL rewards, safety limits, or force monitoring |
You need per-contact-point geometry (position, normal, impulse) for custom contact sensors or collision analysis |
Data shape |
|
Variable-length arrays of event headers + contact points (raw buffers) |
API style |
Create a binding, then read tensors each step |
Call once per step, receive pointers to internal buffers |
USD requirement |
No extra schema — just rigid body prims |
Prims must have |
Key functions |
|
|
Contact Binding (Aggregate Force Tensors)#
Contact bindings give you aggregate net force vectors between sets of sensor and filter bodies, delivered as DLPack tensors that work on both CPU and GPU. No extra USD schema is required.
See the Contact Binding tutorial for a full walkthrough. Key points:
Create the binding before the first step whose contacts you want to observe.
Net forces shape:
[S, 3]— one force vector per matched sensor.Force matrix shape:
[S, F, 3]— per (sensor, filter) pair.CUDA output tensors require DirectGPU TensorAPI;
physxScene:enableGPUDynamics=trueby itself only selects GPU dynamics. See GPU Warmup and Determinism.dtfor impulse-to-force conversion is taken automatically from the last step.
Contact Report (Per-Point Event Data)#
The contact report exposes the raw per-step contact events collected by ovphysx. This gives you every individual contact point with position, normal, impulse, and separation — useful for custom contact sensors, collision debugging, or building higher-level contact processing.
Prims must have PhysxContactReportAPI applied in the USD stage for contacts
to be reported.
const ovphysx_contact_event_header_t* headers = NULL;
const ovphysx_contact_point_t* data = NULL;
uint32_t num_headers = 0, num_data = 0;
// Basic contact report (headers + contact points)
ovphysx_get_contact_report(handle, &headers, &num_headers, &data, &num_data,
NULL, NULL);
// Access fields directly: headers[0].actor0, data[0].position[1], etc.
// With friction anchors
const ovphysx_friction_anchor_t* anchors = NULL;
uint32_t num_anchors = 0;
ovphysx_get_contact_report(handle, &headers, &num_headers, &data, &num_data,
&anchors, &num_anchors);
The friction anchor parameters are optional – pass NULL to skip them.
The C API returns typed struct pointers defined in ovphysx_types.h.
The C++ experimental API provides aliases (PhysX::ContactEventHeader,
PhysX::ContactPoint, PhysX::FrictionAnchor) and a typed
getContactReport() method.
Data is valid until the next simulation step. A typical usage pattern:
Apply
PhysxContactReportAPIto prims of interest.step()+wait_all().Call
get_contact_report()to read that step’s contacts.Parse event headers for contact pairs; index into contact data for per-point details.
⚠️ Lifetime hazard (default zero-copy mode). The arrays / pointers returned by
get_contact_report()withcopy=False(the C default and the Python default) are views into internal C buffers that are valid only until the nextstep()/step_sync(). After the next step the buffers may be reallocated or reused — accessing the views is undefined behavior (silent data corruption or segfault). Python cannot detect this dangling state.For RL training loops or any code path that retains contact data across a step, use
copy=Truein Python to receive Python-ownedlist[dict]for each section (safe to hold across steps, small per-step overhead).
Buffer capacity between steps. The runtime keeps reusable C-heap vectors for
contact headers, contact points, and friction anchors. Each step clears and
refills them, but capacity grows to the peak contact count seen so far while
physics scenes remain loaded. That growth is outside the Python heap and is not
visible to tracemalloc. Call reset_stage() to tear down the attached physics
session and release native buffers. reset_stage() detaches the stage;
resetSimulation() / releasePhysicsObjects() delete all PhysXScene objects
(and their ContactReport state), which releases the grown vector capacity.
In Python (zero-copy, default):
report = physx.get_contact_report()
for i in range(report["num_headers"]):
h = report["headers"][i]
print(f"pair {i}: actor0={h.actor0:#x}, {h.numContactData} points")
for j in range(report["num_points"]):
p = report["points"][j]
print(f" pos=({p.position[0]:.3f}, {p.position[1]:.3f}, {p.position[2]:.3f})")
# Must finish access before the next step — buffers will be reused.
In Python (safe across steps, copy=True):
report = physx.get_contact_report(copy=True)
physx.step_sync(dt) # buffers are reused, but report is owned
for h in report["headers"]:
print(f"actor0={h['actor0']:#x}, {h['numContactData']} points")
for p in report["points"]:
pos = p["position"]
print(f" pos=({pos[0]:.3f}, {pos[1]:.3f}, {pos[2]:.3f})")
Running With Other Carbonite Users#
ovphysx can share a process with other OV libraries that use Carbonite and the same namespaced USD runtime. For example, if another library has already loaded the namespaced USD monolith, ovphysx reuses that loaded USD library instead of loading a second copy.
ovphysx does not reuse a PhysX runtime loaded by another Carbonite user.
IPhysxSimulation is an internal direct runtime table, not a Carbonite-acquired
interface. ovphysx still uses Carbonite to load plugin binaries and dependencies,
then starts its statically linked OvruntimePhysX runtime directly. A foreign Carbonite
framework is allowed by default, but OVPHYSX_COEXIST_REFUSE=1 makes ovphysx fail
fast, and foundation-interface mismatches still fail with clear probe errors.
Compatible OV libraries can share Carbonite and namespaced USD without sharing the
PhysX runtime.
When another USD-aware subsystem, such as ovrtx, shares the same namespaced USD runtime, publish every subsystem’s schema/plugin paths before either subsystem opens a USD stage or otherwise touches the USD schema registry:
#include <stddef.h>
#include <ovphysx/ovphysx.h>
#include <ovrtx/ovrtx.h>
int main(void)
{
ovphysx_register_schema_paths();
ovrtx_register_schema_paths();
ovphysx_initialize();
ovphysx_create_args physx_args = OVPHYSX_CREATE_ARGS_DEFAULT;
ovphysx_handle_t physx = OVPHYSX_INVALID_HANDLE;
ovphysx_create_instance(&physx_args, &physx);
ovrtx_config_t rtx_config = { 0 };
ovrtx_renderer_t* renderer = NULL;
ovrtx_create_renderer(&rtx_config, &renderer);
ovrtx_destroy_renderer(renderer);
ovphysx_destroy_instance(physx);
ovphysx_shutdown();
return 0;
}
For ovphysx-only applications, this explicit call is optional:
ovphysx_create_instance() registers the same ovphysx schema path automatically.
The explicit call is for multi-subsystem processes where later initialization
order should not decide which USD schemas are visible.
Important process rules:
Sharing an already-loaded namespaced USD library is supported.
Call each subsystem’s schema-path registration function before the first USD stage or schema-registry access. Late calls cannot repair an already-populated schema registry.
Sharing a pre-loaded Carbonite PhysX runtime stack is not supported.
Kit or Isaac Sim processes that already loaded their physics extensions are not supported ovphysx embedding targets.
Device settings are process-global. If
/physics/cudaDeviceor/physics/suppressReadbackis already set to a conflicting value,ovphysx_create_instance()fails instead of silently using the wrong mode.If another Carbonite user already set the app directory, ovphysx preserves it.
Logging#
ovphysx uses Carbonite as its internal logging backend.
By default, the global log level is LogLevel.WARNING — only warnings and errors are emitted.
Controlling the Log Level#
Python#
import ovphysx
ovphysx.set_log_level(ovphysx.LogLevel.VERBOSE)
print(ovphysx.get_log_level())
C#
#include <ovphysx/ovphysx.h>
// Set before or after instance creation — applies globally to all outputs.
ovphysx_set_log_level(OVPHYSX_LOG_VERBOSE);
uint32_t current = ovphysx_get_log_level();
Custom Log Callbacks (C)#
Register one or more callbacks to receive log messages programmatically.
The caller must ensure the callback and any resources it references remain
valid until it is unregistered. If the callback and its resources naturally
outlive the process (e.g. a static function with no user_data), calling
ovphysx_unregister_log_callback() is not required. When called, it
guarantees the callback is not running on any thread and will never be
invoked again.
void my_logger(uint32_t level, const char* message, void* user_data) {
fprintf(stderr, "[%u] %s\n", level, message);
}
ovphysx_register_log_callback(my_logger, NULL);
// Run simulation here.
ovphysx_unregister_log_callback(my_logger, NULL);
// Safe to destroy any resources referenced by the callback here.
Controlling Default Console Output#
By default, Carbonite logs to the console. When a custom callback is registered
that also writes to the console, output may be doubled. Use
ovphysx_enable_default_log_output() to suppress the built-in console logger:
Python#
ovphysx.enable_python_logging()
ovphysx.enable_default_log_output(False)
C#
ovphysx_register_log_callback(my_logger, NULL);
ovphysx_enable_default_log_output(false); // only my_logger receives messages now
Python Logging Bridge#
Route native log messages into Python’s standard logging module:
import logging
import ovphysx
# Route native messages to the "ovphysx" Python logger
ovphysx.enable_python_logging()
# Add a handler to see the output
logging.getLogger("ovphysx").addHandler(logging.StreamHandler())
logging.getLogger("ovphysx").setLevel(logging.DEBUG)
# Run simulation here; native messages appear in Python logging.
ovphysx.disable_python_logging()
OmniPVD Recording#
ovphysx supports recording PhysX simulation internals to .ovd files through OmniPVD. The resulting files can be opened in any Kit application with the OmniPVD extension for frame-by-frame inspection of shapes, contacts, and solver state.
Recording is controlled by two typed config fields, both set at instance creation:
Python field |
C builder |
Description |
|---|---|---|
|
|
Directory for |
|
|
Enable recording |
Both must be set before the PhysX instance is created. The recording directory is auto-created if it does not exist.
from ovphysx import PhysX, PhysXConfig
physx = PhysX(
config=PhysXConfig(
omnipvd_ovd_recording_directory="/tmp/pvd_output",
omnipvd_output_enabled=True,
)
)
stage = ovstage.Stage("recorded-scene")
ovstage.population.open_usd(
stage, "scene.usda", ordinal=1, domains=ovstage.PopulationDomain.PHYSICS
)
physx.attach_ovstage(stage, read_ordinal=1)
for i in range(100):
physx.step_sync(1/60)
physx.detach_ovstage()
physx.release() # finalizes recording → <timestamp>_rec.ovd
stage.destroy()
No additional runtime dependencies or plugins are needed beyond the standard ovphysx SDK or wheel — the OmniPVD writer is built into the bundled PhysX runtime.
For the full tutorial with C examples and Kit inspection instructions, refer to OmniPVD Recording.
You now have the core integration rules for building, running, and operating ovphysx. For the full C API reference, refer to the C API Reference. For Python, refer to the Python API Reference.