From 778135a66fb8ca951fa7ffc46adef01d3284d234 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Mon, 10 Aug 2026 23:20:38 -0700 Subject: [PATCH 1/4] Disable Kit developer warnings in Isaac Lab app experiences Isaac Lab's experience files were adapted from isaacsim.exp.base.kit but did not carry over its `enableDeveloperWarnings = false` line. Every Isaac Sim experience disables developer warnings to reduce log noise; all six Isaac Lab experiences left them enabled, and Isaac Lab always launches its own experience rather than inheriting Isaac Sim's. `carb.deprecated()` logs at warning level only when `/app/enableDeveloperWarnings` is truthy, and formats every message with a `Callstack:` block. Routine Kit deprecation notices therefore appeared as traceback-shaped text in Isaac Lab logs, which QA automation cannot distinguish from real errors without also ignoring genuine tracebacks. The setting has to be applied at app boot: `AppLauncher._create_app()` constructs `SimulationApp` before `_load_extensions()` runs, so Isaac Lab's central carb settings block executes after these warnings have already fired. The experience files are the only hook early enough. Measured with `play.py --task Isaac-Reach-UR10 --visualizer kit`: warnings carrying a callstack drop from 3 to 0 and total warnings from 35 to 5. The suppressed diagnostics stay available per run via `--kit_args "--/app/enableDeveloperWarnings=true"`. --- apps/isaaclab.python.headless.kit | 1 + apps/isaaclab.python.headless.rendering.kit | 1 + apps/isaaclab.python.kit | 1 + apps/isaaclab.python.rendering.kit | 1 + apps/isaaclab.python.xr.openxr.headless.kit | 1 + apps/isaaclab.python.xr.openxr.kit | 1 + 6 files changed, 6 insertions(+) diff --git a/apps/isaaclab.python.headless.kit b/apps/isaaclab.python.headless.kit index d67973ae28f4..0a2f2133f169 100644 --- a/apps/isaaclab.python.headless.kit +++ b/apps/isaaclab.python.headless.kit @@ -16,6 +16,7 @@ app.versionFile = "${exe-path}/VERSION" app.folder = "${exe-path}/" app.name = "IsaacLab" app.version = "3.0.0" +app.enableDeveloperWarnings = false # disable developer warnings to reduce log noise log.level = "Warn" # Suppress third-party debug/info noise log.outputStreamLevel = "Warn" diff --git a/apps/isaaclab.python.headless.rendering.kit b/apps/isaaclab.python.headless.rendering.kit index 1ff78e19b0b6..29629c53fe66 100644 --- a/apps/isaaclab.python.headless.rendering.kit +++ b/apps/isaaclab.python.headless.rendering.kit @@ -40,6 +40,7 @@ app.versionFile = "${exe-path}/VERSION" app.folder = "${exe-path}/" app.name = "IsaacLab" app.version = "3.0.0" +app.enableDeveloperWarnings = false # disable developer warnings to reduce log noise ### FSD app.useFabricSceneDelegate = true diff --git a/apps/isaaclab.python.kit b/apps/isaaclab.python.kit index 1baa493817bc..99aaec66adf2 100644 --- a/apps/isaaclab.python.kit +++ b/apps/isaaclab.python.kit @@ -133,6 +133,7 @@ name = "IsaacLab" version = "3.0.0" versionFile = "${exe-path}/VERSION" content.emptyStageOnStart = true +enableDeveloperWarnings = false # disable developer warnings to reduce log noise fastShutdown = true file.ignoreUnsavedOnExit = true font.file = "${fonts}/OpenSans-SemiBold.ttf" diff --git a/apps/isaaclab.python.rendering.kit b/apps/isaaclab.python.rendering.kit index 1b95927a22e2..f4c39e700674 100644 --- a/apps/isaaclab.python.rendering.kit +++ b/apps/isaaclab.python.rendering.kit @@ -32,6 +32,7 @@ app.versionFile = "${exe-path}/VERSION" app.folder = "${exe-path}/" app.name = "IsaacLab" app.version = "3.0.0" +app.enableDeveloperWarnings = false # disable developer warnings to reduce log noise ### FSD app.useFabricSceneDelegate = true diff --git a/apps/isaaclab.python.xr.openxr.headless.kit b/apps/isaaclab.python.xr.openxr.headless.kit index 293dd55dfab4..fe750978e41d 100644 --- a/apps/isaaclab.python.xr.openxr.headless.kit +++ b/apps/isaaclab.python.xr.openxr.headless.kit @@ -16,6 +16,7 @@ app.versionFile = "${exe-path}/VERSION" app.folder = "${exe-path}/" app.name = "IsaacLab" app.version = "3.0.0" +app.enableDeveloperWarnings = false # disable developer warnings to reduce log noise ### FSD app.useFabricSceneDelegate = true diff --git a/apps/isaaclab.python.xr.openxr.kit b/apps/isaaclab.python.xr.openxr.kit index 42b10139bfc6..abb271b5fa7a 100644 --- a/apps/isaaclab.python.xr.openxr.kit +++ b/apps/isaaclab.python.xr.openxr.kit @@ -16,6 +16,7 @@ app.versionFile = "${exe-path}/VERSION" app.folder = "${exe-path}/" app.name = "IsaacLab" app.version = "3.0.0" +app.enableDeveloperWarnings = false # disable developer warnings to reduce log noise ### async rendering settings # omni.replicator.asyncRendering needs to be false for external camera rendering From 04ca6ef84a997b204f86ecd56ce9d4c5561cb87d Mon Sep 17 00:00:00 2001 From: jichuanh Date: Tue, 11 Aug 2026 10:02:36 -0700 Subject: [PATCH 2/4] Replace the deprecated Kit post-update event stream for debug visualization `IApp.get_post_update_event_stream` is deprecated in favour of Events 2.0, and it was the last deprecated Kit API Isaac Lab called. The seven remaining call sites now register through the simulation context's visualization marker registry, which is what assets, sensors and the non-experimental managers already use, so this adopts an existing mechanism rather than introducing one. The two `isaaclab_experimental` managers were byte-for-byte copies of their core twins from before those were migrated. This also fixes debug visualization in kitless mode. `omni.kit.app` was imported only when Kit is present but referenced unconditionally, so enabling debug visualization without Kit raised `NameError: name 'omni' is not defined` in the direct environments and `ModuleNotFoundError: No module named 'omni.kit'` in the Warp-frontend managers. The registry path has no Kit dependency. Verified against develop for all five affected classes. Behaviour change: debug visualization callbacks now run when a marker-capable visualizer dispatches them instead of on every Kit post-update tick, so they no longer run when nothing is drawing them. This matches the existing registry call sites. The handle returned by the registry is a string id rather than a subscription object, so the two `__del__` methods that called `unsubscribe()` on it are updated to clear through the registry, using the same defensive attribute lookup as the core managers so teardown is safe during interpreter shutdown. --- .../jichuanh-debug-vis-registry.minor.rst | 18 ++++++++ .../isaaclab/isaaclab/envs/direct_marl_env.py | 15 +------ .../isaaclab/isaaclab/envs/direct_rl_env.py | 14 +----- .../ui/widgets/manager_live_visualizer.py | 27 ++++++------ .../test/envs/test_direct_marl_env_unit.py | 43 +++++++++++++++++++ .../jichuanh-debug-vis-registry.minor.rst | 15 +++++++ .../envs/direct_rl_env_warp.py | 12 +----- .../managers/action_manager.py | 20 +++------ .../managers/command_manager.py | 20 +++------ 9 files changed, 110 insertions(+), 74 deletions(-) create mode 100644 source/isaaclab/changelog.d/jichuanh-debug-vis-registry.minor.rst create mode 100644 source/isaaclab_experimental/changelog.d/jichuanh-debug-vis-registry.minor.rst diff --git a/source/isaaclab/changelog.d/jichuanh-debug-vis-registry.minor.rst b/source/isaaclab/changelog.d/jichuanh-debug-vis-registry.minor.rst new file mode 100644 index 000000000000..02f39ada92dd --- /dev/null +++ b/source/isaaclab/changelog.d/jichuanh-debug-vis-registry.minor.rst @@ -0,0 +1,18 @@ +Changed +^^^^^^^ + +* Changed :meth:`~isaaclab.envs.DirectRLEnv.set_debug_vis` and + :meth:`~isaaclab.envs.DirectMARLEnv.set_debug_vis`, and the + :class:`~isaaclab.ui.widgets.ManagerLiveVisualizer` debug visualization toggles, to register + their callbacks through the simulation context's visualization marker registry instead of the + deprecated Kit ``IApp.get_post_update_event_stream`` API. This matches how assets, sensors and + the managers already register. Debug visualization callbacks now run when a marker-capable + visualizer dispatches them, rather than on every Kit post-update tick, so they no longer run + when nothing is drawing them. + +Fixed +^^^^^ + +* Fixed debug visualization failing in kitless mode. Enabling it raised + ``NameError: name 'omni' is not defined`` because ``omni.kit.app`` is imported only when Kit is + present but was used unconditionally. The registry path has no Kit dependency. diff --git a/source/isaaclab/isaaclab/envs/direct_marl_env.py b/source/isaaclab/isaaclab/envs/direct_marl_env.py index 3ca2ce181492..44c94c8d9b4f 100644 --- a/source/isaaclab/isaaclab/envs/direct_marl_env.py +++ b/source/isaaclab/isaaclab/envs/direct_marl_env.py @@ -9,7 +9,6 @@ import logging import math import sys -import weakref from abc import abstractmethod from collections.abc import Sequence from dataclasses import MISSING @@ -19,11 +18,6 @@ import numpy as np import torch -from isaaclab.utils.version import has_kit - -if has_kit(): - import omni.kit.app - from isaaclab.managers import EventManager from isaaclab.scene import InteractiveScene from isaaclab.sim import SimulationContext @@ -657,15 +651,10 @@ def set_debug_vis(self, debug_vis: bool) -> bool: if debug_vis: # create a subscriber for the post update event if it doesn't exist if self._debug_vis_handle is None: - app_interface = omni.kit.app.get_app_interface() - self._debug_vis_handle = app_interface.get_post_update_event_stream().create_subscription_to_pop( - lambda event, obj=weakref.proxy(self): obj._debug_vis_callback(event) - ) + self._debug_vis_handle = self.sim.vis_marker_registry.add_debug_vis_callback(self) else: # remove the subscriber if it exists - if self._debug_vis_handle is not None: - self._debug_vis_handle.unsubscribe() - self._debug_vis_handle = None + self.sim.vis_marker_registry.clear_debug_vis_callback(self) # return success return True diff --git a/source/isaaclab/isaaclab/envs/direct_rl_env.py b/source/isaaclab/isaaclab/envs/direct_rl_env.py index b002cf79b901..b81172d6851f 100644 --- a/source/isaaclab/isaaclab/envs/direct_rl_env.py +++ b/source/isaaclab/isaaclab/envs/direct_rl_env.py @@ -10,7 +10,6 @@ import math import sys import warnings -import weakref from abc import abstractmethod from collections.abc import Sequence from dataclasses import MISSING @@ -28,16 +27,12 @@ from isaaclab.utils.noise import NoiseModel from isaaclab.utils.seed import configure_seed from isaaclab.utils.timer import Timer -from isaaclab.utils.version import has_kit from .common import VecEnvObs, VecEnvStepReturn, _apply_deprecated_viewer_cfg from .direct_rl_env_cfg import DirectRLEnvCfg from .utils.spaces import sample_space, spec_to_gym_space from .utils.video_recorder import VideoRecorder -if has_kit(): - import omni.kit.app - # import logger logger = logging.getLogger(__name__) @@ -655,15 +650,10 @@ def set_debug_vis(self, debug_vis: bool) -> bool: if debug_vis: # create a subscriber for the post update event if it doesn't exist if self._debug_vis_handle is None: - app_interface = omni.kit.app.get_app_interface() - self._debug_vis_handle = app_interface.get_post_update_event_stream().create_subscription_to_pop( - lambda event, obj=weakref.proxy(self): obj._debug_vis_callback(event) - ) + self._debug_vis_handle = self.sim.vis_marker_registry.add_debug_vis_callback(self) else: # remove the subscriber if it exists - if self._debug_vis_handle is not None: - self._debug_vis_handle.unsubscribe() - self._debug_vis_handle = None + self.sim.vis_marker_registry.clear_debug_vis_callback(self) # return success return True diff --git a/source/isaaclab/isaaclab/ui/widgets/manager_live_visualizer.py b/source/isaaclab/isaaclab/ui/widgets/manager_live_visualizer.py index 4136b7cdd0e6..5d02550b395b 100644 --- a/source/isaaclab/isaaclab/ui/widgets/manager_live_visualizer.py +++ b/source/isaaclab/isaaclab/ui/widgets/manager_live_visualizer.py @@ -6,7 +6,6 @@ from __future__ import annotations import logging -import weakref from dataclasses import MISSING from typing import TYPE_CHECKING @@ -223,14 +222,15 @@ def _set_debug_vis_impl(self, debug_vis: bool): if debug_vis: # if enabled create a subscriber for the post update event if it doesn't exist if not hasattr(self, "_debug_vis_handle") or self._debug_vis_handle is None: - app_interface = omni.kit.app.get_app_interface() - self._debug_vis_handle = app_interface.get_post_update_event_stream().create_subscription_to_pop( - lambda event, obj=weakref.proxy(self): obj._debug_vis_callback(event) - ) + sim_ctx = SimulationContext.instance() + if sim_ctx is not None: + self._debug_vis_handle = sim_ctx.vis_marker_registry.add_debug_vis_callback(self) else: # if disabled remove the subscriber if it exists - if self._debug_vis_handle is not None: - self._debug_vis_handle.unsubscribe() + sim_ctx = SimulationContext.instance() + if sim_ctx is not None: + sim_ctx.vis_marker_registry.clear_debug_vis_callback(self) + else: self._debug_vis_handle = None self._vis_frame.visible = False @@ -393,13 +393,14 @@ def _set_debug_vis_impl(self, debug_vis: bool): if debug_vis: if not hasattr(self, "_debug_vis_handle") or self._debug_vis_handle is None: - app_interface = omni.kit.app.get_app_interface() - self._debug_vis_handle = app_interface.get_post_update_event_stream().create_subscription_to_pop( - lambda event, obj=weakref.proxy(self): obj._debug_vis_callback(event) - ) + sim_ctx = SimulationContext.instance() + if sim_ctx is not None: + self._debug_vis_handle = sim_ctx.vis_marker_registry.add_debug_vis_callback(self) else: - if self._debug_vis_handle is not None: - self._debug_vis_handle.unsubscribe() + sim_ctx = SimulationContext.instance() + if sim_ctx is not None: + sim_ctx.vis_marker_registry.clear_debug_vis_callback(self) + else: self._debug_vis_handle = None self._vis_frame.visible = False return diff --git a/source/isaaclab/test/envs/test_direct_marl_env_unit.py b/source/isaaclab/test/envs/test_direct_marl_env_unit.py index 4a918791254f..6869dc801b1c 100644 --- a/source/isaaclab/test/envs/test_direct_marl_env_unit.py +++ b/source/isaaclab/test/envs/test_direct_marl_env_unit.py @@ -10,12 +10,14 @@ from __future__ import annotations +import inspect from types import SimpleNamespace import gymnasium as gym import pytest from isaaclab.envs import DirectMARLEnv, DirectMARLEnvCfg +from isaaclab.markers.vis_marker_registry import VisMarkerRegistry from isaaclab.test.env_cfgs import make_empty_direct_marl_env_cfg pytestmark = pytest.mark.unit @@ -51,3 +53,44 @@ def test_agent_and_space_configuration(): assert env.action_spaces["agent_1"].shape == (2,) assert isinstance(env.state_space, gym.spaces.Box) assert env.state_space.shape == (7,) + + +class _DebugVisStubMARLEnv(_StubMARLEnv): + """Stub whose debug visualization is implemented, so ``set_debug_vis`` runs its handle logic.""" + + def __init__(self, cfg: DirectMARLEnvCfg) -> None: + super().__init__(cfg) + # mirrors what DirectMARLEnv.__init__ derives, which the stub skips + self.has_debug_vis_implementation = "NotImplementedError" not in inspect.getsource(self._set_debug_vis_impl) + self._debug_vis_handle = None + self.sim = SimpleNamespace(device=cfg.sim.device, vis_marker_registry=VisMarkerRegistry()) + self.callback_count = 0 + + def _set_debug_vis_impl(self, debug_vis: bool) -> None: + pass + + def _debug_vis_callback(self, event) -> None: + self.callback_count += 1 + + +def test_set_debug_vis_registers_without_kit(): + """Debug visualization registers through the marker registry, so it needs no Kit application. + + Guards against reintroducing the deprecated ``IApp.get_post_update_event_stream`` subscription, + which raised ``NameError`` in kitless mode because ``omni.kit.app`` is only imported when Kit is + present. + """ + env = _DebugVisStubMARLEnv(make_empty_direct_marl_env_cfg(device="cpu")) + registry = env.sim.vis_marker_registry + + assert env.set_debug_vis(True) is True + assert isinstance(env._debug_vis_handle, str) + + registry.dispatch_callbacks() + assert env.callback_count == 1 + + env.set_debug_vis(False) + assert env._debug_vis_handle is None + + registry.dispatch_callbacks() + assert env.callback_count == 1 diff --git a/source/isaaclab_experimental/changelog.d/jichuanh-debug-vis-registry.minor.rst b/source/isaaclab_experimental/changelog.d/jichuanh-debug-vis-registry.minor.rst new file mode 100644 index 000000000000..a73ba9d5eafd --- /dev/null +++ b/source/isaaclab_experimental/changelog.d/jichuanh-debug-vis-registry.minor.rst @@ -0,0 +1,15 @@ +Changed +^^^^^^^ + +* Changed the Warp-frontend ``ActionTerm``, ``CommandTerm`` and ``DirectRLEnvWarp`` debug + visualization toggles to register their callbacks through the simulation context's visualization + marker registry instead of the deprecated Kit ``IApp.get_post_update_event_stream`` API, matching + their non-experimental counterparts. Callbacks now run when a marker-capable visualizer + dispatches them rather than on every Kit post-update tick. + +Fixed +^^^^^ + +* Fixed debug visualization failing in kitless mode. Enabling it raised + ``ModuleNotFoundError: No module named 'omni.kit'`` because ``omni.kit.app`` was imported inside + the toggle. The registry path has no Kit dependency. diff --git a/source/isaaclab_experimental/isaaclab_experimental/envs/direct_rl_env_warp.py b/source/isaaclab_experimental/isaaclab_experimental/envs/direct_rl_env_warp.py index 39d3c658fed6..33888dda519f 100644 --- a/source/isaaclab_experimental/isaaclab_experimental/envs/direct_rl_env_warp.py +++ b/source/isaaclab_experimental/isaaclab_experimental/envs/direct_rl_env_warp.py @@ -10,7 +10,6 @@ import logging import math import os -import weakref from abc import abstractmethod from dataclasses import MISSING from typing import Any, ClassVar @@ -643,19 +642,12 @@ def set_debug_vis(self, debug_vis: bool) -> bool: self._set_debug_vis_impl(debug_vis) # toggle debug visualization handles if debug_vis: - import omni.kit.app - # create a subscriber for the post update event if it doesn't exist if self._debug_vis_handle is None: - app_interface = omni.kit.app.get_app_interface() - self._debug_vis_handle = app_interface.get_post_update_event_stream().create_subscription_to_pop( - lambda event, obj=weakref.proxy(self): obj._debug_vis_callback(event) - ) + self._debug_vis_handle = self.sim.vis_marker_registry.add_debug_vis_callback(self) else: # remove the subscriber if it exists - if self._debug_vis_handle is not None: - self._debug_vis_handle.unsubscribe() - self._debug_vis_handle = None + self.sim.vis_marker_registry.clear_debug_vis_callback(self) # return success return True diff --git a/source/isaaclab_experimental/isaaclab_experimental/managers/action_manager.py b/source/isaaclab_experimental/isaaclab_experimental/managers/action_manager.py index 902c74d6d095..b85748504867 100644 --- a/source/isaaclab_experimental/isaaclab_experimental/managers/action_manager.py +++ b/source/isaaclab_experimental/isaaclab_experimental/managers/action_manager.py @@ -9,7 +9,6 @@ import inspect import re -import weakref from abc import abstractmethod from collections.abc import Sequence from typing import TYPE_CHECKING, Any @@ -65,9 +64,11 @@ def __init__(self, cfg: ActionTermCfg, env: ManagerBasedEnv): def __del__(self): """Unsubscribe from the callbacks.""" - if self._debug_vis_handle: - self._debug_vis_handle.unsubscribe() - self._debug_vis_handle = None + env = getattr(self, "_env", None) + sim = getattr(env, "sim", None) + registry = getattr(sim, "vis_marker_registry", None) + if registry is not None: + registry.clear_debug_vis_callback(self) """ Properties. @@ -128,23 +129,16 @@ def set_debug_vis(self, debug_vis: bool) -> bool: if not self.has_debug_vis_implementation: return False - import omni.kit.app - # toggle debug visualization objects self._set_debug_vis_impl(debug_vis) # toggle debug visualization handles if debug_vis: # create a subscriber for the post update event if it doesn't exist if self._debug_vis_handle is None: - app_interface = omni.kit.app.get_app_interface() - self._debug_vis_handle = app_interface.get_post_update_event_stream().create_subscription_to_pop( - lambda event, obj=weakref.proxy(self): obj._debug_vis_callback(event) - ) + self._debug_vis_handle = self._env.sim.vis_marker_registry.add_debug_vis_callback(self) else: # remove the subscriber if it exists - if self._debug_vis_handle is not None: - self._debug_vis_handle.unsubscribe() - self._debug_vis_handle = None + self._env.sim.vis_marker_registry.clear_debug_vis_callback(self) # return success return True diff --git a/source/isaaclab_experimental/isaaclab_experimental/managers/command_manager.py b/source/isaaclab_experimental/isaaclab_experimental/managers/command_manager.py index dfafeef5e206..1a94639c97f9 100644 --- a/source/isaaclab_experimental/isaaclab_experimental/managers/command_manager.py +++ b/source/isaaclab_experimental/isaaclab_experimental/managers/command_manager.py @@ -8,7 +8,6 @@ from __future__ import annotations import inspect -import weakref from abc import abstractmethod from collections.abc import Sequence from typing import TYPE_CHECKING @@ -125,9 +124,11 @@ def __init__(self, cfg: CommandTermCfg, env: ManagerBasedRLEnv): def __del__(self): """Unsubscribe from the callbacks.""" - if self._debug_vis_handle: - self._debug_vis_handle.unsubscribe() - self._debug_vis_handle = None + env = getattr(self, "_env", None) + sim = getattr(env, "sim", None) + registry = getattr(sim, "vis_marker_registry", None) + if registry is not None: + registry.clear_debug_vis_callback(self) """ Properties @@ -180,17 +181,10 @@ def set_debug_vis(self, debug_vis: bool) -> bool: return False # create a subscriber for the post update event if it doesn't exist if self._debug_vis_handle is None: - import omni.kit.app - - app_interface = omni.kit.app.get_app_interface() - self._debug_vis_handle = app_interface.get_post_update_event_stream().create_subscription_to_pop( - lambda event, obj=weakref.proxy(self): obj._debug_vis_callback(event) - ) + self._debug_vis_handle = sim_context.vis_marker_registry.add_debug_vis_callback(self) else: # remove the subscriber if it exists - if self._debug_vis_handle is not None: - self._debug_vis_handle.unsubscribe() - self._debug_vis_handle = None + self._env.sim.vis_marker_registry.clear_debug_vis_callback(self) # return success return True From 0ff85760d854d26579bc9f028443d7043cadba22 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Tue, 11 Aug 2026 10:49:23 -0700 Subject: [PATCH 3/4] Address review findings on the debug visualization registry migration Four fixes from the codex review of this branch. `CommandTerm.set_debug_vis` guarded on `SimulationContext.has_omniverse_visualizer()`, which does not exist anywhere in the tree, so the call raised `AttributeError` before reaching any registration. Drop the guard and use the environment-owned registry as the core `CommandTerm` does. Marker callbacks and live-plot panels share one registry, but `update_visualizers` gated dispatch on marker support alone. The two visualizer flags are independent, so a visualizer with markers disabled and live plots enabled registered a panel that never updated. Gate on either capability. A callback whose owner was garbage collected without deregistering left a stale weak proxy that raised `ReferenceError` and aborted the entire dispatch, taking every other visualizer's callbacks with it. Drop stale entries instead. Both changelog fragments were `.minor`, but this adds no public API, so retier to patch per the contributing guide. Adds unit coverage for the registry's add/clear contract and for the stale-owner case, which fails without the dispatch fix. --- ...or.rst => jichuanh-debug-vis-registry.rst} | 13 +++- .../isaaclab/markers/vis_marker_registry.py | 14 +++- .../isaaclab/sim/simulation_context.py | 8 ++- .../test/markers/test_vis_marker_registry.py | 68 +++++++++++++++++++ ...or.rst => jichuanh-debug-vis-registry.rst} | 7 +- .../managers/command_manager.py | 8 +-- 6 files changed, 101 insertions(+), 17 deletions(-) rename source/isaaclab/changelog.d/{jichuanh-debug-vis-registry.minor.rst => jichuanh-debug-vis-registry.rst} (56%) create mode 100644 source/isaaclab/test/markers/test_vis_marker_registry.py rename source/isaaclab_experimental/changelog.d/{jichuanh-debug-vis-registry.minor.rst => jichuanh-debug-vis-registry.rst} (64%) diff --git a/source/isaaclab/changelog.d/jichuanh-debug-vis-registry.minor.rst b/source/isaaclab/changelog.d/jichuanh-debug-vis-registry.rst similarity index 56% rename from source/isaaclab/changelog.d/jichuanh-debug-vis-registry.minor.rst rename to source/isaaclab/changelog.d/jichuanh-debug-vis-registry.rst index 02f39ada92dd..fb8031358a78 100644 --- a/source/isaaclab/changelog.d/jichuanh-debug-vis-registry.minor.rst +++ b/source/isaaclab/changelog.d/jichuanh-debug-vis-registry.rst @@ -6,9 +6,9 @@ Changed :class:`~isaaclab.ui.widgets.ManagerLiveVisualizer` debug visualization toggles, to register their callbacks through the simulation context's visualization marker registry instead of the deprecated Kit ``IApp.get_post_update_event_stream`` API. This matches how assets, sensors and - the managers already register. Debug visualization callbacks now run when a marker-capable - visualizer dispatches them, rather than on every Kit post-update tick, so they no longer run - when nothing is drawing them. + the managers already register. Debug visualization callbacks now run when a visualizer + dispatches them, rather than on every Kit post-update tick, so they no longer run when nothing + is consuming them. Fixed ^^^^^ @@ -16,3 +16,10 @@ Fixed * Fixed debug visualization failing in kitless mode. Enabling it raised ``NameError: name 'omni' is not defined`` because ``omni.kit.app`` is imported only when Kit is present but was used unconditionally. The registry path has no Kit dependency. + +* Fixed live-plot panels never updating when the active visualizer had markers disabled and live + plots enabled. Marker callbacks and live-plot panels share one registry, but dispatch was gated + on marker support alone even though the two visualizer flags are independent. + +* Fixed a visualization marker callback whose owner had been garbage collected aborting the whole + dispatch with ``ReferenceError``. Stale callbacks are now dropped instead. diff --git a/source/isaaclab/isaaclab/markers/vis_marker_registry.py b/source/isaaclab/isaaclab/markers/vis_marker_registry.py index a50d26713971..d7edd8e6952b 100644 --- a/source/isaaclab/isaaclab/markers/vis_marker_registry.py +++ b/source/isaaclab/isaaclab/markers/vis_marker_registry.py @@ -49,9 +49,17 @@ def remove_callback(self, callback_id: str) -> None: self._callbacks.pop(callback_id, None) def dispatch_callbacks(self, event: Any = None) -> None: - """Invoke all registered visualization marker callbacks.""" - for callback in list(self._callbacks.values()): - callback(event) + """Invoke all registered visualization marker callbacks. + + Callbacks hold a weak proxy to their owner. An owner collected without + deregistering leaves a stale entry whose proxy raises on use, so drop those + rather than letting one dead owner abort the whole dispatch. + """ + for callback_id, callback in list(self._callbacks.items()): + try: + callback(event) + except ReferenceError: + self._callbacks.pop(callback_id, None) def set_group(self, group_id: str, state: Any) -> None: """Set or replace one visualization marker group state.""" diff --git a/source/isaaclab/isaaclab/sim/simulation_context.py b/source/isaaclab/isaaclab/sim/simulation_context.py index 474c28641ac2..d8dde53e8f7a 100644 --- a/source/isaaclab/isaaclab/sim/simulation_context.py +++ b/source/isaaclab/isaaclab/sim/simulation_context.py @@ -848,8 +848,12 @@ def update_visualizers(self, dt: float, skip_app_pumping: bool = False) -> None: self.physics_manager.forward() # Marker callbacks update VisualizationMarkers state; visualizer step() - # consumes that state later in this method. - if any(viz.supports_markers() for viz in self._visualizers): + # consumes that state later in this method. Live-plot panels register in the same + # registry and their flag is independent of markers, so gate on either capability. + if any( + viz.supports_markers() or (viz.supports_live_plots() and getattr(viz.cfg, "enable_live_plots", True)) + for viz in self._visualizers + ): self.vis_marker_registry.dispatch_callbacks() visualizers_to_remove = [] diff --git a/source/isaaclab/test/markers/test_vis_marker_registry.py b/source/isaaclab/test/markers/test_vis_marker_registry.py new file mode 100644 index 000000000000..210ef8af5f58 --- /dev/null +++ b/source/isaaclab/test/markers/test_vis_marker_registry.py @@ -0,0 +1,68 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Unit tests for the visualization marker registry.""" + +from __future__ import annotations + +import gc + +import pytest + +from isaaclab.markers.vis_marker_registry import VisMarkerRegistry + +pytestmark = pytest.mark.unit + + +class _Owner: + """Minimal debug-visualization owner; a real class so it can be weak-referenced.""" + + def __init__(self) -> None: + self.calls = 0 + + def _debug_vis_callback(self, event) -> None: + self.calls += 1 + + +def test_add_and_clear_debug_vis_callback(): + """Registering returns an id, and clearing removes it and resets the owner's handle.""" + registry = VisMarkerRegistry() + owner = _Owner() + + owner._debug_vis_handle = registry.add_debug_vis_callback(owner) + assert isinstance(owner._debug_vis_handle, str) + + registry.dispatch_callbacks() + assert owner.calls == 1 + + registry.clear_debug_vis_callback(owner) + assert owner._debug_vis_handle is None + + registry.dispatch_callbacks() + assert owner.calls == 1 + + +def test_dispatch_drops_callbacks_whose_owner_was_collected(): + """A collected owner must not abort dispatch for the callbacks that are still live. + + Callbacks hold a weak proxy, so an owner freed without deregistering leaves an entry + that raises ``ReferenceError`` when invoked. + """ + registry = VisMarkerRegistry() + live = _Owner() + dead = _Owner() + + registry.add_debug_vis_callback(live) + registry.add_debug_vis_callback(dead) + + del dead + gc.collect() + + registry.dispatch_callbacks() + assert live.calls == 1 + + # the stale entry is gone, so later dispatches keep working + registry.dispatch_callbacks() + assert live.calls == 2 diff --git a/source/isaaclab_experimental/changelog.d/jichuanh-debug-vis-registry.minor.rst b/source/isaaclab_experimental/changelog.d/jichuanh-debug-vis-registry.rst similarity index 64% rename from source/isaaclab_experimental/changelog.d/jichuanh-debug-vis-registry.minor.rst rename to source/isaaclab_experimental/changelog.d/jichuanh-debug-vis-registry.rst index a73ba9d5eafd..1d155bfa68ae 100644 --- a/source/isaaclab_experimental/changelog.d/jichuanh-debug-vis-registry.minor.rst +++ b/source/isaaclab_experimental/changelog.d/jichuanh-debug-vis-registry.rst @@ -4,8 +4,7 @@ Changed * Changed the Warp-frontend ``ActionTerm``, ``CommandTerm`` and ``DirectRLEnvWarp`` debug visualization toggles to register their callbacks through the simulation context's visualization marker registry instead of the deprecated Kit ``IApp.get_post_update_event_stream`` API, matching - their non-experimental counterparts. Callbacks now run when a marker-capable visualizer - dispatches them rather than on every Kit post-update tick. + their non-experimental counterparts. Fixed ^^^^^ @@ -13,3 +12,7 @@ Fixed * Fixed debug visualization failing in kitless mode. Enabling it raised ``ModuleNotFoundError: No module named 'omni.kit'`` because ``omni.kit.app`` was imported inside the toggle. The registry path has no Kit dependency. + +* Fixed ``CommandTerm.set_debug_vis`` raising ``AttributeError`` whenever a command term + implemented debug visualization. It guarded on ``SimulationContext.has_omniverse_visualizer()``, + which does not exist, so the call failed before any callback was registered. diff --git a/source/isaaclab_experimental/isaaclab_experimental/managers/command_manager.py b/source/isaaclab_experimental/isaaclab_experimental/managers/command_manager.py index 1a94639c97f9..e9f0ce3113a9 100644 --- a/source/isaaclab_experimental/isaaclab_experimental/managers/command_manager.py +++ b/source/isaaclab_experimental/isaaclab_experimental/managers/command_manager.py @@ -173,15 +173,9 @@ def set_debug_vis(self, debug_vis: bool) -> bool: self._set_debug_vis_impl(debug_vis) # toggle debug visualization handles if debug_vis: - # only enable debug_vis if omniverse is available - from isaaclab.sim.simulation_context import SimulationContext - - sim_context = SimulationContext.instance() - if not sim_context.has_omniverse_visualizer(): - return False # create a subscriber for the post update event if it doesn't exist if self._debug_vis_handle is None: - self._debug_vis_handle = sim_context.vis_marker_registry.add_debug_vis_callback(self) + self._debug_vis_handle = self._env.sim.vis_marker_registry.add_debug_vis_callback(self) else: # remove the subscriber if it exists self._env.sim.vis_marker_registry.clear_debug_vis_callback(self) From 3dd76f8d8dbec31dbf25bc0946a1bf32a1ff6b40 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Thu, 13 Aug 2026 11:01:51 -0700 Subject: [PATCH 4/4] Fix visualizer orchestration tests for the widened marker-dispatch gate update_visualizers now consults supports_live_plots() so live-plot panels dispatch even when markers are off. _FakeVisualizer is duck-typed rather than a BaseVisualizer subclass and only implemented supports_markers(), so the widened gate raised AttributeError in all five update_visualizers tests. Add the missing protocol method to the stub, give the fake context a marker registry, and cover both halves of the new condition: a live-plot-only visualizer dispatches, and one with live plots disabled does not. --- .../test_simulation_context_visualizers.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/source/isaaclab/test/sim/test_simulation_context_visualizers.py b/source/isaaclab/test/sim/test_simulation_context_visualizers.py index 1c72233204f7..31457db946e4 100644 --- a/source/isaaclab/test/sim/test_simulation_context_visualizers.py +++ b/source/isaaclab/test/sim/test_simulation_context_visualizers.py @@ -23,6 +23,7 @@ from isaaclab_visualizers.rerun.rerun_visualizer_cfg import RerunVisualizerCfg from isaaclab_visualizers.viser.viser_visualizer_cfg import ViserVisualizerCfg +from isaaclab.markers.vis_marker_registry import VisMarkerRegistry from isaaclab.sim.simulation_context import SimulationContext from isaaclab.visualizers.visualizer_cfg import VisualizerCfg @@ -123,6 +124,9 @@ def pumps_app_update(self): def supports_markers(self): return False + def supports_live_plots(self): + return False + def flush_startup_messages(self): pass @@ -132,6 +136,7 @@ def _make_context(visualizers, provider=None): ctx._visualizers = list(visualizers) ctx._scene_data_provider = provider ctx.physics_manager = _FakePhysicsManager() + ctx.vis_marker_registry = VisMarkerRegistry() return ctx @@ -200,6 +205,37 @@ def test_update_visualizers_handles_training_pause_loop(): assert viz.step_calls == [0.0, 0.2] +class _LivePlotVisualizer(_FakeVisualizer): + def __init__(self, *, enable_live_plots: bool = True, **kwargs): + super().__init__(**kwargs) + self.cfg = VisualizerCfg(enable_live_plots=enable_live_plots) + + def supports_live_plots(self): + return True + + +def test_update_visualizers_dispatches_callbacks_for_live_plot_only_visualizer(): + """Live-plot panels share the marker registry, so dispatch must not require marker support.""" + dispatched = [] + ctx = _make_context([_LivePlotVisualizer()], provider=_FakeProvider()) + ctx.vis_marker_registry.add_callback("probe", dispatched.append) + + ctx.update_visualizers(0.1) + + assert len(dispatched) == 1 + + +def test_update_visualizers_skips_dispatch_when_live_plots_disabled(): + """Live-plot support with the flag off consumes nothing, so callbacks stay idle.""" + dispatched = [] + ctx = _make_context([_LivePlotVisualizer(enable_live_plots=False)], provider=_FakeProvider()) + ctx.vis_marker_registry.add_callback("probe", dispatched.append) + + ctx.update_visualizers(0.1) + + assert dispatched == [] + + def test_newton_visualizer_is_initialized_and_rebound_before_capture(): created = [] reset_calls = []