Changelog#
All notable changes to ovphysx are documented in this file.
[0.6.3] - Date 2026-09-10#
Added#
ovphysx.utilsis now a USD authoring package. It was a single module holdingstep_and_write_to_ovstage(); it is now a package of USD authoring helpers for shapes, meshes, joints, materials, planes, particles, deformables, collision filtering, transforms, paths and codeless schema access. Names stay flat, soovphysx.utils.add_rigid_boxandovphysx.utils.shapes.add_rigid_boxare the same function, andstep_and_write_to_ovstageis still importable fromovphysx.utils. The package needs onlypxrand the standard library and loads no native library. One deliberate behavior change: the sixadd_rigid_*helpers now always author a rigid body, including atdensity=0.0; use the matchingadd_collider_*helper for a static collider.ovphysx_get_object_type()classifies standalone and custom joints. A maximal-coordinate joint between plain rigid bodies, and a plugin-registered custom joint, simulated correctly but reportedOVPHYSX_OBJECT_TYPE_INVALID– the same value as a path with no object at all – which made the call useless for identity or existence checks. They now report the newOVPHYSX_OBJECT_TYPE_JOINT/ObjectType.JOINT(6) andOVPHYSX_OBJECT_TYPE_CUSTOM_JOINT/ObjectType.CUSTOM_JOINT(7). Articulation joints keep reportingARTICULATION_JOINT(5).INVALIDnow means only that no classified simulation object lives at the path.A physics population contract, with generated builders and per-component documentation. The contract records, for every USD prim type and applied API schema that physics reads, the exact ovstage encoding of each column, its raw USD fallback, what population writes, what the parser assumes when the column is absent, and how the column shows up in the PhysX objects the runtime creates. It covers 19 prim types, 31 API schemas and 292 columns. A Warp-based
ovphysx.populationPython module and a header-only C++17 builder (ovphysx/population/Population.hpp) are generated from it, so an application can build a populated stage without hand-encoding column names and types. One documentation page per component ships under Physics Schemas. No runtime behavior changes.Read and write data-contract documentation. New pages describe the
ovphysx_read()/ovphysx_write()data model, the readable and writable attribute sets, device placement, and the known limitations. The ovstage integration guide now links to them instead of carrying its own partial copy.OvStageOutputCachereduces the cost of publishing output every frame.step_and_write_to_ovstage()reads the current transform and point-instancer values on every call by default. Passing a cache (with OvStageOutputCache(physx) as cache:, thenstep_and_write_to_ovstage(..., cache=cache)) lets it own copies and reuse its CPU and CUDA output buffers across calls. Callcache.refresh()after a transform, point-instancer pose-array or topology change; the cache is bound to one ovstage attachment and cannot survive a detach and reattach.
Changed#
step_and_write_to_ovstage()publishes world transforms, not shadow pose attributes. Poses used to land onsim:<name>shadow attributes that scene consumers did not read. For fixed rigid bodies, articulation links and vehicle wheels the helper now reads the currentomni:fabric:worldMatrix, keeps its shear-free signed scale, and writes back a reconstructed world matrix; PhysX pose output itself carries no scale, so scale stays ovstage state. It never writesomni:xformand never changesomni:resetXformStack. Rigid-body point instancers get their nativepositionsandorientationsarrays written instead, preserving unsimulated slots and authored trailing rows;scalesand prototype indices are untouched. Every other emitted attribute still goes to itssim:<name>shadow attribute. The sampled world matrix must already be current, so compute the hierarchy and advance its write floor before calling the helper. This helper does not make its output the prim’s new local transform, and does not propagate to descendants.ovphysx_attach_ovstage()refuses a stage populated without the PhysX USD schemas. ovstage drops applied API schemas it cannot resolve, so a stage populated beforeovstage.population.register_usd_schemas([str(ovphysx.codeless_schema_root())])silently lost everyPhysx*setting – self-collision flags, joint velocity limits and the rest. Simulation then diverged on configurations that depend on them, which could crash the process inside the first step. Attach now verifies the registration and fails with an error naming the missing call, before the runtime is touched. The refusal is sticky for the process: registering late fixes the plugin list but not USD’s already-built schema registry. Your application still owns the registration; ovphysx only checks it. Flows that already register are unchanged.The wheel and SDK no longer contain OmniClient. ovphysx stopped copying OmniClient, the
omniverse_connectionlibrary and their provenance file into its payloads, and no longer preloads OmniClient or rejects a mismatched version atPhysX()construction. OmniClient, the USD resolver and the USD runtime belong to the ovstage distribution your application supplies. Local USD population, attach and simulation are unaffected in either startup order.The retired Fabric GPU plugins are gone from the package. The unused
omni.cubric.pluginandomni.gpucompute-cuda.plugindependencies and theplugins/gpusearch path are removed. PhysX GPU simulation is unchanged and still runs fromPhysXGpu_64in the flatplugins/directory.The bundled PhysX SDK moves to a build from the 0.6 release line. It picks up solver batching fixes, better load balancing in the CPU-only articulation code, and a bounds check on the contact-manager index during narrowphase refresh that could otherwise fault.
A path pattern component longer than 4096 characters is now rejected. Every entry point that takes a path pattern – tensor binding
patternandprim_paths, SDF viewpattern, contact bindingsensor_patternsandfilter_patterns– returnsOVPHYSX_API_INVALID_ARGUMENT(RuntimeErrorin Python) instead of accepting the input. Components are delimited the way the runtime tokenizes them, at/outside balanced parentheses. Real prim names and alternation lists stay far below the bound; a caller with a very long explicit list should pass literal paths.Source builds fetch the pinned ovstage wheel from public PyPI. The pinned version does not change, and the wheels are the same builds that were served internally. This affects only how a source build resolves its dependency.
A source build needs CMake 3.22 or newer. The README, the local development guide and the source-link tutorial said 3.16, but the runtime the build pulls in has required 3.22 for some time. The
find_package(ovphysx)flows against an installed SDK are unaffected and still work at 3.16.
Performance#
An incremental ovstage spawn costs a handful of round trips instead of hundreds. Creating and draining one rigid body used to rebuild the whole scan context from cold on every drain – whole-stage schema probes, a per-family stage query, a live ancestor walk for material bindings, and a re-intern of every well-known token – so the cost was fixed per drain rather than proportional to what changed. The incremental scan now runs on the warm attached source, answers family and column probes from one cached stage vocabulary, prefetches bodies, shapes and ancestors once per spawn and reuses that window for the mass update and object creation. Simulation results are unchanged.
Repeated ovstage reads and writes no longer evict each other’s row uploads. The rigid view’s ovstage row-list device cache held one slot, so a read/write loop whose row lists differ re-uploaded on every access. It is now a small LRU, and a stable read keeps its upload while a changing list cycles through the spare slots.
Attaching a scene with render-only scene-graph instances no longer runs reverse instancing queries during cooking. The lookup is needed only outside the cooking pass; keeping it out removes a query per prototype from attach on scenes whose instances carry no physics.
Fixed#
Instance-proxy colliders keep their authored collision settings. ovstage instance-proxy rows carry the logical collider path, but some collision values live only on the prototype’s backing row. The runtime read the logical row alone, so an explicitly disabled collider came back enabled from the default value and authored contact and rest offsets were lost – self collision then made colliders interact that were meant to be off. Collision enablement, the scalar
PhysxCollisionAPIvalues and the contact margin and gap values now fall back to the nearest instance-root prototype backing when the logical row has no value. Logical identity, transforms, material bindings, relationships, instance overrides and value blocks stay authoritative. This applies at initial population and attach; it does not add live fan-out of prototype edits.A very long path-pattern component no longer kills the process. On Linux, a single
/-separated component longer than roughly 58K characters overflowed the thread stack inside the regular-expression compiler and the process died with no exception and no log line. The matcher now bounds the token it compiles and warns instead, and the public entry points reject the input outright (see Changed). Windows was never affected.A read taken right after a write no longer returns another scene’s prim paths. The rigid read cache is keyed by the scene pointer and shared with the write path. A write refreshed the entry’s keys but left its resolved path handles in place, so when a new scene reused a destroyed scene’s address with the same body count, the following read served the old scene’s paths. A write that changes the key set now drops the stale handles; a steady-state write over an unchanged key set keeps its warm ones.
The public source drop configures with the documented CMake version.
CMakePresets.jsondeclared preset schema version 6, which only CMake 3.25 and newer parse, so the documented preset flow failed on stock CMake 3.22 with anUnrecognized "version" fielderror that named neither CMake nor a version. The file uses nothing newer than schema 3 and now declares 3.The public source drop no longer advertises
-n/--no-docker. Nothing in the drop read the variable behind that flag, so passing it did nothing and omitting it started no container. A developer who trusted the help text built against the host’s glibc and was then rejected by the ABI check. The flag and the Docker claim are gone from the public tree, and the README states plainly that meeting the glibc 2.35 baseline is the build environment’s job.The public source drop ships
python/uv.lock. Its own documented entry point,cmake -P scripts/validate_all.cmake, needs that lock for the frozenuv syncits type-check step runs; without it the step silently skipped, so the stub tree was never type-checked outside this repository.
[0.6.2] - Date 2026-09-05#
ovphysx_read_raw_contact_data()changes its tensor count from seven to six. The four per-contact value tensors (force, point, normal, separation) are unchanged, but thecounts/start_indices/sensor_actor_ids/other_actor_idstensors are replaced by two paired tensors:sensor_layout([S, 2]: contact count, start index) andactor_ids([C, 2]: reporting sensor’s actor, other actor). Update call sites to the new signature; there is no versioned alternate symbol.
Added#
Auto deformable attachments now work without a live USD stage. A
PhysxAutoDeformableAttachmentAPIprim produced nothing on an ovstage attach with no backing USD stage, because the runtime only generated its attachment sub prims by authoring them into USD. The runtime now builds and keeps that set in memory when it cannot author, so the attachment simulates on both the USD and USD-free arms.
Changed#
The Python test run resolves USD from stock pip
usd-core. Source builds no longer fetch an internal USD package for the Python tests; they install stockusd-corefrom PyPI instead, the only supported source. ovphysx does not test OmniClient or remote-USD loading (the application owns USD and its resolver). On linux aarch64 (no PyPIusd-corewheel) the two mass-unit doc-contract checks skip. This changes only the repo’s own test/dev flow; the shipped SDK and wheel carry no USD either way.The pinned ovstage runtime moves to
0.2.0.377349.ovphysx, including the bundled PhysX SDK, is now licensed under the Apache License 2.0, replacing BSD-3-Clause. There is no source, ABI, or behavior change; the updated
LICENSE.txtships in the wheel and SDK package.ovphysx_read_raw_contact_data()reports which actor pair produced each contact, in six tensors instead of seven. See the breaking-change note above for the new shape. Resolve an id withget_other_actor_paths_from_ids(); a removed actor now resolves to an empty path instead of a stale one.
Performance#
ovstage change-feed drains scale with what changed, not with stage or scene size. Reading and applying value, velocity, and transform updates from ovstage after a step, and the per-spawn/despawn cost as a scene grows, both used to carry fixed per-drain costs that grew with scene size. Measured scenarios show drain cost dropping roughly 3-5x, with the worst-case teleport and velocity-update paths dropping by more than an order of magnitude; population growth and shrink are also faster. No API or behavior change.
Fixed#
A GPU write to ovstage no longer intermittently poisons the CUDA context. Writing rigid-body pose, velocity, or wrench data through DirectGPU could race an internal index build against PhysX’s read of that index, which occasionally produced an illegal memory access that aborted the CUDA context for the whole process. The write path now waits for the index build to finish before PhysX reads it.
A read of a GPU-resident tensor binding no longer faults when
active_cuda_gpusselects a non-default device. The read staged through a buffer allocated and freed on whatever CUDA context happened to be current on the calling thread, which could be the wrong device once a non-zero ordinal was selected. Reads and writes now stage in the binding’s own CUDA context.Disabling one rigid body on DirectGPU no longer blinds ovstage reads and writes for the whole scene. The shared read view used to be invalidated whenever any body was disabled, taking every other body’s ovstage I/O down with it. Disabled bodies are now tracked without dropping the shared view, and
disableSimulation = 0still re-enables normally.update_from_ovstage()now applies rigid body, vehicle wheel, and tendon changes through the same write path asovphysx_write(). The previous per-object update loop was inert for several of these properties on a DirectGPU scene, so values written to ovstage did not reach physics. The change also turns a drain into one vectorized publish instead of one call per object.A cooking crash from a released CUDA context is fixed. Cooking on the GPU could crash with a pure-virtual call if the host released and recreated its CUDA context while a cook was still using the old one. Cooking now holds its own reference to the context for as long as it needs it.
A process that never calls
PhysX.destroy()no longer leaves cooked-collider cache directories behind. Exiting the interpreter without an explicitdestroy()skipped the shutdown path that removes the process-private cache from the temp directory. A Python exit handler now runs that cleanup on normal exit and most interrupted exits; abrupt termination such asSIGKILLremains out of scope.Rigid-body and articulation tensor views no longer warn about their own matches. A path pattern that also matches a same-named object of a different type still produces one aggregate “no match” diagnostic when nothing valid is found; it no longer additionally warns once per wrong-type candidate along the way. Valid matches are unchanged.
The
clone.py,tensor_bindings.py, andomnipvd_recording.pytutorial snippets now find their bundled USD scenes when copied into another project. They resolved scenes relative to the running script’s own directory, which only worked inside the installed samples tree. They now resolve relative to the installedovphysxpackage.The cloning tutorial and samples now document and demonstrate CPU clone collision isolation.
PhysX.clone()/ovphysx_clone()isolate cloned environments from each other’s collisions only under GPU dynamics with GPU broadphase; on CPU, co-located clones share one collision space and can shove each other apart. This is now documented at the API and in the tutorial, and the shipped samples space clones apart instead of stacking them.Docs and the
tensor-bindings-gpuskill no longer claim GPU dynamics needs authoring.physxScene:enableGPUDynamicsdefaults totruein the PhysX schema, so a scene that never authors it already runs GPU dynamics; two docs said otherwise.
[0.6.1] - Date 2026-09-03#
Added#
Writability is queryable.
ovphysx_writability(object_type, attribute, &out)reports whetherovphysx_write()accepts a pair, asWRITABLE,CONDITIONAL,WRITE_ONLY,READ_ONLYorUNCLASSIFIED.CONDITIONALmeans writable only under a condition:jointLimitis refused on a free axis. It needs no instance, scene or step. Anobject_typeoutsideovphysx_sim_object_type_treturnsINVALID_ARGUMENT, notUNCLASSIFIED.ovphysx_get_tensor_binding_native_device()reports a binding’s device. A binding exposed its dtype and shape but not whether its storage is on the CPU or a CUDA device, so callers had to reproduce ovphysx’s placement rules to allocate a tensor. Experimental C++ and Python wrappers are included. The tensor-binding API is deprecated (see Deprecated); preferovphysx_read()andovphysx_write().Five hidden benchmark rows. The opt-in C++ suite gains
WriteScalingHighN.velocity_{ovstage,tensor}_{8192,16384}_cpuandRuntimeSpawnScaling.collider_heavy_1280_cpu. They run only when selected explicitly with--hidden, and are diagnostics with no pass/fail threshold.
Performance#
Attaching an ovstage scene is now faster than loading the same scene from USD. On a 132k-joint tracked-vehicle scene attach went from 68.6 s to about 7 s, and on a 512-environment instanced robot scene from 48.6 s to 4.3 s. USD load is not regressed and the resulting physics is unchanged. One upgrade cost: a multi-material mesh cooked as convex, decomposition or sphere fill gets a new cooking key, so it cooks once more on first use.
Attach cost no longer scales with scene content that physics does not use. Attach scanned the whole stage even when few prims carried physics. On a benchmark holding five rigid bodies inside a large render-only scene it went from about 990 ms to about 220 ms. Scenes that are all physics are unaffected.
The ovstage GPU output read reuses its device buffers. On the DirectGPU path each read allocated and freed its device and staging buffers, which dominated the per-read cost of a loop that reads the same columns every step. Buffers are now pooled and reused.
PhysXConfig(ovstage_read_pool_max_mb=...)/ovphysx_config_entry_ovstage_read_pool_max_mb()(carbonite/physics/ovstageReadPoolMaxMB) caps what is retained; default 256 MiB,0or negative disables pooling. It bounds retained memory, not a single read’s peak.
Changed#
ovphysx has exactly one build, and it is USD-free. The former USD-linked variant and its
OVPHYSX_NO_USDoption are gone, so the shipped configuration is now the one the unit tests exercise. Public APIs and physics behavior are unchanged, with one exception: the experimental Mineways voxel map (InfiniteVoxelMapAPI) is no longer supported. Prims applying it are ignored with a warning, andIPhysx::setVoxelRangereturnsfalse.Every Python write tensor is a
warp.array. Host columns used to come back as NumPy views; now every group tensor is a Warp array on its native CPU or CUDA device. A non-empty tensor is still a mutable alias of runtime storage – fill it in place, then commit – while an empty one is a Warp-owned empty array. Each write tensor also reports its own residency, which can differ from the matching read when the write stages through the host on a GPU scene. The C ABI and device placement are unchanged.Your application must register the PhysX USD schemas itself. The pinned ovstage runtime no longer bundles or self-registers them. Call
ovstage_population_register_usd_schemas()(Python:ovstage.population.register_usd_schemas([str(ovphysx.codeless_schema_root())])) before the first population call, or the scene parses as if no physics schema had been applied. Scenes in non-default units keep their scaling either way.The pinned ovstage runtime moves to
0.2.0.375783. ovphysx binds to one ovstage release’s ABI; the wheel installs that exact version.SDK packaging rejects a mismatched USD build. The USD build ovphysx was linked against is recorded in the installed
config.tomlasbuild_package, and packaging now fails if the OVStage runtime supplies a different USD kit build. Two kit builds of one USD version share a library name, so the old filename check could not tell them apart.CPU-only tensor bindings require host buffers. The documented contract was not explicit: these bindings need
kDLCPUorkDLCUDAHostsource, destination, index and mask buffers even on a GPU simulation, and rejectkDLCUDAandkDLCUDAManagedinstead of staging them to the host. Documentation only; behavior is unchanged.
Fixed#
A
find_package(ovphysx)consumer no longer fails to start withlibovstage.so: cannot open shared object file. On Linux the installed target emitted its rpath asDT_RUNPATH, which the loader does not apply to a dependency’s own dependencies. It now links with--disable-new-dtags, fixing every consumer, not only the samples.Linear BasisCurves work as surface-velocity curves.
PhysxSplinesSurfaceVelocityAPIignored the BasisCurvestypeand treated every curve as cubic, so atype = linearconveyor path of three points was rejected for too few control points and longer polylines were smoothed off the authored path. Linear curves now follow the polyline exactly. Two-point curves of any basis are fixed too. A rejected curve names the cause – no target, target not in the attached stage, or target not a BasisCurves – instead of one generic message.A vehicle is no longer created without its brakes. Under the ovstage backend a sub-schema applied through an unqualified multi-apply name went undetected.
OmniPhysicsDeformablePoseAPIwas missed the same way and now applies.A tendon whose attributes are all left at their defaults is no longer dropped. The scan looked for authored attributes instead of the applied schema. Vehicle context, friction tables and shared components were skipped by the same kind of check.
Deprecated#
The tensor-binding API is deprecated in favor of read and write sessions. The seven C entry points, the C++
ovphysx::TensorBindingandcreateTensorBinding, and the PythonTensorBindingandcreate_tensor_binding()are marked deprecated, and the Python ones raiseDeprecationWarning. Useovphysx_read()/PhysX.read()andovphysx_write()/PhysX.write(). Nothing is removed and behavior is unchanged; removal is not scheduled for 0.6.
Removed#
The
OVPHYSX_NO_USDbuild option and the USD-linked variant. There is one ovphysx build and it links no USD. Consumers that attached a nativeUsdStagemust attach through ovstage instead.
[0.6.0] - Date 2026-08-31#
C ABI break.
ovphysx_debug_render_set_parameter()changes its third parameter frombooltofloat. A binary built against an earlier 0.5 header must be recompiled and relinked; source callers should pass1.0ffor enabled and0.0ffor disabled. This is independent of theovphysx_cloneABI change introduced in 0.5.4.Python source breaks.
PhysX.clone()renames theparent_transformskeyword toanchor_transforms. Positional callers, the C ABI, and runtime placement behavior are unchanged. The formerPhysX.release()method and main-object context-manager protocol are removed. Replacephysx.release()withphysx.destroy(), and replacewith PhysX()with explicit construction plusdestroy()in afinallyblock.C ABI break.
ovphysx_contact_event_header_t.stageId(int64_t) is renamed and retyped to.attachHandle(uint64_t). A stageless attach used to reportstageId == 0indistinguishably from “no attach”;attachHandleis nonzero for every live attach, stageless or not, and never repeats across a detach/reattach pair. A binary built against an earlier header reading.stageIdmust be recompiled and relinked — the field keeps the same offset and the same 8-byte width, so a stale consumer silently reinterprets a live attach handle as a USD stage id instead of reading a mismatched-width neighbor field or plainly zero. Addedovphysx_get_attach_handle()/PhysX.get_attach_handle()to read an instance’s current attach handle directly, without waiting for a contact event to report one.C++ source break. Many public runtime entry points move from
uint64_t stageIdtoAttachHandle, and becauseAttachHandleis itself auint64_talias, a stale out-of-repo source caller can compile unchanged and get wrong runtime behavior rather than a build failure. Affected:IPhysxSimulation::addForceAtPos,addTorque,wakeUp,putToSleep,isSleeping,subscribePhysicsTriggerReportEvents, and their*Instancedcounterparts;IPhysxReplicator::registerReplicator,unregisterReplicator,replicate,isReplicatorStage, and theReplicationAttachFn/ReplicationAttachEndFncallback typedefs;IPhysxCookingService’sPhysxCookingComputeRequest::attachHandlefield and its completion callback; andTriggerEvent.h’sTriggerEventData::stageId, renamed to::attachHandlein parallel with the ABI rename documented above. PasskActiveAttachwhere a caller used to pass stage id 0 meaning “the current attach” — it resolves late, at the point of use, so a pre-attach subscription still works, but it only resolves while exactly one attach is live. Obtain a concrete handle for a specific attach fromIPhysxSimulation::getAttachHandle(). A stale or otherwise unresolvable handle resolves to no attach rather than aliasing a different live one. Diagnostics on that path are not uniform across this surface — for exampleregisterReplicator()emitsCARB_LOG_ERRORfor a stale or non-live handle, while other entry points here resolve silently — so a caller must not rely on a log appearing and should check return values/results to diagnose staleness itself.subscribePhysicsTriggerReportEventsno longer acceptskNoAttach(0) as an “all attaches” wildcard the way the old stage-id spelling did;kNoAttachis now rejected outright (returnskInvalidSubscriptionId). A consumer that wants every attach must subscribe once per attach, using each attach’s own handle orkActiveAttach.C++ source/binary break. The runtime’s public
IPhysxinterface no longer names an object by USD path.ObjectCreationNotificationFn,ObjectDestructionNotificationFn,getObjectId(), andgetPhysXPtr()takeomni::physics::parse::ObjectKeywhere they previously took anSdfPath; the two types are not layout-compatible, so a stale binary fails to compile or link rather than silently misinterpreting the parameter. This is a hard break with no compatibility shim — out-of-repo consumers of these entry points must rebuild against the new signatures. Resolve a path string to anObjectKey(and back) only through the two boundary functions designated for that; no other public function takes or returns a path.C ABI break.
ovphysx_scene_query_hit_t’scollision,rigid_body, andmaterialfields (part of the same object-identity migration as theIPhysxbreak above) no longer hold a uint64-encodedSdfPath; they hold an opaqueomni::physics::parse::ObjectKey.handleassigned by the runtime. The two encodings are not comparable — a consumer that reproduced ovphysx’s old SdfPath bit-cast to match hit fields against known prim paths will silently compare against the wrong values instead of failing loudly. Struct layout, size, and field order are unchanged, so this is a behavioral break only, not an ABI-layout break — no recompile is required, but any comparison logic against the old encoding now silently misbehaves and must be removed. Use the newovphysx_scene_query_get_paths_from_ids()(see Added below) to resolve these fields to a path instead.C++ source/binary break.
IPhysxSceneQuery.h’s scene-query surface (the C++ counterpart of theovphysx_scene_query_hit_tbreak above, part of the same object-identity migration) no longer names a shape, body, or material by USD path.SphereShapeReportFn,BoxShapeReportFn,CapsuleShapeReportFn,ConeShapeReportFn,CylinderShapeReportFn,ConvexMeshShapeReportFn, andTriangleMeshShapeReportFn’s leading identity parameter now takeomni::physics::parse::ObjectKeyin place of auint64_t-encodedSdfPath;SceneQueryHitObject::collision/rigidBodyandSceneQueryHitLocation::materialretype the same way.overlapMesh,overlapMeshAny,reportCollisionShapes,overlapShape,overlapShapeAny,sweepMeshClosest,sweepShapeClosest,sweepMeshAny,sweepShapeAny,sweepMeshAll, andsweepShapeAlltakeObjectKeyin place of their mesh/gPrim/traversal-root path parameter. The primitive queries (raycast, sphere/box sweep and overlap) are unaffected — they never carried a path parameter. A stale binary fails to compile or link rather than silently misinterpreting the parameter.C++ source/binary break.
IPhysxCooking::precookMesh’smeshPathparameter (part of the same object-identity migration) retypes and renames tomeshKey(omni::physics::parse::ObjectKey), matching this same header’s already-migratedcreateConvexMesh/cookAutoDeformableBody.C++ source/binary break.
TriggerEvent.h’sTriggerEventData::triggerColliderPrimId,otherColliderPrimId,triggerBodyPrimId, andotherBodyPrimId(part of the same object-identity migration) retype and rename totriggerColliderPrimKey,otherColliderPrimKey,triggerBodyPrimKey, andotherBodyPrimKey(omni::physics::parse::ObjectKey) in place of auint64_t-encodedSdfPath. Independent of this same struct’sstageId→attachHandleretype —TriggerEventDatahas no publicovphysxC ABI mirror, so that retype is not separately called out elsewhere in this changelog.C ABI struct-layout break.
PhysxCookingComputeRequest(ovphysx/ovruntime/include/omni/physx/IPhysxCookingService.h) drops itsDataInputMode dataInputModefield, theDataInputModeenum (eINPUT_MODE_FROM_PRIM_ID,eINPUT_MODE_FROM_PRIM_MESH_VIEW), anddouble primTimeCode, shrinking the struct by 16 bytes (4-byte enum, 4 bytes of alignment padding, and the 8-bytedouble). The removedeINPUT_MODE_FROM_PRIM_IDmode let the cooking service resolve a bareprimStageId/primIdpair by reading USD directly; every request now carries aPhysxCookingMeshViewthat the caller fills through the same backend-agnosticIPhysicsSourcepath ovstage callers already used exclusively.primStageIdremains on the struct but is now a caller-owned correlation key only, not a stage-lookup input. This is a public SDK API break with no compatibility shim: a binary built against the earlier header must be recompiled and relinked. A caller already submitting mesh-view requests needs no change; a caller that relied on the removed bare prim-id/stage-id default must build and populate aPhysxCookingMeshViewbefore submitting.Source break.
ConvexDecomposition::applySphereApproximation(const char* primPath, uint32_t stageId)is removed from the publicomni/convexdecomposition/ConvexDecomposition.hheader. It had zero callers anywhere in this repo; the retainedConvexDecomposition::computeSphereApproximation, which takes a caller-suppliedSimpleMeshinstead of a USD prim path and stage id, remains available for sphere-approximation authoring. A caller relying on the removed prim-path overload must read its own mesh data and callcomputeSphereApproximationdirectly — there is no drop-in replacement.C++ source break.
IPhysx::createD6JointAtPath()changes from(ObjectKey jointKey, ObjectKey body0, ObjectKey body1)to(const char* jointPath, ObjectKey body0, ObjectKey body1). This is the third narrow place a path string is allowed to cross the public API boundary: the call is create-shaped, soresolveObjectKey’s existence gate can never resolvejointPathbefore the joint exists, andjointPathis instead minted into anObjectKeyexistence-independently viaAttachedStage::keyFor(). A nulljointPath, or no attached stage, does not fail the call – the joint is still created and returned if the underlyingPxD6JointCreatesucceeds, but it is silently skipped forObjectKeyregistration and stays unresolvable by key afterward.C++ source/binary break.
IPhysxSimulation::setSimulationOutputFlags(),addSimulationOutputFlags(), andremoveSimulationOutputFlags()drop theirpaths/numPathsparameters and become pure global on/off toggles for the givenSimulationOutputType. The per-path variant bit-castSdfPathasuint64_tinstead of using the runtime’s object-identity type and had no callers anywhere in this repo, including theovphysxSDK; it is removed rather than migrated. The global mode — which gates real transform/velocity write-back and replicator skip-write behavior — is unchanged in behavior; it is simply the only mode now. A binary built against an earlier header must be recompiled and relinked.Selected configuration arguments on
PhysX(),wait_op(),wait_all(),attach_ovstage(),read(),read_tokens(),get_contact_report(), andenable_python_logging()are now keyword-only. Positional use of those arguments raisesTypeError; pass them by name. Primary operands remain positional.Logging C ABI break. Log levels now use
DEFAULT=0,VERBOSE=1,INFO=2,WARNING=3,ERROR=4,NONE=5. The former multi-callbackovphysx_register_log_callback/ovphysx_unregister_log_callbackAPI is replaced by the single-slotovphysx_set_log_callback, whose callback also receives channel and Unix-epoch timestamp metadata.ovphysx_set_log_level()now changes only the namedomni_physx_sdk,omni.physx, andovphysx_internalCarbonite source policies instead of Carbonite’s process-global threshold. Unnamed, host, and dependency sources remain unchanged; the application callback still observes and filters the process log stream.OVPHYSX_LOG_NONEis therefore not a whole-runtime or process mute. Every successful shutdown disables and drains the callback, including when live handles remain solely for explicit destruction. Recompile C/C++ callers.Logging Python source change.
enable_python_logging()now accepts a minimum severity and channel filter and owns the sole native callback slot while enabled, replacing any C-level callback. The callback’s channel and timestamp are exposed asovphysx_channelandovphysx_timestampon each PythonLogRecord. CallingPhysX()orPhysX.destroy()from that callback is rejected; retry after callback delivery returns. Concurrent construction during process initialization waits; it shares a successful initialization or retries after a known failure. Construction rejects during ambiguous rollback and while final shutdown is draining so callback dependencies cannot deadlock the drain. A callback must not synchronously wait for work that may emit into the same serialized callback registration. Successful final shutdown disables and drains the Python bridge and releases its callback owners.Returned-string source change. Empty
ovphysx_get_last_error()andovphysx_get_last_op_error()results now have a non-NULL pointer; testlength, notptr, to distinguish an empty result.
Python source break.
ReadGroupgains two trailing fields,cuda_streamandcuda_wait_event, taking it from 14 to 16. It is aNamedTuple, so its arity is part of its public shape: unpacking a whole group (a, b, ... = group), comparing one against a 14-tuple, or unpickling one written by an earlier version now fails. Both fields default to0, so keyword construction, attribute access, indexing and slicing are unaffected. Readgroup.cuda_wait_eventrather than positionally unpacking.Python source break.
PhysX.read()andPhysX.read_tokens()now returnwarp.array. Every non-empty tensor is a Warp array on the read’s native CPU or CUDA device; every non-empty index map is a CPUwarp.arrayofuint32on both backends. They no longer return CPU NumPy arrays or CUDAManagedDLTensorwrappers. A native lane count above 1 becomes a trailing Warp dimension. Use the Warp array directly, or call.numpy().copy()when an independent host copy is needed.Behavior change — disabling a rigid body, and the unsupported raw-pointer path. On a DirectGPU scene, disabling one rigid body no longer invalidates the whole scene view: bulk reads and writes omit the disabled body and keep serving its enabled peers (query discovery still counts it; CPU output stays inclusive). Because that hard invalidation is gone, toggling
PxActorFlag::eDISABLE_SIMULATIONon aPxRigidDynamic*obtained fromovphysx_get_physx_ptr()is now unsupported and undefined: ovphysx does not observe the change, so the next DirectGPU read or write silently resolves a stale index to the wrong body — where a 0.5 build failed loudly with a whole-view invalidation. Disable a standalone rigid body through the ovstagedisableSimulationattribute viaovphysx_write()instead. A point-instancer instance has no supported per-instance disable route in this release —disableSimulationis not an instancer-writable column and there is no per-instance tensor equivalent — so disabling an individual instance is unsupported.
Added#
Output read: full attribute coverage.
ovphysx_read()/PhysX.read()now serve the same attribute set as the tensor API.object type
attributes added
RIGID_BODY,ARTICULATION_LINKlinear/angular acceleration;
mass,inverseMass,inertia,inverseInertia,centerOfMassPosition,centerOfMassOrientation,disableSimulation,disableGravity;staticFriction,dynamicFriction,restitution,contactOffset,restOffset,shapeCountARTICULATION(new)rootPosition,rootOrientation,rootLinearVelocity,rootAngularVelocity,centerOfMassWorld,centerOfMassLocal, the per-shape properties,shapeCount, andjacobian,massMatrix,coriolisForce,gravityForce,centroidalMomentum,jacobianShapeARTICULATION_JOINTthe full DOF set: position/velocity targets, actuation and projected force, stiffness, damping, limits, max velocity/force, armature, friction, drive model,
driveTypeFIXED_TENDON,SPATIAL_TENDON(new)tendonStiffness,tendonDamping,tendonLimitStiffness,tendonOffset; plustendonLimitandtendonRestLengthon fixed tendonsDEFORMABLE_VOLUME,DEFORMABLE_SURFACErestPoints,simElementIndices, andcollisionElementIndices(volume only)DEFORMABLE_MATERIAL(new)dynamicFriction,youngsModulus,poissonsRatio,elasticityDamping,bendingStiffness,thickness,bendingDampingApp-to-physics write.
ovphysx_write()/ovphysx_fetch_write_next()/ovphysx_commit_group()/ovphysx_release_write(), andPhysX.write()in Python, open a session over a query and hand back writable groups whose columns match whatovphysx_read()emits for the same query — same prims, same order, same residency. A column can be read, edited and written back with no repack, and on a GPU scene without leaving the device. Values are published through the tensor backend, not through per-actor USD authoring.A failed
ovphysx_commit_group()says WHICH failure it was. A group that was never live — unknown session, or unknown, foreign or already committed — is rejected before any publish and reports that nothing was written. A live group whose publish failed does not: the scatter ran, a device scatter can fail partway, and commit is not retryable, so the error says the group is spent and that how much reached the solver is not reported at this layer. The two previously shared one message that claimed nothing was published.In Python a group’s columns are MUTABLE ALIASES of the runtime’s storage, never copies: fill them in place, then commit. Device columns come back as Warp arrays on their own device, matching what
read()returns, so torch/cupy can take them zero-copy; host columns come back as NumPy views over the mapped pointer. Neither outlives its session.Coverage:
OVPHYSX_OBJECT_RIGID_BODY—position,orientation,linearVelocity,angularVelocity.OVPHYSX_OBJECT_ARTICULATION_JOINT—jointPosition,jointVelocity.
Contract notes:
One attribute per session. The ovstage map group carries no attribute field, so writing position and orientation over one prim set is two sessions.
A group commits once, identified by its address. Committing an unknown, foreign or already-committed group is refused rather than silently accepted.
Uncommitted groups are discarded on release, so a caller that fails mid-fill publishes nothing from the group it was filling.
positionandorientationare two slices of one transform, so writing either preserves the other — at the cost of reading the current pose first. The two velocities are independent and pay no such cost. The same applies to joint DOFs: a write preserves the DOFs it does not address.Articulation links refuse per ATTRIBUTE, not per type. A
PxArticulationLinkis aPxRigidBody, soOVPHYSX_OBJECT_ARTICULATION_LINKacceptsmass,inertia,centerOfMassPosition/centerOfMassOrientation,disableGravityand the per-shape properties. It refusesposition,orientation,linearVelocityandangularVelocity— PhysX has no link write for them — and refusesdisableSimulation, whichPxActorFlagsupports onPxRigidStaticandPxRigidDynamiconly. Each refusal names the attribute and the alternative.Articulation DOF properties and drive inputs are writable on
OVPHYSX_OBJECT_ARTICULATION_JOINT: the thirteen properties the read publishes —jointStiffness,jointDamping,jointLimit,jointMaxVelocity,jointMaxForce,jointArmature, the friction triple, the drive-envelope triple andjointDriveType— plusjointPositionTarget,jointVelocityTargetandjointActuationForce. Names are shared with the read, so the two cannot drift apart.Each write applies the exact inverse of the read’s unit fold, per attribute rather than per object: on the same rotational axis the two targets carry the rad→deg fold and the body-order sign, while
jointActuationForcecarries only the sign — a joint effort is not an angular quantity.jointLimitis one attribute of two lanes, not two attributes. It is refused on an axis whose motion is noteLIMITED: PhysX does not allow an axis to become limited while its articulation is in a scene, so the limit would silently do nothing. The refusal names the condition and writes nothing at all, so a limit never half-lands. Writing back the±FLT_MAXsentinel the read reports for a free axis is not a finite limit and still succeeds, which keeps read-modify-write over an articulation with a mix of limited and free axes working.Fixed: host-only rigid properties failed to commit on a DirectGPU scene.
mass,inertia,centerOfMassPosition/centerOfMassOrientation,disableGravityanddisableSimulationpublish a host column even on a DirectGPU scene, and the commit was choosing which backend view to resolve rows through from that column’s residency — so it asked for the CPU simulation view, which does not exist there, and the write failed while the matching read worked. The view now follows the scene’s pipeline. Affects every DirectGPU scene, not only scenes with disabled bodies.Fixed: disabling one rigid body no longer blocks DirectGPU bulk reads and writes for its scene. Device output and ordinary write groups omit disabled bodies while continuing to serve enabled peers. Query discovery still counts every matching prim, and
disableSimulation=0becomes available for re-enabling a disabled body. CPU reads remain inclusive because the host actor state is still available.Deformable simulation-mesh state is writable on
OVPHYSX_OBJECT_DEFORMABLE_VOLUMEandOVPHYSX_OBJECT_DEFORMABLE_SURFACE:pointsandvelocities, one array group per body.pointsis accepted in the SIM-MESH local frame — the frame the read publishes it in — andvelocitiesin world, matching the read on both.These columns are device-resident, unlike the particle and material ones: PhysX exposes sim-mesh state only as device buffers, so the write is a device scatter into PhysX’s own memory followed by the
markDirtyits header calls mandatory. Volume and surface bodies expose different getters and different dirty-flag enums for the same two columns, and both are chosen from the object’s concrete PhysX type.restPointsandkinematicTargetare refused by name.restPointsis authored geometry the solver never rewrites.kinematicTargetis set by handing PhysX a buffer it keeps, which a write session cannot supply because its column is freed at release — serving it needs an owned per-body buffer with a lifetime this API does not have.Deformable material properties are writable on
OVPHYSX_OBJECT_DEFORMABLE_MATERIAL:deformableYoungsModulus,deformablePoissonsRatio,deformableDynamicFriction,deformableElasticityDamping, plus the surface-onlydeformableBendingStiffness,deformableThicknessanddeformableBendingDamping. One fixed group stacking every material prim, one f32 each, host-resident on every scene — these are authored inputs PhysX never writes back, so there is no device copy to hand out.The three surface-only names address the SURFACE materials only, which is the subset the read publishes them over: PhysX puts them on
PxDeformableSurfaceMaterialalone, and the read omits the row on a volume material rather than reporting0.0. Which kind a material is comes from PhysX’s own concrete type, not from the record that names it.Particle points and velocities are writable on
OVPHYSX_OBJECT_PARTICLE_SET, as one array group per set.pointsis accepted in the set prim’s LOCAL frame — the frame the read publishes it in — andvelocitiesin world, matching the read on both counts.These columns are host-resident even on a DirectGPU scene, unlike the read’s, which are device-resident. The destination is the reason: a particle write lands in the set’s pinned host staging arrays and raises its upload flag, and PhysX copies that to the device buffer at the next step. It is the same mechanism USD authoring already publishes these two quantities through, which is what keeps a written column readable before the next step — the read serves the staging arrays exactly when that flag is raised.
Point-instancer instances are writable, on both devices. A
position,orientation,linearVelocityorangularVelocitywrite onOVPHYSX_OBJECT_RIGID_BODYemits the standalone group and one array group per instancer, carrying that instancer’s full instance array placed by index — the same shape the read emits, so a column can be read and written straight back. An index with no live instance is skipped: there is nothing to write to, which mirrors the read leaving such a slot as the caller zero-filled it. Note a scaled instancer round-trips only up to its scale: the local pair carries position and orientation, and USD keepsscalesas its own array.Vehicle wheel controls are writable on
OVPHYSX_OBJECT_VEHICLE_WHEEL:driveTorque,brakeTorqueandsteerAngle, for vehicles with no drive, whose control surface is per wheel.positionandorientationare refused on that type by name and permanently — a wheel’s transform is composed each step from the chassis, its suspension and its steer angle, so a written value is overwritten by the next step. Vehicles are CPU-only, so these have no device path. Vehicles WITH a drive are accelerated and steered as a whole and their commands are not yet reachable.Force and wrench are writable, and are the API’s only write-only attributes.
forceis a vec3 applied at the centre of mass;wrenchis[N,9]— force, torque, and the WORLD-space point the load applies at — deliberately not split, because the split rule exists so each half can be read back and preserved, and a control input the solver clears each step has no stored value to preserve. Both are accepted onOVPHYSX_OBJECT_RIGID_BODYandOVPHYSX_OBJECT_ARTICULATION_LINK. Neither is ever emitted byovphysx_read: reading a force back would report what the solver did with it, not what was written. A force applies for one step and is then cleared, so it must be written before each step it should act on. On a link, note that a commit covers the articulation view as a whole: links the query did not match have their force zeroed for that step, because a write-only input has no stored value to preserve.Tendon properties are writable, on
OVPHYSX_OBJECT_FIXED_TENDONandOVPHYSX_OBJECT_SPATIAL_TENDON:tendonStiffness,tendonDamping,tendonLimitStiffnessandtendonOffseton both kinds, plustendonLimit(the(low, high)pair) andtendonRestLengthon fixed tendons only. The two fixed-only names are refused on a spatial tendon by name — not because PhysX lacks a setter, but because the schema places both on the tendon’s leaf attachment, which is the same reason the read refuses them. No unit conversion in either direction: the rad→deg fold lives in the gearing coefficient, so everything downstream of it is passthrough.The articulation itself is writable, through the new
OVPHYSX_OBJECT_ARTICULATION(SimObjectType.ARTICULATION, enum value 9), whose prims are the articulation root prims. It acceptsrootPosition,rootOrientation,rootLinearVelocityandrootAngularVelocity— the same names the read uses for this type, not the bare rigid spellings — the articulation’s ROOT state, and the documented way to place an articulation now that link poses are refused. The selector overlapsOVPHYSX_OBJECT_ARTICULATION_LINKon the root prim, which is fine: these are queries, not a partition, and the two reach different state on it.Link poses are not refreshed by a write. After writing joint state or a root pose, link poses read back at their last-stepped values until the next step, matching the read’s contract of reporting the most recently completed step. No
eUPDATE_KINEMATICis issued.
Articulation tendon output read. Two new simulated object types,
OVPHYSX_OBJECT_FIXED_TENDONandOVPHYSX_OBJECT_SPATIAL_TENDON(SimObjectType.FIXED_TENDON/SPATIAL_TENDONin Python), read through the sameovphysx_query/ovphysx_readlifecycle as every other type. Both servetendonStiffness,tendonDamping,tendonLimitStiffnessandtendonOffset; fixed tendons also servetendonLimit(a(low, high)pair,dtype.lanes == 2) andtendonRestLength— the spatial tendon has neither, because the schema places them on its leaf attachment. The prim reported for a tendon is the prim carrying its root API: the joint withPhysxTendonAxisRootAPI, or the link withPhysxTendonAttachmentRootAPI. A tendon’s other axes and attachments do not produce rows. Groups are fixed (one row per tendon, all tendons stacked), and columns are device-resident on a DirectGPU scene like the other backend-sourced reads. Tendon properties are authoring-time values, soOVPHYSX_SCOPE_ACTIVEbehaves asOVPHYSX_SCOPE_ALLfor both types.Output-read acceleration attributes.
ovphysx_read()serveslinearAccelerationandangularAcceleration(OVPHYSX_ATTR_LINEAR_ACCELERATION/OVPHYSX_ATTR_ANGULAR_ACCELERATION,vec3 f32) forOVPHYSX_OBJECT_RIGID_BODYandOVPHYSX_OBJECT_ARTICULATION_LINK, on both devices, closing the gap against the tensor binding’sRIGID_BODY_ACCELERATION_F32andARTICULATION_LINK_ACCELERATION_F32. Point-instancer instances publish them as the array attributesaccelerationsandangularAccelerations. Values come from the same DirectGPU scratch buffers velocity uses, so a read asking for both pays one extra bulk read rather than a second gather path.Output-read body properties.
ovphysx_read()servesmass,inverseMass,inertia,inverseInertia,centerOfMassPosition,centerOfMassOrientation,disableGravityanddisableSimulationforOVPHYSX_OBJECT_RIGID_BODYandOVPHYSX_OBJECT_ARTICULATION_LINK, closing theRIGID_BODY_MASS/INERTIA/COM/…andARTICULATION_BODY_*tensor-binding gap. Three things differ from the existing columns and a consumer must handle them:disableGravity/disableSimulationareuint8({kDLUInt, 8}), not f32 — readtensors[i].dtype; all eight are always host-resident (kDLCPU), because they are simulation inputs PhysX never writes back, so on a DirectGPU scene one read can returnkDLCUDApose alongsidekDLCPUmass — branch ontensors[i].device.device_type; and they have no point-instancer array form, so a read asking for one warns and omits instanced bodies from that column.centerOfMass*is in the body’s local frame, unlikeposition/orientation.Output-read per-shape properties.
ovphysx_read()servesstaticFriction,dynamicFriction,restitution,contactOffset,restOffsetandshapeCountforOVPHYSX_OBJECT_RIGID_BODYandOVPHYSX_OBJECT_ARTICULATION_LINK, completing rigid-body parity with the tensor binding apart from the write-only force/wrench pair. A body has a variable shape count, so these are a padded fixed group: one stacked tensor whosedtype.lanesis the widest body in that read — it varies between reads of the same attribute, so do not cache it. Row i holdsshapeCount[i]real values then zeros; readshapeCount(int32) to find the end, since 0.0 is a legal offset and is not a terminator. Padding is zero-filled, unlike the tensor API’s, which is uninitialised. Only a shape’s first material is reported, matching the binding. Host-resident like the other properties. Note each material attribute costs its owngetMaterials()pass over every shape, so reading all three walks them three times.Three things to check in your reader:
Take
dtype.lanesfrom each group, not once per attribute. The inverse dynamics matrices are sized by each articulation’s topology, so a scene with two robot types returns two groups per attribute; a uniform fleet returns one, as before.A joint-state read returns one group per attribute, not one per joint prim. Deformable and particle reads changed the same way: a scene of N sets now yields one array group of N tensors where it used to yield N groups of one. Walk
tensor_countand pair each tensor with the prim at the same index in the group’s prim list.One read can mix devices — a CUDA pose column beside a CPU property column. Branch on
device_typeper tensor. Deformable and particlepoints/positions/velocitiesare nowkDLCUDAwhere they used to be host, with adata.cuda_sync.wait_eventto wait on (ReadGroup.cuda_wait_eventin Python). Point-instancer arrays arekDLCUDAon a GPU scene at either scope and for every attribute, including acceleration; they used to fall back to host for all but the simplest read.PhysX.readalready handles this.
Not served:
kinematicTarget, which is simulation input rather than output. Omitted where it does not apply:centroidalMomentumon fixed-base articulations,collisionElementIndiceson surface deformables, andbendingStiffness/thickness/bendingDampingon volume materials. A group can therefore cover a subset of its type’s prims — pair values with the group’s own prim list.Reads are also faster, at 8,192 environments: joint state ~0.31 ms (was ~728 ms), links ~0.28 ms (was ~1.09 ms), and a four-attribute read over 163,860 rigid bodies ~0.65 ms on GPU, ~33 ms on CPU (was ~52 ms). The same read over 163,840 point-instancer instances is ~0.7 ms on GPU (was ~5.5 ms) and ~19 ms on CPU (was ~21 ms). The particle read gained two orders of magnitude separately, by using the world-to-local matrix the runtime already maintains instead of resolving one out of USD per set on every read. A read that cannot produce a column now reports failure instead of returning short or zero-filled data.
Benchmark timing diagnostics.
--timing-diagnostics=<path>writes one schema-version-1 JSONL object per emitted row with the count, mean, population standard deviation, minimum, and maximum across every actual timed step; the existing trimmed report value is unchanged. A separate hidden DirectGPU row measures the first creation and spec lookup of the five bindings used by the 4,096-Cartpole control row. Stage loading, cloning, warmup, and caller CUDA tensor-buffer allocation are outside that timer.OmniPVD TCP startup transport. Four typed C and Python config fields select TCP, address, port, and millisecond send timeout at instance creation. FILE remains the default; TCP connects synchronously to an already-ready trusted plaintext listener.
Named native timeout contract. Added the ABI-identical
ovphysx_timeout_talias plusOVPHYSX_TIMEOUT_POLLandOVPHYSX_TIMEOUT_INFINITE. Timeout-bearing C and experimental C++ APIs now use the named type and values; existing binaries anduint64_tsource callers remain compatible. Python keeps its idiomatic0(poll), positive integer (finite nanoseconds), andNone(infinite) spellings.Logging and returned-string contracts. Added single-callback replacement, longest-prefix channel filtering, serialized delivery, reentrancy guards, and a bounded barrier for callback deliveries already accepted by the ovphysx dispatcher. Every
ovphysx_string_tsuccessfully produced or delivered by ovphysx, including populated outputs and callback values, now has a non-NULL pointer and trailing NUL atptr[length]; input strings remain length-prefixed views.OmniPVD capture compatibility docs. The OmniPVD recording tutorial now states the OVD format, OmniPVD stream version (0.4.0), the independent PhysX OVD integration version (3.1), Kit
omni.physx.pvdas the canonical reader, and both reader compatibility checks.NVTX profiling for Nsight Systems. ovphysx can emit NVTX ranges for its API calls (
ovphysxdomain) and for the PhysX SDK profile zones, CPU and GPU (PhysXdomain). Enable withOVPHYSX_NVTX=1in the environment orPhysXConfig(nvtx_enabled=True)/ovphysx_config_entry_nvtx_enabled(true)before instance creation; read the effective state withovphysx_get_global_config_bool(OVPHYSX_CONFIG_NVTX_ENABLED, ...)orPhysX.get_config_bool(ConfigBool.NVTX_ENABLED). Instrumentation is compiled into release builds and the wheel, so profiling needs no rebuild, and is off by default. NVTX 3 is header-only: nothing extra is linked or shipped. See NVTX Profiling With Nsight Systems.Checked Python instance destruction.
PhysX.destroy()is now the canonical deterministic lifecycle operation. It is idempotent after the instance reaches terminal state and raises when native destruction or final process shutdown reports a failure.Process-wide hard CPU-only mode is observable. Added
ovphysx_get_cpu_mode/PhysX.get_cpu_mode()/ experimentalPhysX::getCpuMode()to report the effective hard CPU-only policy (ovphysx_set_cpu_mode(true)orOVPHYSX_DISABLE_GPU). Successfulovphysx_create_instance()now emits an INFO line withprocess_cpu_only=,cuda_available=, andactive_cuda_gpus=(no_overridefor empty create-args; explicit"-1"logs as-1).OVPHYSX_DISABLE_GPUis latched atovphysx_initialize(live before that). This is hard-policy / create-intent observability only – not attach-time resolved scene dynamics or ordinal.ovphysx_scene_query_get_paths_from_ids()resolves scene-query hit identities to paths. New C API (Python:PhysX.get_scene_query_paths_from_ids()) resolves the opaqueObjectKey.handlevalues in a raycast/sweep/overlap hit’scollision,rigid_body, andmaterialfields back to physics-object paths, mirroringovphysx_contact_binding_get_other_actor_paths_from_ids()for contact bindings. Unlike that resolver, returned strings are not a per-call cache: each is owned by the currently attached physics source and stays valid until the next detach/re-attach, so a later call to this function (against the same attach) does not invalidate a pointer an earlier call returned. Unresolvable ids (the zero sentinel, an id from an object removed since the query, or no active attach) yield an empty path rather than an error. Seedeveloper_guide.md’s “Path Encoding” section under Scene Queries. Open question, disclosed rather than silently decided: this resolver is a compatibility bridge that restores the path-resolvability the object-identity migration above took away from the raycast/sweep/overlap family, not a statement about that family’s long-term home. Whether that family is kept, removed, or relocated to a successor scene-query library is not yet decided.ovphysx_cuda_stream_wait_event(stream, event)orders your stream after a read’s work. A device read column is handed over before the work producing it has necessarily finished; call this to order your own CUDA stream after the group’s event, without taking a CUDA dependency of your own. Aneventof0is a no-op success.ovphysx_debug_render_set_scope_tokens()filters debug visualization by exact OVStage path handles. The scope API has no string-prefix or path- interning variant: callers expand a hierarchy to its exact object set and use the Stage’spath_dictionary_instance_tto createovx_primpath_thandles. Matching uses the runtime object’s canonical source key, so it does not require exposed PhysX actor names. Handles require no individual release, remain valid only for their originating Stage dictionary, and the scope is cleared on detach.Python type stubs (
.pyi) andpy.typedmarker for IDE support. The wheel and editable source tree now ship PEP 561 stubs forPhysX,TensorBinding,ContactBinding, scene-query result types, DLPack structures, and module-level exports so VS Code, PyCharm, and Cursor can provide autocompletion and static type checking without loading native code. CI andvalidate_allrunpyrighton the stub tree viascripts/test_pyright.cmake.Kinematic support-geometry guidance and samples. New C and Python samples demonstrate a translating support driven by ovstage transform updates, a stationary
PhysxSurfaceVelocityAPIconveyor, and their additive combination with dynamic riders. The accompanying guide explains how kinematic transform changes become PhysX targets, distinguishes them from teleporting legacy tensor pose writes, and documents friction, sleeping, reset/readback, control ordinals, authored-scale preservation, local versus resolved world matrices, descendant world updates, and the DirectGPU limitations.ovphysx.utils.step_and_write_to_ovstage(physx, *, dt, output_ordinal, outputs=None)utility. Runs onestep_sync, the matchingPhysX.readoutput selections, and the OVStage write-back without expanding the corePhysXAPI. Each group’s emitted attribute name is written to a shadowsim:<name>attribute (avoiding a type collision with the authored column),prim_listforwards directly, fixed-group tensors pass through as-is, and array-group tensors are lane-folded into OVStage’sdtype.lanesvector representation; the whole write seals with oneadvance_write_floor(ordinal=output_ordinal).output_ordinalmust never be passed toupdate_from_ovstage, or physics would re-ingest its own output.Three physics fixtures are added to the shipped sample data payload.
empty_dynamic_boxes.usda,empty_dynamic_boxes_cpu.usdaandsimple_physics_scene_cpu.usdaland undertests/data/, whichinstall.cmakeandbuild_wheel.cmakecopy into the SDK (samples/data/) and the wheel (ovphysx/samples/data/). The two_cpulayers are sublayer overlays declaring an explicit CPU solver and MBP broadphase over their base scene, for consumers needing the CPU pipeline rather than the GPU-dynamics default aPhysicsScenewithoutPhysxSceneAPIselects. No existing fixture changed.Sequential OmniPVD recording.
ovphysx_start_recording(),ovphysx_stop_recording()andovphysx_is_recording()—start_recording(),stop_recording()andis_recording()in Python — record to an exact.ovdfile or to a TCP listener that is already accepting, and can be started and stopped repeatedly while the runtime is live. Every session, whether it starts at startup or later, captures the current core PhysX state plus the full PhysXExtensions and PhysXVehicle schemas and their live objects. Recording is off unless it is asked for: the new creation-timeomnipvd_recording_capablesetting is false by default, and a runtime created without it (and without startup output) passes noPxOmniPvdto PhysX at all, so the default path carries no sampler, writer or stream cost. Opting in creates the provider and enables scene readback, which a DirectGPU scene pays for even before recording starts. Only one recording is active per shared runtime; starting a second one returnsINVALID_STATEand leaves the first destination in place.newton:velocityLimitis honored on joints. The NewtonNewtonJointAPIattribute maps ontophysxJoint:maxJointVelocitywith the same precedence as the other Newton attributes: an authored PhysX value wins, then an authored Newton value, then the PhysX default. Both are joint-level and share units — degrees per second for angular DOFs, distance per second for linear ones — so the value is used as authored. A per-axisPhysxJointAxisAPI:maxJointVelocitystill overrides it, at parse time and on a live edit.
Performance#
A stage with several physics scenes no longer rebuilds its backend cache on every read. The tensor backend’s simulation data and its cached superset views were keyed on the attach handle alone, so two
PhysicsSceneprims under one attach shared a single entry: reading them in turn evicted one and rebuilt the other, re-runningGpuSimulationData::init()– every device allocation – plus the superset view construction, on every read of every partition. Six alternating acquires produced six rebuilds where two suffice. Cache entries are now keyed on the attach and the scene, so each scene keeps its own. Single-scene stages are unaffected, and values were always correct – only the cost changes.Hidden persistent-contact step/read benchmarks. Added separate CPU and conventional-GPU rows that measure one 1/60-second synchronous step plus the public raw contact-report pull over 512 isolated persistent reporter/static pairs. Both rows validate exact pair/cardinality and borrowed report contents outside the timer, fail on known GPU fallback or capacity warnings, and reject DirectGPU. The existing seventeen-row Authoring/WriteScaling L1B inventory is unchanged. Its six hidden requested-GPU Authoring diagnostics already failed on recognized CPU-fallback warnings and now also fail on GPU contact-capacity warnings observed during attachment or warm-up. This changes their pass/fail gate, not their names or the CPU KPI contract.
Vehicle wheel
positionandorientationshare one transform composition. Reading both now composes each wheel’s world transform once –getGlobalPose,getCMassLocalPoseand either the shape’s local pose or the vehicle SDK’s – instead of running the whole composition per column and discarding the half it did not publish. On 1,024 vehicles a two-column read costs ~149 us against ~278 us before (1.87x); a one-column read is unchanged. Values are identical – only the cost changes.Faster per-shape rigid-body reads. A read asking for more than one of
staticFriction/dynamicFriction/restitution/contactOffset/restOffsetnow walks each body’s shapes ONCE for the whole set instead of once per column, and resolves the shape’s material once per shape rather than once per shape per column. On 8,192 bodies (655,440 shape visits) a five-column read costs ~47.4 ms against ~76.1 ms before (1.60x); a one-column read is unchanged. Values are identical – only the cost changes.Three of the five properties are read off the shape’s material and two are not, so a
contactOffset+restOffsetread now touches no material at all, where per-column dispatch resolved one for every column that asked.Faster whole-articulation root-state reads on CPU. A read asking for more than one of
rootPosition/rootOrientation/rootLinearVelocity/rootAngularVelocitynow hands the whole requested set to one backend gather instead of dispatching per column. On 8,192 articulations the four-column read costs ~1.01 ms against ~2.17 ms before (2.15x); a one-column read is unchanged. Values are identical – only the cost changes.The four columns come from fewer sources than there are columns. On the host all four are filled by a single
copyInternalStateToCacheper articulation, the root-transform and root-velocity flags OR’d into one call, where per-column dispatch made four. On a DirectGPU scene oneeROOT_GLOBAL_POSEcopy now serves both pose columns, taking four device fetches to three; linear and angular velocity are distinct read types and still need one each. The device read measures the same as before, so its per-column cost is not the DirectGPU copy.Hidden in-process Authoring and WriteScaling benchmark contract. Added fail-closed C++ rows for population growth/churn, runtime writes, transform/velocity updates, and the 4,096-body write-scaling pair. The frozen contract is CPU-canonical (eleven CPU rows); six requested-GPU Authoring rows remain unscheduled diagnostics. Initial lower-is-better absolute-latency KPIs are
Authoring.population_add_drip_cpuandWriteScaling.velocity_ovstage_4096_cpu; every other retained row is a diagnostic or comparator. A dedicated Linux CI job builds and installs the opt-in suite and requires exactly eleven positive CPU rows without adding benchmarks to the publish/security-critical SDK build path.Tensor-binding creation and OVStage attach and write throughput restored. A regression made tensor-binding creation and the OVStage population and velocity-write paths several times slower on large stages. Parse contexts now reuse their token set instead of rebuilding it per operation, object keys are resolved from the runtime databases before falling back to a source query so runtime-only clones cost no lookup, incremental OVStage updates reuse source-native token ids, and wildcard matches are converted to keys once while keeping their creation-order result ordering. In a local measurement, tensor-binding creation went from 5.93 s to 0.61 s and OVStage velocity writes from 306 ms to 110 ms.
Changed#
ovphysx is built USD-free by default, and no longer ships a USD runtime. The build option that produced the USD-free library was experimental and off by default; it is now on by default and is renamed to
OVPHYSX_NO_USD, dropping theEXPERIMENTAL_marker from its old name.libovphysxhas no link-time dependency on any USD library, and the shipped SDK and wheel no longer contain the namespaced USD monolith at all — roughly 70 MB smaller. USD comes entirely from the ovstage runtime the library already requires, and ovphysx binds to that one copy.This closes a class of hard aborts: a process that reached two monolith images — ovstage’s and ovphysx’s — registered USD’s process-wide singletons twice and died with
multiple debug symbol definitions, even when the two files were identical. Packaging now verifies that the ovstage runtime provides a monolith matching the one ovphysx was built against, and fails the install rather than producing a payload with no USD behind it.Scene replication and cloning, previously the one capability that did not work in the USD-free configuration, now work. Set
-DOVPHYSX_NO_USD=OFFto build the USD-linked variant, which is still supported for consumers that attach a nativeUsdStage.Updated the pinned USD and Carbonite runtimes to USD
0.25.11.kit.5and Carbonite214.0.0-pre.The bundled PhysX engine moves from 5.10 to 5.11. The simulation engine inside the wheel and the SDK is a newer PhysX SDK release. Solver results can differ in the last bits from 0.5, so a test that pins exact trajectories may need rebaselining; no ovphysx API changes with it.
Articulation Jacobians, mass matrices and centroidal momentum now use the authored joint basis. DOF state and generalized forces already followed each USD joint’s authored body relationship, but these dense quantities were returned in PhysX’s raw basis. For a joint whose
body1is the articulation parent, related quantities on the same view therefore pointed in opposite directions. All of them now share one basis, inovphysx_read()and in the tensor bindings, on CPU and DirectGPU. Articulations whose joints are all parent-first are unaffected. A caller that flipped signs itself for reversed joints — on Jacobian columns, mass-matrix rows and columns, or centroidal joint columns — must drop that correction. Floating-root coordinates, the mass-matrix root block and the centroidal bias are unchanged; Coriolis and gravity already carried the fold.ARTICULATION_MASS_CENTER_WORLDis now reported relative to the view’s subspace origin on the DirectGPU path. The GPU tensor binding previously returned PhysX’s raw computed centre of mass, while the CPU path already subtracted the subspace origin; the two disagreed for any view built with a non-zero origin. The GPU path now matches the CPU one, so the value is world-frame in the same sense on both devices.This changes a shipped value. A view whose subspace origin is zero — the default, and every view that does not opt into a subspace — is unaffected. A consumer that was subtracting the origin itself to work around the GPU behaviour will now double-subtract and must drop that correction. The ovstage output read’s centre-of-mass column is scene-world and is not affected either way. The same read no longer depends on what the destination buffer held before the call, and its DirectGPU result is complete when the getter returns.
**CPU-only tensor property APIs no longer silently stage GPU tensors Shape properties, disable-gravity/simulation flags, and wake/sleep end in CPU PhysX calls. Gpu*View helpers that copied caller GPU buffers to host (and the matching ovphysx write/read staging for those types) are removed: pass host tensors or the call fails. On GPU simulation,
eDISABLE_SIMULATIONremoves DirectGPU rows — the parent simulation view is invalidated (getValid() == false) and callers must recreate bindings for the enabled set (no disabled-pose patch cache / escape hatch). DirectGPUcreateRigidBodyViewomits already-disabled rigid dynamics so a wildcard pattern still yields a valid enabled-only view; re-enable is out of band, then recreate. Bad index tensors (GPU, wrong dtype, oversized, or out-of-range values) fail with no side effects on both CPU and GPU sims.BaseArticulationView::setCOMsnow clears the COM cache used by GPU force-at-position (matching rigid bodies).String config buffer accounting.
ovphysx_get_global_config_string()rejects NULL, zero-capacity, and capacities aboveUINT32_MAX. OnOVPHYSX_API_BUFFER_TOO_SMALL,value_out->lengthremains the caller’s writable capacity so the same descriptor can be reused after growing its buffer;out_required_sizereports the required size including the NUL.Build dependencies no longer come from the kit-kernel package. Carbonite and Omni framework headers now come from
carb_sdk_static(carb_sdk_plugins), python 3.12 from the same package, and the namespaced USD monolith and gsl fromovphysx/ovruntime. The USD version is taken from theovruntime_depspackage that ovruntime uses, so the build USD and the py312 USD used by the python tests cannot drift apart. This removes the kit-kernel download from the ovphysx build; there is no change to the shipped SDK or wheel contents.Clone target poses are now named
anchor_transforms. The C parameter, experimental C++ parameter, and Python keyword were renamed fromparent_transforms(parentTransformsin C++) because every entry anchors the exact target subtree root at its final absolute world pose; it is not a parent pose. Python keyword callers must update toanchor_transforms. The C binary ABI and runtime placement behavior are unchanged.ovphysx_warmup_gpurenamed toovphysx_warmupand extended to CPU mode. The warmup step (a 1ns simulate+fetchResults pass that initializes PhysX lazy structures and disables per-step Fabric sync overhead) now runs in CPU mode as well as GPU mode. The old function name is removed; callers must update toovphysx_warmup(C) /warmup()(Python).Statically linked the OmniPVD runtime. SDK and wheel artifacts no longer contain PVDRuntime_64.dll or libPVDRuntime_64.so; recording APIs and behavior are unchanged.
Fixed#
LogLevel.NONEnow silences the remaining PhysX runtime warnings.set_log_level(NONE)no longer lets GPU-broadphase fallback or deformable CUDA-context warnings through. The GPU-broadphase fallback warning is emitted at most once, and rigid-only scenes no longer warn about a missing deformable CUDA context. Source-level updates also leave a host-globally disabled Carbonite log channel disabled, soset_log_level(WARNING)cannot reopen it.OmniPVD recording works on Linux AArch64. FILE and TCP recording were compiled out on that platform, left over from the days when PVDRuntime shipped as a separate shared library. They are now built and tested there, and no shared-library dependency is added.
OVStage change events are delivered once.
update_from_ovstage()now honors the consumed-ordinal cursor that attachment positions at itsread_ordinal: ordinals at or below it are skipped, so a fully consumed range is a successful no-op and an overlapping range applies only its unread suffix. Object-created notifications for population authored after attachment are preserved.ovphysx_attach_ovstage()rejectsread_ordinal == 0(OVPHYSX_API_INVALID_ARGUMENT); 0 is the runtime’s internal skip-cursor sentinel and would leave attach replay unguarded.Mimic joints on a D6 joint now act on the authored rotational axis. The parse library encoded the resolved axis as a positional index (0/1/2) rather than the
JointAxisenumerator (eRotXis 4), so the engine’s axis lookup fell through and bound every D6 mimic joint toPxArticulationAxis::eSWING2regardless of thePhysxMimicJointAPI:<axis>instance applied. Nothing was logged on either CPU or GPU – the scene simply simulated the wrong degree of freedom. Revolute and prismatic mimic joints, which carry no axis, were unaffected.Fitted child mesh colliders now preserve authored mass frames.
boundingSphereandboundingCubefit offsets still place the collision shape, but no longer translate or rotate collider-local center of mass or inertia frames a second time during parent-body aggregation. USD and ovstage use the same corrected path.The process-private cooked-collider cache cleanup at shutdown now retries briefly before giving up. With
cooked_collider_cache_dirunset, a cook completing immediately before process exit could race the cache directory’s teardown:wait_all()only drains the cook compute queue, not the underlying datastore’s on-disk write-back, so the last bytes could still be trickling out when the process-private temp directory was removed, leaving it (or part of it) behind. Cleanup now retries for up to ~80 ms, which narrows the window in practice for short write-backs; it remains best-effort, and a leftover directory after retries exhaust is now logged instead of silently abandoned. No effect on the common case where cleanup already succeeds on the first attempt.Simulation-operation polling no longer blocks for completion. A zero or finite
ovphysx_wait_op()timeout now checks simulation readiness before calling the blocking result-finalization path. If the operation is not ready before the requested budget, the wait reportsOVPHYSX_API_TIMEOUT, returns the lowest pending operation, and leaves it available for a later wait. Poll and finite waits use the generic tracked-operation path; the direct single-operation sync fast path remains available only to infinite waits. Once the final boundary check observes readiness, result finalization and its actual terminal result win even if finalization extends total call duration.Unsealed articulation attachment now fails closed. Unreadable initial articulation/joint schema data returns an error; seal and retry. Attribute-scoped seals remain valid.
Repeated native destroy rejection is side-effect-free. A handle that is already absent now returns
OVPHYSX_API_ERRORbefore teardown, so Python can retry after an ambiguous ctypes exception without clearing pending-operation state owned by a surviving instance.Getting-started prerequisites now match the shipped packages. Quickstart and Hello World point to the bundled
samples/data/stages and distinguish prebuilt GPU runtime requirements from source-build requirements. GPU simulation with a prebuilt wheel or SDK needs a driver compatible with CUDA 12.8, not a CUDA Toolkit installation; the Toolkit remains a GPU-enabled source-build prerequisite.Optional Python USD authoring now documents its
usd-coreprerequisite. The public Physics Schemas page and shippedovphysx-usd-authoringskill now direct external authoring and validation tools to install stockusd-corebefore importingpxr. It remains a tool-owned optional package, not an ovphysx dependency or simulator runtime; ovstage populates the authored USD before ovphysx attaches and simulates the resulting stage. The Linux aarch64 PyPI limitation and non-Python authoring alternatives are also documented.The advertised
OVPHYSX_PHYSX_TYPE_PHYSICSlookup is now reachable.ovphysx_get_physx_ptr()accepts either zero-length string representation ({ NULL, 0 }or{ "", 0 }) for the pathless, process-globalPxPhysicsobject. A non-emptyPHYSICSselector now returnsOVPHYSX_API_INVALID_ARGUMENTinstead of falling through toOVPHYSX_API_NOT_FOUND; empty selectors for path-bound types remain invalid.Tensor views are now invalidated when the stage is detached. Detach releases every PhysX object for the stage but told the views nothing:
UsdLoad::detachdisables object-change notifications before the release, andphysXDetachemitseStoppedonly once the simulation has been started – which theIPhysxSimulationstepping path never sets. The view kept reporting valid while holding rawPxRigidBodypointers, so the nextget_transforms()dispatched a virtual call through a freed actor (NvBugs 6583612). The tensors simulation-event listener now also handlesePhysicsObjectsReleased, which every bulk release emits, covering detach,release_physics_objects()andforce_load_physics_from_usd().GPU tensor reads no longer rebuild the rigid-body GPU-index map on every read. The map is rebuilt by calling
getGPUIndex()on every body, and a dirty flag exists to skip that when no body has been enabled or disabled since the last rebuild. The flag was cleared only after an early return taken whenever no index had actually moved — which is the steady-state case — so it was cleared only on reads where something had changed, and stayed dirty otherwise. The rebuild therefore ran on every read of every GPU scene, disable-free or not, and was the single largest cost in a read: reading pose and velocity for 163,860 rigid bodies through a tensor binding went from ~11.2 ms to ~1.0 ms. Behavior is unchanged — while any body is disabled the flag still stays dirty so the map keeps refreshing until every index lands.CPU tensor views no longer leak a
PxArticulationCacheper articulation.PxArticulationReducedCoordinate::release()explicitly does not free caches created from it, so the view that calledcreateCache()owns them, but~CpuArticulationViewand~CpuRigidBodyViewwere empty. Every CPU articulation view leaked one cache per articulation, and every CPU rigid-body view one per articulation root, for the lifetime of the process. Both destructors now release them, guarded on the PhysX plugin still being loaded sincerelease()frees through the foundation allocator.Point instancers with
inactiveIdsno longer read out of bounds, andinactiveIdsis now interpreted per the USD spec. WithinactiveIdsauthored, only the prototypes still referenced by an active instance were parsed, and their descriptors were appended rather than stored at their prototype index — so the instance loop, which indexes byprotoIndices, read past the end of the list and dereferenced a garbage descriptor (NvBugs 6455958). Additionally,inactiveIdsentries were treated as positional instance indices; they are ids into the optionalidsattribute when it is authored, and positional only when it is not. An instancer withids = [100, 200]andinactiveIds = [200]now deactivates instance 1 instead of writing out of range. Out-of-rangeinactiveIdsentries and out-of-rangeprotoIndicesare skipped with a warning.Tensor views are now invalidated when their physics scene is deleted. Destroying the scene notifies subscribers as
ePTScene, which the simulation view ignored – it kept reporting valid while the GPU path held the freedPxSceneas a raw pointer, so the next read jumped through a stale vtable (observed from an on-step callback viaGpuRigidBodyView::getVelocities; NvBugs 6521047). The view now records the scene it is bound to and invalidates on its destruction, the same way it already did for bodies, links and shapes.Replacing a stage’s physics scene is fixed along with it. The scene’s cached simulation data is now dropped when the scene is destroyed, so a view created afterwards is built against the live scene rather than handed buffers, actor maps and a CUDA context belonging to the freed one.
Ragged articulation views no longer read past an articulation’s own DOFs. A view spanning articulations of different sizes reports
getMaxDofs()/getMaxLinks()as the maximum over its entries and pads every row out to that width, but the per-articulation accessors are bounded by each articulation’s own count. Several call sites walked to the maximum: the GPU articulation-view constructor queried the metatype for padding columns (logging an error per column), and the CPU Coriolis and gravity-compensation readers indexed the PhysX articulation cache past its end, producing out-of-bounds reads and sign-flipped values. Padding columns now read as0, matching the convention used elsewhere in the tensor API.IArticulationView::getUsdDofPath/getUsdLinkPathalso returned a pointer into a path destroyed on return; the path is now held for the call’s lifetime. The padding-null contract and the returned buffer’s lifetime are documented on the view interfaces. Separately, a view mixing fixed- and floating-base articulations is now refused by the accessors whose row layout is derived from base type —getCoriolisAndCentrifugal- CompensationForces,getGravityCompensationForcesandgetArticulationCentroidalMomentum. They took that layout from the first entry and applied it to all of them, reading a fixed-base articulation’s cache six values past its end when the first entry was floating-base. They now fail with an error naming the remedy: build one view per base type. Raggedness in DOF or link count alone is unaffected.Index tensors are now bounded by the view’s entry count. The loop over an index tensor was driven entirely by the descriptor’s declared shape; nothing cross-checked it against the view, and the real allocation size is not knowable from a descriptor. A shape larger than the view’s entry count therefore walked off the end of the caller’s buffer, which is how NvBugs 6504465 ended up faulting inside the CUDA driver’s reserved address range. Every index-taking entry point now rejects an oversized index tensor with a logged error, matching how the data tensor was already size-checked – the articulation, rigid-body, GPU deformable-body and deformable-material views, the property setters implemented on the shared base classes, and the GPU staged setters, whose device-to-host copy is sized from the descriptor and so read past the caller’s allocation before any bound applied.
Contract change. An index tensor may now hold at most as many indices as the view has entries. Previously a CPU view silently accepted an oversized one, processing the in-range indices and skipping the rest, so a caller passing more indices than entities – duplicates, or a deliberately padded index array – worked and will now fail with an error. That set can never be legitimate: it is exactly the set these functions build for themselves when no index tensor is supplied. Out-of-range index values within a correctly sized tensor are unaffected and continue to be skipped.
Character controllers now load and update on an OVStage-backed stage. A prim carrying
PhysxCharacterControllerAPIhad never produced a controller on any non-USD parse source: the OVStage walker had noemitCct, and its scan result’scctslist was not forwarded onto the scanned stage. An empty list is indistinguishable from a stage that authors no controller, so nothing reported the loss. Post-attachphysxCharacterController:*property edits were dropped for the same reason on the change-feed side -PhysxCharacterControllerAPIwas missing from the known-physics family lists that decide whether a path’s changes are delivered at all, in the initial seed, the structural refresh, and the cached read path alike. A controller on a capsule with no authoredradiusnow also falls back to theUsdGeomCapsuleschema value of0.5rather than1.0, which had made an unauthored controller twice as wide as the USD path wherever the source has no backing USD stage to resolve the fallback through.No spurious default physics scene on a scoped OVStage scan. “This stage authors no
PhysicsScene” is a whole-stage fact, but the OVStage walker concluded it from any scan whose result held no scene - including an incremental re-scan rooted at a single newly added prim. Each such scan published an extra synthetic/__defaultPhysicsScene__, reported as an additional object-created notification where the USD path reports none. Synthesis is now gated on a genuinely unscoped scan; a scoped initial load still gets the loader’s own default-scene fallback. A scan scoped purely by excluded subtrees counts as scoped for the same reason, which closes the same hole on the direct scan entry point (stage loading itself always scans from the pseudo-root, so it was never reachable that way).UsdGeomPointsprims answer OVStage type-identity questions consistently.Pointswas listed underXformableandPointBasedbut not underGprim, so the same prim answered true to two of its base types and false to the third. A points cloud therefore satisfied the deformable-body root gate (Xformableand notGprim) and was admitted as a deformable root. Note this does not give a points cloud a collision shape on OVStage - that dispatch does not exist yet.Stale SDF views are rejected before any GPU work.
evaluate()on anSdfViewwhose stage was torn down byreset_stage()ordetach_ovstage()already raisedRuntimeErrorinstead of crashing, but the validity check ran after the implicit warmup, so a stale handle could still trigger a simulation step against the newly attached stage before the error was returned. The handle is now resolved and validated first, and the stale call has no side effects.Deformable skinning synchronization no longer crashes when the CUDA context is unavailable. It now warns once and skips.
Clone transform documentation now matches the existing target-root placement behavior. Each
anchor_transformsentry is the final absolute world pose of the exact target subtree root. Descendants keep their poses relative to the source subtree root.OmniPVD C-API recording config is independent of config-entry order.
ovphysx_create_instance()now applies the recording directory before either the typed or raw Carbonite output-enable trigger when reusing the process-wide runtime, so[OUTPUT_ENABLED, OVD_RECORDING_DIRECTORY]produces the requested.ovdcapture just like the reverse order.Bounding-sphere and bounding-cube colliders now honor the prim’s world scale. Both approximations copied unscaled mesh points into the merged mesh description, and unlike the cooked-mesh path there is no later scale to apply, so the resulting sphere or box was sized in mesh-local units. A gprim scaled by 0.01 produced a collider 100 times too large — correctly placed and rotated, but overlapping everything around it from the first step.
Live transform edits reach colliders inside an instanceable prim. Editing the transform of an instanceable prim’s own root never reached the PhysX collider for its referenced subtree once the scene had cooked, so the collider stayed at its first-cook pose. The descendant walk now descends into instance proxies, and a static actor’s pose is no longer re-derived from a source prim that resolves inside the shared prototype, which discarded the instance-specific pose. Affects assets whose instance root carries the collision API while the geometry lives on nested prims in the prototype.
Object-deletion callbacks now run while a simulation is shutting down. Tensor simulation views subscribed with the default notification gate, which is deliberately closed as a simulation ends, so the handler that invalidates a view when its scene is released never ran on that path. Views could survive their scene until the next explicit detach.
Crash when a GPU scene was created while another was being torn down. The scene-release path moved its scene map aside before deleting the scenes, so a GPU setup re-entering during that loop saw no live scenes and was free to release and recreate the CUDA context manager that the scenes still held. The round-robin cursor that picks a context manager was also used without being reduced, so it could index past the end of its vector and return a non-null garbage pointer that passed every null check.
Crash from a mass update on an unresolved object. A rigid-body mass update carrying the invalid object id indexed the internal record array with it, reading far past the end of the array. The invalid id is now skipped, the object-creation result is checked before it is recorded, and entry ids are bounds-checked before they become record references.
Crash from an actor released during an active-actor callback. Actors released while active-actor results were being processed left stale references behind. Released actors are now tracked and skipped.
Crash when a body’s simulation owner changed and the body was then removed. Changing the owner reassigned the actor’s scene pointer without moving it between the scenes’ actor lists, so the removal searched the wrong list, found nothing, and freed the actor anyway — leaving a dangling pointer that the next simulation reset dereferenced.
Crash when OmniPVD output was toggled with a scene attached. Changing the recording setting recreated the PhysX SDK object even though live scenes still belonged to it. The recreation is now deferred while any scene is attached, and the requested setting takes effect at the next attach.
Crash from concurrent profile-statistics collection during replication. Replication opened a profile scope in two lambdas that run concurrently, and both appended to the same unsynchronized statistics vector. Profiling is on by default, so this affected every session that cloned.
Removing the default-simulator setting no longer crashes. A null value for that setting is handled instead of dereferenced.
Getting-started examples now step synchronously. The Python quickstart, the READMEs and the shipped Hello World sample used the asynchronous
step()for a single-step workflow, so Hello World could report success before the step finished and cleanup could lose the detailed operation error. They now usestep_sync(). The advanced async samples are unchanged.The Python tensor-bindings sample now shows a link that actually moves. It applied articulation velocity targets but printed link 0, which the bundled fixture fixes to the world, so the output looked static while all 14 driven links moved. It now reports the chain tip, like the C sample, and checks that the displayed pose really changed before reporting success. The misleading X-Euler line is gone — the fixture’s revolute joints turn about Y.
Removed#
pkg-config support (
ovphysx.pc). The generated.pcfile could not describe the whole dependency chain: the public headers include ovstage headers, and ovstage publishes no pkg-config metadata, so a consumer still had to inject ovstage’s include and library paths by hand.find_package(ovphysx)resolves the chain throughfind_dependency(ovstage)and is the supported integration path.ovphysx.dlpack.ManagedDLTensorand its DLPack capsule provider. The Python output read returnswarp.array, so the hand-written wrapper and its__dlpack__()capsule export are gone rather than kept as a second result type The ctypes mirror of the DLPack structs remains for the compatibility APIs that still type their Python arguments as DLPack.
Known limitations#
Point-instancer rigid bodies are not available to TensorBindingsAPI.
UsdGeom.PointInstancerinstances are simulated and available through the ovstageRIGID_BODYoutput-read path, but they are not exposed as individual rigid-body tensor-binding rows. With the default empty-binding behavior, a binding that targets only the point instancer has count zero. Read instance state through output read; control it by authoring the point-instancer arrays through ovstage.