From 55470ca5d3b9cc59f25d08f2c7cb475c2c32afc0 Mon Sep 17 00:00:00 2001 From: Kelly Guo Date: Sun, 11 Oct 2026 22:27:11 -0700 Subject: [PATCH 1/2] Skip empty PhysX rigid-body view creation --- .../skip-empty-rigid-body-view.fixed.rst | 1 + .../isaaclab_physx/physics/physx_manager.py | 38 ++++++++++--------- .../test/sim/test_simulation_context_kit.py | 30 +++++++++++++++ 3 files changed, 52 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 00000000000..2666084c68a --- /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 a46852ca75f..a798997d471 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, UsdPhysics, UsdUtils +from pxr import Sdf, Usd, UsdPhysics, UsdUtils import isaaclab.sim as sim_utils from isaaclab.physics import CallbackHandle, PhysicsEvent, PhysicsManager @@ -1006,22 +1006,26 @@ 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] - ) + # The native API logs errors instead of returning an empty view when no rigid bodies exist. + rigid_body_prims = Usd.PrimRange.Stage(sim.stage, Usd.TraverseInstanceProxies()) + has_rigid_bodies = any(prim.HasAPI(UsdPhysics.RigidBodyAPI) for prim in rigid_body_prims) + if has_rigid_bodies: + 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 08fed4b8738..66966e9acaa 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,36 @@ 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() + ground_cfg = sim_utils.GroundPlaneCfg() + ground_cfg.func("/World/defaultGroundPlane", ground_cfg) + cube_cfg = sim_utils.CuboidCfg(size=(0.1, 0.1, 0.1)) + 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.""" From 50e8859c00bda1e440161749d0866b50b26c690d Mon Sep 17 00:00:00 2001 From: Antoine RICHARD Date: Mon, 12 Oct 2026 15:57:33 +0200 Subject: [PATCH 2/2] Inline static-scene guard and drop the Nucleus fixture from its test The guard comment now matches the handle-less wrapper comment below it, and the test uses a cube collider for the static-collider case instead of the network-loaded ground plane. Co-Authored-By: Claude Opus 5.5 --- .../isaaclab_physx/isaaclab_physx/physics/physx_manager.py | 7 +++---- .../isaaclab_physx/test/sim/test_simulation_context_kit.py | 4 +--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/source/isaaclab_physx/isaaclab_physx/physics/physx_manager.py b/source/isaaclab_physx/isaaclab_physx/physics/physx_manager.py index a798997d471..b7cae7a8b62 100644 --- a/source/isaaclab_physx/isaaclab_physx/physics/physx_manager.py +++ b/source/isaaclab_physx/isaaclab_physx/physics/physx_manager.py @@ -1006,10 +1006,9 @@ 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 - # The native API logs errors instead of returning an empty view when no rigid bodies exist. - rigid_body_prims = Usd.PrimRange.Stage(sim.stage, Usd.TraverseInstanceProxies()) - has_rigid_bodies = any(prim.HasAPI(UsdPhysics.RigidBodyAPI) for prim in rigid_body_prims) - if has_rigid_bodies: + # 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 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 66966e9acaa..4276ca6bdaa 100644 --- a/source/isaaclab_physx/test/sim/test_simulation_context_kit.py +++ b/source/isaaclab_physx/test/sim/test_simulation_context_kit.py @@ -53,9 +53,7 @@ def create_rigid_body_view(self, pattern): monkeypatch.setattr(omni.physics.tensors, "create_simulation_view", _SimulationViewGuard) sim = SimulationContext() - ground_cfg = sim_utils.GroundPlaneCfg() - ground_cfg.func("/World/defaultGroundPlane", ground_cfg) - cube_cfg = sim_utils.CuboidCfg(size=(0.1, 0.1, 0.1)) + 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()