|
6 | 6 | """Package containing the PhysX simulation interfaces for IsaacLab core package.""" |
7 | 7 |
|
8 | 8 | import importlib.metadata |
9 | | - |
10 | | -from ._simulation_manager_patch import _SimulationManagerPatch |
| 9 | +import sys |
| 10 | +from contextlib import suppress |
| 11 | +from types import ModuleType |
| 12 | +from typing import Any |
11 | 13 |
|
12 | 14 | try: |
13 | 15 | __version__ = importlib.metadata.version("isaaclab_physx") |
14 | 16 | except importlib.metadata.PackageNotFoundError: |
15 | 17 | __version__ = "0.0.0" |
16 | 18 |
|
| 19 | +_SIMULATION_MANAGER_ENABLE_HOOK: Any | None = None |
| 20 | + |
| 21 | + |
| 22 | +def _patch_isaacsim_simulation_manager(enable_if_available: bool = False) -> None: |
| 23 | + """Patch Isaac Sim's ``SimulationManager`` to use :class:`PhysxManager`. |
| 24 | +
|
| 25 | + This redirects future ``from isaacsim.core.simulation_manager import SimulationManager`` |
| 26 | + consumers to :class:`isaaclab_physx.physics.PhysxManager`, but the original |
| 27 | + Isaac Sim ``SimulationManager`` class has *already* registered timeline |
| 28 | + (PLAY/STOP) and stage (OPENED/CLOSED) subscriptions during its extension |
| 29 | + startup. Those subscriptions live on the original class, not the module |
| 30 | + attribute, so swapping the attribute alone is not enough. |
| 31 | +
|
| 32 | + Starting with Isaac Sim 6.0.0-alpha.180 (commit ``8df6beeb0`` on |
| 33 | + ``develop``, "hmazhar/autofix_bugs"), the original |
| 34 | + ``SimulationManager._on_stop``/``_on_play``/``_on_stage_*`` methods were |
| 35 | + decorated with ``@staticmethod`` so they finally fire correctly from the |
| 36 | + Carb event subscriptions. Before that fix they were silently broken (the |
| 37 | + subscriptions invoked them as bound methods, so the ``event`` argument was |
| 38 | + being passed as ``self``/``cls`` and the bodies never executed). |
| 39 | +
|
| 40 | + The newly-working ``_on_stop`` calls |
| 41 | + ``SimulationManager.invalidate_physics()``, which calls |
| 42 | + ``view.invalidate()`` on its ``omni.physics.tensors`` simulation view. |
| 43 | + Because ``omni.physics.tensors.create_simulation_view("warp", stage_id=...)`` |
| 44 | + returns the same underlying SimulationView per stage_id, that invalidation |
| 45 | + also wrecks the view that :class:`PhysxManager` (and any articulation |
| 46 | + ``_root_view`` derived from it) relies on. The result is the runtime error |
| 47 | + ``Simulation view object is invalidated and cannot be used again to call |
| 48 | + getDofVelocities`` on the very first ``scene.update()`` after |
| 49 | + ``sim.reset()``. |
| 50 | +
|
| 51 | + To prevent this, we disable the original class's default callbacks here |
| 52 | + *before* swapping the module attribute, so :class:`PhysxManager` becomes |
| 53 | + the single owner of the simulation lifecycle. |
| 54 | +
|
| 55 | + This function is intentionally lazy. Config loading may import |
| 56 | + :mod:`isaaclab_physx` before Kit has launched; in that case, there is no app |
| 57 | + interface and no Isaac Sim callbacks to patch yet. |
| 58 | +
|
| 59 | + When ``enable_if_available`` is set, :meth:`PhysxManager.initialize` asks Kit |
| 60 | + to enable the extension before Isaac Lab creates PhysX tensor views. This |
| 61 | + prevents later optional Isaac Sim imports (for example from a controller or |
| 62 | + gripper utility) from starting the original manager after our tensor views |
| 63 | + exist and invalidating them during its startup reset. |
| 64 | + """ |
| 65 | + if enable_if_available: |
| 66 | + _subscribe_to_simulation_manager_enable() |
| 67 | + _enable_isaacsim_simulation_manager_if_available() |
| 68 | + |
| 69 | + original_module = sys.modules.get("isaacsim.core.simulation_manager") |
| 70 | + if original_module is None: |
| 71 | + return |
| 72 | + |
| 73 | + from .physics.physx_manager import IsaacEvents, PhysxManager |
| 74 | + |
| 75 | + # Tear down the original Isaac Sim SimulationManager's default timeline / |
| 76 | + # stage subscriptions so they cannot invalidate the omni.physics.tensors |
| 77 | + # view that PhysxManager owns. ``enable_all_default_callbacks(False)`` |
| 78 | + # covers warm_start (PLAY), on_stop (STOP), stage_open (OPENED) and |
| 79 | + # stage_close (CLOSED). Older Isaac Sim builds may not expose this API, so |
| 80 | + # fall back gracefully. |
| 81 | + original_class = _get_original_simulation_manager_class(original_module, PhysxManager) |
| 82 | + if original_class is not None and original_class is not PhysxManager: |
| 83 | + try: |
| 84 | + original_class.enable_all_default_callbacks(False) |
| 85 | + except Exception: |
| 86 | + # Defensive: API changed or original class never finished startup. |
| 87 | + # Manually clear the subscription handles if they exist so any |
| 88 | + # remaining references go through the dead-callback path. |
| 89 | + for attr in ( |
| 90 | + "_default_callback_warm_start", |
| 91 | + "_default_callback_on_stop", |
| 92 | + "_default_callback_stage_open", |
| 93 | + "_default_callback_stage_close", |
| 94 | + ): |
| 95 | + if hasattr(original_class, attr): |
| 96 | + setattr(original_class, attr, None) |
| 97 | + |
| 98 | + original_module.SimulationManager = PhysxManager |
| 99 | + original_module.IsaacEvents = IsaacEvents |
| 100 | + |
| 101 | + |
| 102 | +def _get_original_simulation_manager_class(original_module: ModuleType, physx_manager: type) -> type | None: |
| 103 | + """Return Isaac Sim's implementation class, including after a module-level patch.""" |
| 104 | + original_class = getattr(original_module, "SimulationManager", None) |
| 105 | + if original_class is not physx_manager: |
| 106 | + return original_class |
| 107 | + |
| 108 | + implementation_module = sys.modules.get("isaacsim.core.simulation_manager.impl.simulation_manager") |
| 109 | + return getattr(implementation_module, "SimulationManager", None) |
| 110 | + |
| 111 | + |
| 112 | +def _get_kit_extension_manager() -> Any | None: |
| 113 | + """Return Kit's extension manager when Kit is running.""" |
| 114 | + try: |
| 115 | + import omni.kit.app # noqa: PLC0415 |
| 116 | + except Exception: |
| 117 | + return None |
| 118 | + |
| 119 | + with suppress(RuntimeError): |
| 120 | + app = omni.kit.app.get_app() |
| 121 | + return app.get_extension_manager() |
| 122 | + return None |
| 123 | + |
| 124 | + |
| 125 | +def _subscribe_to_simulation_manager_enable() -> None: |
| 126 | + """Patch Isaac Sim's manager if it is enabled after PhysxManager starts.""" |
| 127 | + global _SIMULATION_MANAGER_ENABLE_HOOK |
| 128 | + if _SIMULATION_MANAGER_ENABLE_HOOK is not None: |
| 129 | + return |
| 130 | + |
| 131 | + extension_manager = _get_kit_extension_manager() |
| 132 | + if extension_manager is None: |
| 133 | + return |
| 134 | + |
| 135 | + _SIMULATION_MANAGER_ENABLE_HOOK = extension_manager.subscribe_to_extension_enable( |
| 136 | + on_enable_fn=lambda _: _patch_isaacsim_simulation_manager(), |
| 137 | + on_disable_fn=lambda _: None, |
| 138 | + ext_name="isaacsim.core.simulation_manager", |
| 139 | + hook_name="isaaclab_physx simulation manager lifecycle patch", |
| 140 | + ) |
17 | 141 |
|
18 | | -_simulation_manager_patch = _SimulationManagerPatch() |
19 | 142 |
|
| 143 | +def _enable_isaacsim_simulation_manager_if_available() -> None: |
| 144 | + """Enable Isaac Sim's manager before PhysxManager creates tensor views.""" |
| 145 | + extension_manager = _get_kit_extension_manager() |
| 146 | + if extension_manager is None: |
| 147 | + return |
20 | 148 |
|
21 | | -def _patch_isaacsim_simulation_manager() -> None: |
22 | | - """Patch Isaac Sim's ``SimulationManager`` to use Isaac Lab's PhysX manager.""" |
23 | | - _simulation_manager_patch.claim_physics_lifecycle() |
| 149 | + with suppress(Exception): |
| 150 | + if not extension_manager.is_extension_enabled("isaacsim.core.simulation_manager"): |
| 151 | + extension_manager.set_extension_enabled_immediate("isaacsim.core.simulation_manager", True) |
24 | 152 |
|
25 | 153 |
|
26 | 154 | _patch_isaacsim_simulation_manager() |
0 commit comments