Skip to content
1 change: 1 addition & 0 deletions apps/isaaclab.python.headless.kit
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
1 change: 1 addition & 0 deletions apps/isaaclab.python.headless.rendering.kit
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions apps/isaaclab.python.kit
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions apps/isaaclab.python.rendering.kit
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions apps/isaaclab.python.xr.openxr.headless.kit
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions apps/isaaclab.python.xr.openxr.kit
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions source/isaaclab/changelog.d/jichuanh-debug-vis-registry.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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 visualizer
dispatches them, rather than on every Kit post-update tick, so they no longer run when nothing
is consuming 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.

* 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.
15 changes: 2 additions & 13 deletions source/isaaclab/isaaclab/envs/direct_marl_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
14 changes: 2 additions & 12 deletions source/isaaclab/isaaclab/envs/direct_rl_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__)

Expand Down Expand Up @@ -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

Expand Down
14 changes: 11 additions & 3 deletions source/isaaclab/isaaclab/markers/vis_marker_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
8 changes: 6 additions & 2 deletions source/isaaclab/isaaclab/sim/simulation_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
27 changes: 14 additions & 13 deletions source/isaaclab/isaaclab/ui/widgets/manager_live_visualizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from __future__ import annotations

import logging
import weakref
from dataclasses import MISSING
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions source/isaaclab/test/envs/test_direct_marl_env_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
68 changes: 68 additions & 0 deletions source/isaaclab/test/markers/test_vis_marker_registry.py
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading