From 6817552def5bd2932b43a6469eb8c7772b009ff2 Mon Sep 17 00:00:00 2001 From: Kelly Guo Date: Mon, 12 Oct 2026 10:15:23 -0700 Subject: [PATCH] Skip empty PhysX rigid-body view creation (#8473) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skip the PhysX scene-data rigid-body tensor view when the stage has no rigid bodies. Since #8021, `PhysxManager` requests a global `create_rigid_body_view("/**")` at warmup to back the scene-data transforms. `omni.physics.tensors` logs an error whenever a pattern matches no rigid bodies, so every static scene, for example `scripts/tutorials/00_sim/launch_app.py`, starts with: ```text [Error] [omni.physx.tensors.plugin] Pattern '/**' did not match any rigid bodies [Error] [omni.physx.tensors.plugin] Provided pattern list did not match any rigid bodies ``` The manager now checks the loaded USD stage, including instance proxies, for `UsdPhysics.RigidBodyAPI` before requesting the view. Rigid-body scenes keep the existing tensor-view and articulation-alias behavior. - **No false skips.** With Kit, every PhysX body comes from a USD prim: environments are always authored through `UsdReplicateContext`, and PhysX replication copies from USD sources. - **Cost.** `any()` stops at the first rigid body, so only static scenes walk the whole stage. - **Why not ask PhysX.** `omni.physics.tensors.SimulationView` cannot list bodies or silence the no-match error from Python. The tensors C++ layer has a per-view `setNoMatchLoggingQuiet`, which the OVPhysX tensor binding uses for wildcard bindings. Once it is exposed in Python, this stage walk can be replaced. `test_reset_without_rigid_bodies_skips_rigid_body_view` resets a scene whose only physics prim is a static collider and checks that no rigid-body view is requested or published. It fails on the pre-fix code with the `/**` request. `test_timeline_play_stop` covers the rigid-body branch; no existing test had a static scene. No additional dependencies or public API changes are required. - [x] Bug fix (non-breaking change which fixes an issue) - [x] Backport this pull request to the active release branch after it merges into `develop` `release/3.0.0` has the same `/**` request. Not applicable; this removes startup log errors. - `PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 PYTHONEXE=.venv/bin/python /python.sh --no-ros-env -m pytest -q source/isaaclab_physx/test/sim/test_simulation_context_kit.py` — TODO: rerun after the test change (was 5 passed with source-built Isaac Sim 6.2). - The reported `launch_app.py --visualizer kit,viser,rerun,newton_gl` command reached `[INFO]: Setup complete...`; neither PhysX tensor error appeared before the bounded smoke run was stopped. - `ISAACLAB_CHANGELOG_BASE_REF=upstream/develop uv run --no-sync isaaclab -f` — passed. - `uv run --no-sync python tools/changelog/cli.py check upstream/develop --include-worktree` — passed. - `performance-smoke-benchmarks` fails on the `g1-newton` leg (Warp LTO build directory `Permission denied`). This is unrelated; the same workflow fails on `develop`. - [x] I have read and understood the contribution guidelines - [x] I have run the pre-commit checks with `uv run isaaclab --format` - [x] No public documentation change is required; the user-visible fix is recorded in the package changelog - [x] My changes generate no new warnings - [x] I have validated the changed behavior with focused coverage and applied the test-audit criteria to the test change - [x] I have added a changelog fragment under `source/isaaclab_physx/changelog.d/` - [x] My name already exists in `CONTRIBUTORS.md` --------- Co-authored-by: Antoine RICHARD Co-authored-by: Claude Opus 5.5 (cherry picked from commit 4e1f1f717d49f212ac77f7e659e445d3e258f0b9) --- .../skip-empty-rigid-body-view.fixed.rst | 1 + .../isaaclab_physx/physics/physx_manager.py | 37 ++++++++++--------- .../test/sim/test_simulation_context_kit.py | 28 ++++++++++++++ 3 files changed, 49 insertions(+), 17 deletions(-) create mode 100644 source/isaaclab_physx/changelog.d/skip-empty-rigid-body-view.fixed.rst diff --git a/source/isaaclab_physx/changelog.d/skip-empty-rigid-body-view.fixed.rst b/source/isaaclab_physx/changelog.d/skip-empty-rigid-body-view.fixed.rst new file mode 100644 index 000000000000..2666084c68ae --- /dev/null +++ b/source/isaaclab_physx/changelog.d/skip-empty-rigid-body-view.fixed.rst @@ -0,0 +1 @@ +* Fixed PhysX startup error logs in static-only scenes by avoiding empty scene-data rigid-body view requests. diff --git a/source/isaaclab_physx/isaaclab_physx/physics/physx_manager.py b/source/isaaclab_physx/isaaclab_physx/physics/physx_manager.py index 627013fb475e..4376a8b80dc2 100644 --- a/source/isaaclab_physx/isaaclab_physx/physics/physx_manager.py +++ b/source/isaaclab_physx/isaaclab_physx/physics/physx_manager.py @@ -32,7 +32,7 @@ import omni.timeline import omni.usd import usdrt -from pxr import Sdf, UsdGeom, UsdPhysics, UsdUtils +from pxr import Sdf, Usd, UsdGeom, UsdPhysics, UsdUtils import isaaclab.sim as sim_utils from isaaclab.physics import CallbackHandle, PhysicsEvent, PhysicsManager @@ -1025,22 +1025,25 @@ def _warmup_and_create_views(cls) -> None: # Register the complete tensor view only after PhysX has loaded the stage. cls.backend = sim.get_or_create_backend(PhysxBackendCfg(stage_id=stage_id)) cls._scene_data_backend.backend = cls.backend - view = cls.backend.simulation_view.create_rigid_body_view("/**") - # PhysX returns a wrapper with no native handle for an empty selection. - cls._scene_data_backend._rigid_body_view = view if view._backend is not None else None - if view._backend is not None: - # Wildcard bindings expose articulation aliases; publish the actual root-link paths. - native = cls.backend.simulation_view - paths = view.prim_paths - articulation_type = omni.physics.tensors.ObjectType.Articulation - roots = [path for path in paths if native.get_object_type(path) == articulation_type] - if roots: - articulations = native.create_articulation_view(roots) - links = (body_paths[0] for body_paths in articulations.link_paths) - aliases = dict(zip(articulations.prim_paths, links, strict=True)) - cls._scene_data_backend._rigid_body_view = native.create_rigid_body_view( - [aliases.get(path, path) for path in paths] - ) + # omni.physics.tensors logs errors for an empty selection, so static scenes skip the request. + prims = Usd.PrimRange.Stage(sim.stage, Usd.TraverseInstanceProxies()) + if any(prim.HasAPI(UsdPhysics.RigidBodyAPI) for prim in prims): + view = cls.backend.simulation_view.create_rigid_body_view("/**") + # PhysX returns a wrapper with no native handle for an empty selection. + cls._scene_data_backend._rigid_body_view = view if view._backend is not None else None + if view._backend is not None: + # Wildcard bindings expose articulation aliases; publish the actual root-link paths. + native = cls.backend.simulation_view + paths = view.prim_paths + articulation_type = omni.physics.tensors.ObjectType.Articulation + roots = [path for path in paths if native.get_object_type(path) == articulation_type] + if roots: + articulations = native.create_articulation_view(roots) + links = (body_paths[0] for body_paths in articulations.link_paths) + aliases = dict(zip(articulations.prim_paths, links, strict=True)) + cls._scene_data_backend._rigid_body_view = native.create_rigid_body_view( + [aliases.get(path, path) for path in paths] + ) # Final update after view creation physx.update_simulation(cls.get_physics_dt(), 0.0) diff --git a/source/isaaclab_physx/test/sim/test_simulation_context_kit.py b/source/isaaclab_physx/test/sim/test_simulation_context_kit.py index 08fed4b87380..4276ca6bdaa1 100644 --- a/source/isaaclab_physx/test/sim/test_simulation_context_kit.py +++ b/source/isaaclab_physx/test/sim/test_simulation_context_kit.py @@ -34,6 +34,34 @@ def test_setup_teardown(): SimulationContext.clear_instance() +@pytest.mark.isaacsim_ci +def test_reset_without_rigid_bodies_skips_rigid_body_view(monkeypatch): + """Resetting a static scene must not request an empty native rigid-body view.""" + create_simulation_view = omni.physics.tensors.create_simulation_view + requested_patterns = [] + + class _SimulationViewGuard: + def __init__(self, *args, **kwargs): + self._view = create_simulation_view(*args, **kwargs) + + def __getattr__(self, name): + return getattr(self._view, name) + + def create_rigid_body_view(self, pattern): + requested_patterns.append(pattern) + return self._view.create_rigid_body_view(pattern) + + monkeypatch.setattr(omni.physics.tensors, "create_simulation_view", _SimulationViewGuard) + sim = SimulationContext() + cube_cfg = sim_utils.CuboidCfg(size=(0.1, 0.1, 0.1), collision_props=sim_utils.UsdPhysicsCollisionCfg()) + cube_cfg.func("/World/Cube", cube_cfg) + + sim.reset() + + assert requested_patterns == [] + assert sim.physics_manager.get_scene_data_backend().get_rigid_body_view() is None + + @pytest.mark.isaacsim_ci def test_timeline_play_stop(monkeypatch): """Playing shares one native view; stopping releases it before the next play."""