diff --git a/source/isaaclab/changelog.d/mtrepte-shadow-model-collision-filter-oom-test.skip b/source/isaaclab/changelog.d/mtrepte-shadow-model-collision-filter-oom-test.skip new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/source/isaaclab/test/sim/test_newton_manager_visualization_state.py b/source/isaaclab/test/sim/test_newton_manager_visualization_state.py index 407d4e639818..5e6613d5f152 100644 --- a/source/isaaclab/test/sim/test_newton_manager_visualization_state.py +++ b/source/isaaclab/test/sim/test_newton_manager_visualization_state.py @@ -826,6 +826,10 @@ def test_clone_visualization_builder_ignores_non_env_deformables_on_world_import UsdGeom.Xform.Define(stage, "/World/envs/env_1") fake_builder = _FakeShadowBuilder(body_count=1, cloth_delta=3, track_usd=True) + fake_builder.shape_collision_filter_pairs = [] + fake_builder.shape_collision_group = [] + fake_builder.shape_count = 0 + fake_builder.add_builder = lambda _builder: None clone_plan = SimpleNamespace( sources=("/World/envs/env_0",), destinations=("/World/envs/env_{}",), diff --git a/source/isaaclab_newton/changelog.d/fix-shadow-model-collision-filter-oom.rst b/source/isaaclab_newton/changelog.d/fix-shadow-model-collision-filter-oom.rst new file mode 100644 index 000000000000..269487433e0b --- /dev/null +++ b/source/isaaclab_newton/changelog.d/fix-shadow-model-collision-filter-oom.rst @@ -0,0 +1,5 @@ +Fixed +^^^^^ + +* Fixed PhysX-backend Newton visualization models replicating unused collision + filters and contact pairs, which could exhaust memory during ``ModelBuilder.finalize()``. diff --git a/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py b/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py index aa518cffe5ae..d89eec163254 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/visualization_builder.py @@ -113,6 +113,8 @@ def build_visualization_builder_from_stage_envs( shadow_entities, registry_groups = add_shadow_deformables_to_builder( builder, stage, env_paths, device=device, entries=deformable_entries, clone_plan=clone_plan ) + builder.shape_collision_filter_pairs = [] + builder.shape_collision_group[:] = [0] * builder.shape_count return builder, (shadow_entities, registry_groups) if not env_paths: @@ -147,6 +149,12 @@ def build_visualization_builder_from_stage_envs( schema_resolvers, ignore_paths=source_deformable_ignore_paths or None, ) + global_builder = builder + builder = ModelBuilder(up_axis=up_axis) # Preserve Newton's compact empty filter store. + for visual_builder in (global_builder, *source_builders.values()): + visual_builder.shape_collision_filter_pairs = [] + visual_builder.shape_collision_group[:] = [0] * visual_builder.shape_count + builder.add_builder(global_builder) replicate_builder_mapping(builder, sources, mapping, positions, quaternions, source_builders, destinations, env_ids) shadow_entities, registry_groups = add_shadow_deformables_to_builder( builder, stage, env_paths, device=device, entries=deformable_entries, clone_plan=clone_plan diff --git a/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py b/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py index c06a761f24b9..68e7e17bc980 100644 --- a/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py +++ b/source/isaaclab_newton/test/cloner/test_rename_builder_labels.py @@ -16,7 +16,7 @@ from isaaclab_newton.physics import visualization_builder as visualization_builder_module from isaaclab_newton.physics import visualization_deformables as visualization_deformables_module -from pxr import Usd, UsdGeom +from pxr import Sdf, Usd, UsdGeom, UsdPhysics from isaaclab.cloner import ClonePlan from isaaclab.scene_data.deformable_discovery import DeformableStageEntry @@ -40,6 +40,8 @@ class _FakeVisualizationModelBuilder: def __init__(self, up_axis=None): self.up_axis = up_axis + self.shape_collision_filter_pairs = [] + self.shape_collision_group = [] for attr in _VIS_BUILTIN_LABEL_ATTRS: setattr(self, attr, []) setattr(self, attr.replace("_label", "_world"), []) @@ -80,6 +82,7 @@ def add_usd(self, stage, root_path=None, ignore_paths=None, schema_resolvers=Non for attr in _VIS_BUILTIN_LABEL_ATTRS: getattr(self, attr).append(f"{root_path}/{_VIS_LABEL_SUFFIXES[attr]}") getattr(self, attr.replace("_label", "_world")).append(self._current_world or 0) + self.shape_collision_group.append(1) self.custom_attributes["mujoco:equality_constraint_label"].values.append( f"{root_path}/{_VIS_LABEL_SUFFIXES['equality_constraint_label']}" ) @@ -96,6 +99,7 @@ def add_builder(self, builder, xform=None): labels = getattr(builder, attr) getattr(self, attr).extend(labels) getattr(self, attr.replace("_label", "_world")).extend([self._current_world] * len(labels)) + self.shape_collision_group.extend(builder.shape_collision_group) eq_labels = builder.custom_attributes["mujoco:equality_constraint_label"].values self.custom_attributes["mujoco:equality_constraint_label"].values.extend(eq_labels) self.custom_attributes["mujoco:equality_constraint_world"].values.extend([self._current_world] * len(eq_labels)) @@ -363,6 +367,9 @@ def test_visualization_builder_imports_standalone_stage_as_one_world(self): self._define_xform(stage, "/World") self._define_xform(stage, "/World/Robot") builder = mock.Mock() + builder.shape_collision_filter_pairs = [] + builder.shape_collision_group = [] + builder.shape_count = 0 builder.add_usd.return_value = {"path_shape_map": {}} with ( @@ -380,6 +387,47 @@ def test_visualization_builder_imports_standalone_stage_as_one_world(self): self.assertEqual(registry_groups, []) builder.add_usd.assert_called_once_with(stage, schema_resolvers=["newton", "physx"], ignore_paths=None) + def test_visualization_builder_disables_collision_pairs(self): + stage = Usd.Stage.CreateInMemory() + robot_path = "/World/envs/env_0/Robot" + self._define_xform(stage, "/World") + self._define_xform(stage, "/World/envs") + self._define_xform(stage, "/World/envs/env_0") + self._define_xform(stage, "/World/envs/env_1", (2.0, 0.0, 0.0)) + robot = UsdGeom.Xform.Define(stage, robot_path).GetPrim() + UsdPhysics.ArticulationRootAPI.Apply(robot) + robot.CreateAttribute("physxArticulation:enabledSelfCollisions", Sdf.ValueTypeNames.Bool).Set(False) + for name, translation in (("A", 0.0), ("B", 1.0)): + body_path = f"{robot_path}/{name}" + body = UsdGeom.Xform.Define(stage, body_path) + body.AddTranslateOp().Set((translation, 0.0, 0.0)) + UsdPhysics.RigidBodyAPI.Apply(body.GetPrim()) + collision = UsdGeom.Cube.Define(stage, f"{body_path}/Collision") + collision.CreateSizeAttr(0.2) + UsdPhysics.CollisionAPI.Apply(collision.GetPrim()) + joint = UsdPhysics.RevoluteJoint.Define(stage, f"{robot_path}/Joint") + joint.CreateBody0Rel().SetTargets([Sdf.Path(f"{robot_path}/A")]) + joint.CreateBody1Rel().SetTargets([Sdf.Path(f"{robot_path}/B")]) + + clone_plan = ClonePlan( + sources=(robot_path,), + destinations=("/World/envs/env_{}/Robot",), + clone_mask=torch.ones((1, 2), dtype=torch.bool), + env_ids=torch.arange(2), + ) + for env_paths, plan, expected_shape_count in ( + ([], None, 2), + ([(0, "/World/envs/env_0"), (1, "/World/envs/env_1")], clone_plan, 4), + ): + builder, _shadow_metadata = visualization_builder_module.build_visualization_builder_from_stage_envs( + stage, env_paths, plan + ) + model = builder.finalize(device="cpu") + + self.assertEqual(model.shape_count, expected_shape_count) + self.assertEqual(len(model.shape_collision_filter_pairs), 0) + self.assertEqual(model.shape_contact_pair_count, 0) + def test_visualization_builder_rejects_clone_plan_without_environment_paths(self): """A cloned scene must not be cached as an incomplete single-world model.""" stage = Usd.Stage.CreateInMemory()