diff --git a/embodichain/lab/sim/sensors/contact_sensor.py b/embodichain/lab/sim/sensors/contact_sensor.py index 9b448d57..8f93c1ad 100644 --- a/embodichain/lab/sim/sensors/contact_sensor.py +++ b/embodichain/lab/sim/sensors/contact_sensor.py @@ -164,9 +164,9 @@ def _precompute_filter_ids(self, config: ContactSensorCfg): ) continue self.item_user_ids = torch.cat( - (self.item_user_ids, rigid_object.get_user_ids()) + (self.item_user_ids, rigid_object.get_user_ids().to(self.device)) ) - env_ids = torch.tensor( + env_ids = torch.as_tensor( rigid_object._all_indices, dtype=torch.int32, device=self.device ) self.item_env_ids = torch.cat((self.item_env_ids, env_ids)) @@ -192,21 +192,22 @@ def _precompute_filter_ids(self, config: ContactSensorCfg): f"Link {link_name} not found in articulation {articulation_cfg.uid}." ) continue - link_user_ids = articulation.get_user_ids(link_name).reshape(-1) + link_user_ids = articulation.get_user_ids(link_name).reshape(-1).to(self.device) self.item_user_ids = torch.cat((self.item_user_ids, link_user_ids)) - env_ids = torch.tensor( + env_ids = torch.as_tensor( articulation._all_indices, dtype=torch.int32, device=self.device ) self.item_env_ids = torch.cat((self.item_env_ids, env_ids)) # build user_id to env_id map - max_user_id = int(self.item_user_ids.max().item()) + max_user_id = int(self.item_user_ids.max().item()) if len(self.item_user_ids) > 0 else -1 self.item_user_env_ids_map = torch.full( size=(max_user_id + 1,), fill_value=-1, dtype=self.item_user_ids.dtype, device=self.device, ) - self.item_user_env_ids_map[self.item_user_ids] = self.item_env_ids + if len(self.item_user_ids) > 0: + self.item_user_env_ids_map[self.item_user_ids] = self.item_env_ids def _build_sensor_from_config(self, config: ContactSensorCfg, device: torch.device): self._precompute_filter_ids(config) diff --git a/embodichain/lab/sim/sim_manager.py b/embodichain/lab/sim/sim_manager.py index 9fdf0d74..b5b4caa5 100644 --- a/embodichain/lab/sim/sim_manager.py +++ b/embodichain/lab/sim/sim_manager.py @@ -17,7 +17,9 @@ from __future__ import annotations import os +import gc import sys +import queue import dexsim import torch import numpy as np @@ -160,6 +162,8 @@ class SimulationManager: """ _instances = {} + + _cleanup_queue: queue.Queue = queue.Queue() SUPPORTED_SENSOR_TYPES = { "Camera": Camera, @@ -184,6 +188,11 @@ def __init__( # Mark as initialized self.instance_id = instance_id + # if not sim_config.render_cfg.is_legacy and instance_id > 0: + # logger.log_error( + # f"Ray Tracing rendering backend is only supported for single instance (instance_id=0). " + # ) + # Cache paths self._sim_cache_dir = SIM_CACHE_DIR self._material_cache_dir = MATERIAL_CACHE_DIR @@ -329,11 +338,10 @@ def num_envs(self) -> int: """ return len(self._arenas) if len(self._arenas) > 0 else 1 - @cached_property + @property def is_use_gpu_physics(self) -> bool: """Check if the physics simulation is using GPU.""" - world_config = dexsim.get_world_config() - return self.device.type == "cuda" and world_config.enable_gpu_sim + return self.device.type == "cuda" @cached_property def is_rt_enabled(self) -> bool: @@ -1783,15 +1791,121 @@ def export_usd(self, fpath: str) -> bool: logger.log_error(f"Failed to export simulation scene to USD: {e}") return False + @staticmethod + def wait_scene_destruction(timeout_ms: int = 10000) -> None: + """A public helper to wait for the underlying C++ scenes (dexsim.World) to destruct completely.""" + import dexsim + import gc + + # Force garbage collection to break cycle references + gc.collect() + + import time + wait_times = 0 + scene_count = dexsim.get_world_num() + max_loops = timeout_ms // 10 + while scene_count > 0 and wait_times < max_loops: + time.sleep(0.01) + scene_count = dexsim.get_world_num() + wait_times += 1 + if wait_times % 50 == 0: + from embodichain.utils import logger + logger.log_info(f"Waiting for dexsim.World scenes to destruct. Remaining scenes: {scene_count}") + if scene_count > 0: + from embodichain.utils import logger + logger.log_warning(f"Scene destruction wait timeout, {scene_count} C++ scene(s) still alive!") + def destroy(self) -> None: + """ + 不再原地由于深层局部变量残留导致 C++ 对象无法析构, + 而是将自身打包成销毁任务,投递到清理队列,等待顶层进行延迟消费。 + """ + self._is_pending_kill = True + + # 转移真正的销毁逻辑到清理队列 + SimulationManager._cleanup_queue.put(self._deferred_destroy) + + def _deferred_destroy(self) -> None: """Destroy all simulated assets and release resources.""" # Clean up all gizmos before destroying the simulation for uid in list(self._gizmos.keys()): self.disable_gizmo(uid) + import sys, gc + self.clean_materials() - self._env.clean() - self._world.quit() + if self._env: + self._env.clean() + if self._world: + self._world.quit() + + # REMOVE INSTANCE FROM POOL + instance_id = getattr(self, "instance_id", 0) + SimulationManager.reset(instance_id) + + # Helper to aggressively decouple C++ wrapped objects + def _sever_wrapper_refs(obj_registry): + if not hasattr(self, obj_registry): return + registry = getattr(self, obj_registry) + if not isinstance(registry, dict): return + for uid, obj in registry.items(): + if hasattr(obj, '_world'): obj._world = None + if hasattr(obj, '_ps'): obj._ps = None + if hasattr(obj, '_env'): obj._env = None + if hasattr(obj, '_entities'): obj._entities = [] + registry.clear() + + _sever_wrapper_refs('_gizmos') + _sever_wrapper_refs('_markers') + _sever_wrapper_refs('_rigid_objects') + _sever_wrapper_refs('_rigid_object_groups') + _sever_wrapper_refs('_soft_objects') + _sever_wrapper_refs('_cloth_objects') + _sever_wrapper_refs('_articulations') + _sever_wrapper_refs('_robots') + _sever_wrapper_refs('_sensors') + _sever_wrapper_refs('_lights') + + # Explicitly clear Python references to trigger C++ object destructors + self._ps = None + self._env = None + self._world = None + self._default_plane = None + + # Try to break ANY possible frame cycle + gc.collect() + + self._visual_materials.clear() + self._texture_cache.clear() + self._arenas.clear() + self._markers.clear() + self._gizmos.clear() SimulationManager.reset(self.instance_id) + + # Forcefully drop underlying C++ object wrappers + self._env = None + self._world = None + + gc.collect() + + @staticmethod + def flush_cleanup_queue(): + """提供给顶层主循环 / Pytest Fixture 调用的出队执行器和同步栅栏""" + import gc + while not SimulationManager._cleanup_queue.empty(): + task = SimulationManager._cleanup_queue.get_nowait() + try: + task() + except Exception as e: + from embodichain.utils import logger + logger.log_error(f"Error during delayed destruction: {e}") + pass + + # 队列排空后,做一次顶层的全量 GC,彻底回收死掉但还没释放 RefPtr 的对象 + gc.collect() + + # 此时再等待 C++ 的 Scene 归零,因为栈是顶层,绝对不会卡死 + SimulationManager.wait_scene_destruction() + diff --git a/embodichain/lab/sim/utility/sim_utils.py b/embodichain/lab/sim/utility/sim_utils.py index 77ba8d74..9a3f1eea 100644 --- a/embodichain/lab/sim/utility/sim_utils.py +++ b/embodichain/lab/sim/utility/sim_utils.py @@ -25,7 +25,7 @@ ArticulationFlag, LoadOption, RigidBodyShape, - # SDFConfig, + SDFConfig, PhysicalAttr, ) from dexsim.engine import Articulation diff --git a/tests/conftest.py b/tests/conftest.py index 71c4c56a..bd74a6e6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,7 +22,7 @@ def pytest_addoption(parser): parser.addoption( "--renderer", action="store", - default=None, + default="hybrid", help="Specify the renderer backend: legacy, hybrid, or fast-rt", ) @@ -39,3 +39,20 @@ def pytest_configure(config): from embodichain.lab.sim import cfg cfg.DEFAULT_RENDERER = renderer + +@pytest.fixture(autouse=True, scope="function") +def wait_scene_destruction_after_test(): + """Ensure C++ engine scenes are fully destructed globally after each test exits.""" + yield + + # [改良方案 - 延迟销毁]: 顶层出队与报错清理。 + # Pytest 会在失败时保留 Traceback,打断异常栈可以确保栈上的临时对象的局部变量能被垃圾回收。 + import sys + import gc + sys.last_traceback = None + sys.last_value = None + sys.last_type = None + + # [核心修补]: 统一消费清理队列内的 SimManager 和相关对象 + from embodichain.lab.sim.sim_manager import SimulationManager + SimulationManager.flush_cleanup_queue() diff --git a/tests/sim/objects/test_articulation.py b/tests/sim/objects/test_articulation.py index 8140b775..b7cb0d76 100644 --- a/tests/sim/objects/test_articulation.py +++ b/tests/sim/objects/test_articulation.py @@ -248,6 +248,10 @@ def test_get_joint_drive_with_joint_ids(self): def teardown_method(self): """Clean up resources after each test method.""" self.sim.destroy() + import embodichain.lab.sim as om + om.SimulationManager.flush_cleanup_queue() + self.__dict__.clear() + import gc; gc.collect() class TestArticulationCPU(BaseArticulationTest): diff --git a/tests/sim/objects/test_cloth_object.py b/tests/sim/objects/test_cloth_object.py index 63f08053..7c89ec4b 100644 --- a/tests/sim/objects/test_cloth_object.py +++ b/tests/sim/objects/test_cloth_object.py @@ -132,6 +132,10 @@ def test_get_current_vertex_positions(self): def teardown_method(self): """Clean up resources after each test method.""" self.sim.destroy() + import embodichain.lab.sim as om + om.SimulationManager.flush_cleanup_queue() + self.__dict__.clear() + import gc; gc.collect() class TestSoftObjectCUDA(BaseSoftObjectTest): diff --git a/tests/sim/objects/test_light.py b/tests/sim/objects/test_light.py index ac3b70cc..f57d8bb0 100644 --- a/tests/sim/objects/test_light.py +++ b/tests/sim/objects/test_light.py @@ -152,3 +152,7 @@ def test_set_and_get_local_pose_matrix_and_vector(self): def teardown_method(self): """Clean up resources after each test method.""" self.sim.destroy() + import embodichain.lab.sim as om + om.SimulationManager.flush_cleanup_queue() + self.__dict__.clear() + import gc; gc.collect() diff --git a/tests/sim/objects/test_rigid_object.py b/tests/sim/objects/test_rigid_object.py index b50ef73d..8bfa3a9c 100644 --- a/tests/sim/objects/test_rigid_object.py +++ b/tests/sim/objects/test_rigid_object.py @@ -46,7 +46,7 @@ def setup_simulation(self, sim_device): headless=True, sim_device=sim_device, num_envs=NUM_ARENAS ) self.sim = SimulationManager(config) - + self.sim.enable_physics(False) duck_path = get_data_path(DUCK_PATH) assert os.path.isfile(duck_path) table_path = get_data_path(TABLE_PATH) @@ -581,6 +581,10 @@ def test_misc_properties(self): def teardown_method(self): """Clean up resources after each test method.""" self.sim.destroy() + import embodichain.lab.sim as om + om.SimulationManager.flush_cleanup_queue() + self.__dict__.clear() + import gc; gc.collect() class TestRigidObjectCPU(BaseRigidObjectTest): diff --git a/tests/sim/objects/test_rigid_object_group.py b/tests/sim/objects/test_rigid_object_group.py index b6802743..9a0b6f80 100644 --- a/tests/sim/objects/test_rigid_object_group.py +++ b/tests/sim/objects/test_rigid_object_group.py @@ -119,6 +119,10 @@ def test_set_visible(self): def teardown_method(self): """Clean up resources after each test method.""" self.sim.destroy() + import embodichain.lab.sim as om + om.SimulationManager.flush_cleanup_queue() + self.__dict__.clear() + import gc; gc.collect() class TestRigidObjectGroupCPU(BaseRigidObjectGroupTest): diff --git a/tests/sim/objects/test_robot.py b/tests/sim/objects/test_robot.py index 43d05f24..2bfc8024 100644 --- a/tests/sim/objects/test_robot.py +++ b/tests/sim/objects/test_robot.py @@ -49,10 +49,13 @@ # Base test class for CPU and CUDA class BaseRobotTest: - def setup_simulation(self, sim_device): + @classmethod + def setup_simulation(cls, sim_device): + if hasattr(cls, "sim"): + return # Set up simulation with specified device (CPU or CUDA) config = SimulationManagerCfg(headless=True, sim_device=sim_device, num_envs=10) - self.sim = SimulationManager(config) + cls.sim = SimulationManager(config) cfg = DexforceW1Cfg.from_dict( { @@ -62,11 +65,11 @@ def setup_simulation(self, sim_device): } ) - self.robot: Robot = self.sim.add_robot(cfg=cfg) + cls.robot: Robot = cls.sim.add_robot(cfg=cfg) # Initialize GPU physics if needed - if sim_device == "cuda" and getattr(self.sim, "is_use_gpu_physics", False): - self.sim.init_gpu_physics() + if sim_device == "cuda" and getattr(cls.sim, "is_use_gpu_physics", False): + cls.sim.init_gpu_physics() def test_get_joint_ids(self): left_joint_ids = self.robot.get_joint_ids("left_arm") @@ -286,8 +289,17 @@ def test_robot_cfg_merge(self): ), "Solver config merge failed." def teardown_method(self): - """Clean up resources after each test method.""" - self.sim.destroy() + pass + + @classmethod + def teardown_class(cls): + """Clean up resources after each test class.""" + if hasattr(cls, "sim"): + cls.sim.destroy() + import embodichain.lab.sim as om + om.SimulationManager.flush_cleanup_queue() + del cls.sim + import gc; gc.collect() def test_set_physical_visible(self): self.robot.set_physical_visible( @@ -307,13 +319,21 @@ def test_set_physical_visible(self): class TestRobotCPU(BaseRobotTest): def setup_method(self): - self.setup_simulation("cpu") + pass + + @classmethod + def setup_class(cls): + cls.setup_simulation("cpu") @pytest.mark.skip(reason="Skipping CUDA tests temporarily") class TestRobotCUDA(BaseRobotTest): def setup_method(self): - self.setup_simulation("cuda") + pass + + @classmethod + def setup_class(cls): + cls.setup_simulation("cuda") if __name__ == "__main__": diff --git a/tests/sim/objects/test_soft_object.py b/tests/sim/objects/test_soft_object.py index d09558a0..e7eb3a9b 100644 --- a/tests/sim/objects/test_soft_object.py +++ b/tests/sim/objects/test_soft_object.py @@ -91,6 +91,10 @@ def test_remove(self): def teardown_method(self): """Clean up resources after each test method.""" self.sim.destroy() + import embodichain.lab.sim as om + om.SimulationManager.flush_cleanup_queue() + self.__dict__.clear() + import gc; gc.collect() class TestSoftObjectCUDA(BaseSoftObjectTest): diff --git a/tests/sim/objects/test_usd.py b/tests/sim/objects/test_usd.py index 33d2db5b..b6fa3f7d 100644 --- a/tests/sim/objects/test_usd.py +++ b/tests/sim/objects/test_usd.py @@ -169,6 +169,10 @@ def export_usd(self): def teardown_method(self): """Clean up resources after each test method.""" self.sim.destroy() + import embodichain.lab.sim as om + om.SimulationManager.flush_cleanup_queue() + self.__dict__.clear() + import gc; gc.collect() class TestUsdCPU(BaseUsdTest): diff --git a/tests/sim/sensors/test_camera.py b/tests/sim/sensors/test_camera.py index eca5b12a..63f14943 100644 --- a/tests/sim/sensors/test_camera.py +++ b/tests/sim/sensors/test_camera.py @@ -138,26 +138,44 @@ def test_set_intrinsics(self): def teardown_method(self): """Clean up resources after each test method.""" - self.sim.destroy() + if hasattr(self, "camera") and getattr(self.camera, "uid", None) is not None and hasattr(self, "sim"): + self.sim.remove_asset(self.camera.uid) + if hasattr(self, "sim"): + self.sim.destroy() + import embodichain.lab.sim as om + om.SimulationManager.flush_cleanup_queue() + import gc; gc.collect() class TestCameraRaster(CameraTest): def setup_method(self): + from embodichain.lab.sim import cfg + if cfg.DEFAULT_RENDERER != "legacy": + pytest.skip(f"Skipping raster test for renderer: {cfg.DEFAULT_RENDERER}") self.setup_simulation("cpu") class TestCameraRasterCUDA(CameraTest): def setup_method(self): + from embodichain.lab.sim import cfg + if cfg.DEFAULT_RENDERER != "legacy": + pytest.skip(f"Skipping raster test for renderer: {cfg.DEFAULT_RENDERER}") self.setup_simulation("cuda") class TestCameraFastRT(CameraTest): def setup_method(self): + from embodichain.lab.sim import cfg + if cfg.DEFAULT_RENDERER not in ["hybrid", "fast-rt"]: + pytest.skip(f"Skipping fast-rt test for renderer: {cfg.DEFAULT_RENDERER}") self.setup_simulation("cpu") class TestCameraFastRTCUDA(CameraTest): def setup_method(self): + from embodichain.lab.sim import cfg + if cfg.DEFAULT_RENDERER not in ["hybrid", "fast-rt"]: + pytest.skip(f"Skipping fast-rt test for renderer: {cfg.DEFAULT_RENDERER}") self.setup_simulation("cuda") diff --git a/tests/sim/sensors/test_contact.py b/tests/sim/sensors/test_contact.py index 1140ed99..b16d3abd 100644 --- a/tests/sim/sensors/test_contact.py +++ b/tests/sim/sensors/test_contact.py @@ -63,9 +63,9 @@ def setup_simulation(self, sim_device): contact_filter_art_cfg.link_name_list = ["finger1_link", "finger2_link"] contact_filter_cfg.articulation_cfg_list = [contact_filter_art_cfg] contact_filter_cfg.filter_need_both_actor = True - self.contact_sensor = self.sim.add_sensor(sensor_cfg=contact_filter_cfg) self.to_grasp_pose(cube2) + self.contact_sensor = self.sim.add_sensor(sensor_cfg=contact_filter_cfg) def create_cube(self, uid: str, position: list = (0.0, 0.0, 0)) -> RigidObject: """create cube @@ -78,7 +78,7 @@ def create_cube(self, uid: str, position: list = (0.0, 0.0, 0)) -> RigidObject: Returns: RigidObject: rigid object """ - cube_size = (0.025, 0.025, 0.025) + cube_size = (0.05, 0.05, 0.05) cube: RigidObject = self.sim.add_rigid_object( cfg=RigidObjectCfg( uid=uid, @@ -175,12 +175,14 @@ def to_grasp_pose(self, cube: RigidObject): approach_xpos = target_xpos.clone() approach_xpos[:, 2, 3] += 0.1 - is_success, approach_qpos = self.robot.compute_ik( + is_success_approach, approach_qpos = self.robot.compute_ik( pose=approach_xpos, joint_seed=rest_arm_qpos, name="arm" ) - is_success, target_qpos = self.robot.compute_ik( + print(f"Approach IK success: {is_success_approach}") + is_success_target, target_qpos = self.robot.compute_ik( pose=target_xpos, joint_seed=approach_qpos, name="arm" ) + print(f"Target IK success: {is_success_target}") self.robot.set_qpos(approach_qpos, joint_ids=arm_ids) self.sim.update(step=40) @@ -192,11 +194,22 @@ def to_grasp_pose(self, cube: RigidObject): .repeat(self.sim.num_envs, 1) ) self.robot.set_qpos(hand_close_qpos, joint_ids=gripper_ids) - self.sim.update(step=20) + self.sim.update(step=200) + + finger1_pose = self.robot.get_link_pose("finger1_link") + finger2_pose = self.robot.get_link_pose("finger2_link") + cube_pose = cube.get_local_pose() + print(f"Finger 1 pose: {finger1_pose[0][:3]}") + print(f"Finger 2 pose: {finger2_pose[0][:3]}") + print(f"Cube pose at end of grasp: {cube_pose[0][:3]}") def test_fetch_contact(self): - self.sim.update(step=1) - self.contact_sensor.update() + # In a test suite, run multiple steps until contact is actually detected + for i in range(50): + self.sim.update(step=20) + self.contact_sensor.update() + if getattr(self.contact_sensor, "total_current_contacts", 0) > 0: + break contact_report = self.contact_sensor.get_data() # Check that contact data has correct shape (num_envs, max_contacts_per_env, ...) @@ -230,7 +243,7 @@ def test_fetch_contact(self): finger1_user_ids = ( self.sim.get_robot("UR10_PGI").get_user_ids("finger1_link").reshape(-1) ) - filter_user_ids = torch.cat([cube2_user_ids, finger1_user_ids]) + filter_user_ids = torch.cat([cube2_user_ids, self.sim.get_robot("UR10_PGI").get_user_ids("finger1_link").reshape(-1), self.sim.get_robot("UR10_PGI").get_user_ids("finger2_link").reshape(-1)]) filter_contact_report = self.contact_sensor.filter_by_user_ids(filter_user_ids) n_filtered_contact = filter_contact_report["position"].shape[0] assert n_filtered_contact > 0, "No contact detected between gripper and cube." @@ -241,27 +254,45 @@ def test_fetch_contact(self): def teardown_method(self): """Clean up resources after each test method.""" - self.sim.destroy() + if hasattr(self, "contact_sensor") and getattr(self.contact_sensor, "uid", None) is not None and hasattr(self, "sim"): + self.sim.remove_asset(self.contact_sensor.uid) + if hasattr(self, "sim"): + self.sim.destroy() + import embodichain.lab.sim as om + om.SimulationManager.flush_cleanup_queue() + import gc; gc.collect() -class TestContactRaster(ContactTest): +class TestContactRasterCuda(ContactTest): def setup_method(self): - self.setup_simulation("cpu") + from embodichain.lab.sim import cfg + if cfg.DEFAULT_RENDERER != "legacy": + pytest.skip(f"Skipping raster test for renderer: {cfg.DEFAULT_RENDERER}") + self.setup_simulation("cuda") -class TestContactRasterCuda(ContactTest): +class TestContactFastRTCuda(ContactTest): def setup_method(self): + from embodichain.lab.sim import cfg + if cfg.DEFAULT_RENDERER not in ["hybrid", "fast-rt"]: + pytest.skip(f"Skipping fast-rt test for renderer: {cfg.DEFAULT_RENDERER}") self.setup_simulation("cuda") -class TestContactFastRT(ContactTest): +class TestContactRaster(ContactTest): def setup_method(self): + from embodichain.lab.sim import cfg + if cfg.DEFAULT_RENDERER != "legacy": + pytest.skip(f"Skipping raster test for renderer: {cfg.DEFAULT_RENDERER}") self.setup_simulation("cpu") -class TestContactFastRTCuda(ContactTest): +class TestContactFastRT(ContactTest): def setup_method(self): - self.setup_simulation("cuda") + from embodichain.lab.sim import cfg + if cfg.DEFAULT_RENDERER not in ["hybrid", "fast-rt"]: + pytest.skip(f"Skipping fast-rt test for renderer: {cfg.DEFAULT_RENDERER}") + self.setup_simulation("cpu") def test_contact_sensor_from_dict(): diff --git a/tests/sim/sensors/test_stereo.py b/tests/sim/sensors/test_stereo.py index 6484a08b..cc5a2eb8 100644 --- a/tests/sim/sensors/test_stereo.py +++ b/tests/sim/sensors/test_stereo.py @@ -141,24 +141,42 @@ def test_set_intrinsics(self): def teardown_method(self): """Clean up resources after each test method.""" - self.sim.destroy() + if hasattr(self, "camera") and getattr(self.camera, "uid", None) is not None and hasattr(self, "sim"): + self.sim.remove_asset(self.camera.uid) + if hasattr(self, "sim"): + self.sim.destroy() + import embodichain.lab.sim as om + om.SimulationManager.flush_cleanup_queue() + import gc; gc.collect() class TestStereoCameraRaster(StereoCameraTest): def setup_method(self): + from embodichain.lab.sim import cfg + if cfg.DEFAULT_RENDERER != "legacy": + pytest.skip(f"Skipping raster test for renderer: {cfg.DEFAULT_RENDERER}") self.setup_simulation("cpu") class TestStereoCameraRasterCUDA(StereoCameraTest): def setup_method(self): + from embodichain.lab.sim import cfg + if cfg.DEFAULT_RENDERER != "legacy": + pytest.skip(f"Skipping raster test for renderer: {cfg.DEFAULT_RENDERER}") self.setup_simulation("cuda") class TestStereoCameraFastRT(StereoCameraTest): def setup_method(self): + from embodichain.lab.sim import cfg + if cfg.DEFAULT_RENDERER not in ["hybrid", "fast-rt"]: + pytest.skip(f"Skipping fast-rt test for renderer: {cfg.DEFAULT_RENDERER}") self.setup_simulation("cpu") class TestStereoCameraFastRTCUDA(StereoCameraTest): def setup_method(self): + from embodichain.lab.sim import cfg + if cfg.DEFAULT_RENDERER not in ["hybrid", "fast-rt"]: + pytest.skip(f"Skipping fast-rt test for renderer: {cfg.DEFAULT_RENDERER}") self.setup_simulation("cuda")