Skip to content

Fix headless visualizer runtime in SimulationContext and DirectRLEnvWarp#5061

Open
IamHBW wants to merge 1 commit intoisaac-sim:developfrom
IamHBW:IamHBW/fix-warp-visualizer
Open

Fix headless visualizer runtime in SimulationContext and DirectRLEnvWarp#5061
IamHBW wants to merge 1 commit intoisaac-sim:developfrom
IamHBW:IamHBW/fix-warp-visualizer

Conversation

@IamHBW
Copy link

@IamHBW IamHBW commented Mar 19, 2026

Description

Follow-up to #4948.

This PR fixes a regression in headless visualizer-only runs. After the
visualizer resolution changes, some existing environment paths still read the
legacy /isaaclab/visualizer boolean instead of the resolved visualizer
state. As a result, rendering could remain disabled even when a visualizer was
active.

This change:

  • mirrors the resolved visualizer enablement back to /isaaclab/visualizer in
    SimulationContext, preserving compatibility for existing code paths that
    still read the legacy key;
  • updates DirectRLEnvWarp to use SimulationContext.is_rendering for render
    decisions;
  • prevents DirectRLEnvWarp from creating a Kit UI window unless a real GUI
    is available;
  • hardens BaseEnvWindow teardown so partial initialization failures do not
    raise AttributeError;
  • adds regression tests covering the legacy visualizer path and the headless
    UI-window behavior.

User-visible impact:

  • headless visualizer-only runs render correctly again in existing environment
    paths;
  • DirectRLEnvWarp no longer attempts to create a Kit UI window during
    headless visualizer-only runs;
  • partially initialized UI windows now clean up safely.

Related to #5015 and #5056.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Test plan

  • ./isaaclab.sh -p -m pytest source/isaaclab/test/sim/ test_simulation_context_visualizers.py
  • ./isaaclab.sh -p -m pytest source/isaaclab/test/envs/ test_direct_rl_env_warp_ui.py

Manual verification:

  • Launch a headless visualizer-only warp environment and verify rendering
    still updates.
  • Verify no Kit UI window is created when GUI support is unavailable.

Mirror resolved visualizer state to the legacy setting so
visualizer-only runs continue rendering in existing env
paths.

Also require a real GUI before DirectRLEnvWarp creates a
Kit UI window and harden BaseEnvWindow teardown after
partial initialization failures. Add regression tests and
update extension changelogs and versions.
@github-actions github-actions bot added bug Something isn't working isaac-lab Related to Isaac Lab team labels Mar 19, 2026
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 19, 2026

Greptile Summary

This PR fixes a regression (follow-up to #4948) where headless visualizer-only runs would skip rendering because some code paths still read the legacy /isaaclab/visualizer boolean rather than the newly resolved visualizer state. It addresses this by mirroring the resolved enablement back to the legacy key in SimulationContext, centralising render decisions in DirectRLEnvWarp to the authoritative sim.is_rendering property, and hardening BaseEnvWindow teardown against partial initialisation.

Key changes:

  • SimulationContext._sync_legacy_visualizer_setting() keeps /isaaclab/visualizer in sync after init and whenever a /isaaclab/visualizer/* setting changes.
  • DirectRLEnvWarp._should_create_ui_window() now gates Kit UI window creation on sim.has_gui, correctly preventing window creation in headless visualizer-only runs.
  • DirectRLEnvWarp.step() now uses self.sim.is_rendering (which covers GUI, offscreen, RTX-sensor setting, visualizers, and XR) instead of the narrow legacy-setting + has_rtx_sensors() combination.
  • BaseEnvWindow.__del__ uses getattr to tolerate objects whose __init__ never completed.
  • Regression tests are added for the legacy-sync logic and for the headless UI-window guard.

Confidence Score: 3/5

  • Mostly safe for headless visualizer and GUI runs, but warrants verification that sim.is_rendering covers RTX-sensor-only workflows before merging to main.
  • The legacy-sync and UI-window guard changes are correct and well-tested. The main risk is the substitution of the dynamic has_rtx_sensors() runtime check with the static /isaaclab/render/rtx_sensors setting inside sim.is_rendering: if those two can diverge (e.g. cameras added after init without updating the setting), the per-step render call in DirectRLEnvWarp.step() would be silently skipped for RTX-sensor workflows. This is a functional regression risk, not a crash risk, but it deserves explicit confirmation or a targeted test before merging.
  • source/isaaclab_experimental/isaaclab_experimental/envs/direct_rl_env_warp.py — the is_rendering substitution in step() should be verified against RTX-sensor-only scenarios.

Important Files Changed

Filename Overview
source/isaaclab/isaaclab/sim/simulation_context.py Adds _sync_legacy_visualizer_setting() to mirror resolved visualizer enablement back to /isaaclab/visualizer on init and on every relevant set_setting call; logic is correct but there is a transient window of stale state when explicit is set before types.
source/isaaclab_experimental/isaaclab_experimental/envs/direct_rl_env_warp.py Replaces ad-hoc legacy-setting + has_rtx_sensors() render check with sim.is_rendering; the new property is more comprehensive but uses the static /isaaclab/render/rtx_sensors setting rather than the dynamic has_rtx_sensors() call, which may miss rendering for dynamically-added RTX cameras.
source/isaaclab/isaaclab/envs/ui/base_env_window.py Pre-initializes ui_window = None and switches __del__ to getattr for safe partial-initialization teardown; straightforward defensive fix with no issues.
source/isaaclab/test/envs/test_direct_rl_env_warp_ui.py New regression tests for UI-window guard and partial-init teardown; negative case only for _should_create_ui_window — missing positive/null-class-type cases.
source/isaaclab/test/sim/test_simulation_context_visualizers.py Adds _FakeSettingsHelper and two regression tests covering the legacy-key sync for cfg-based and CLI-based visualizer configurations; tests are well-structured and cover the important paths.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[SimulationContext.__init__] --> B[_apply_render_cfg_settings]
    B --> C[_sync_legacy_visualizer_setting]
    C --> D{resolve_visualizer_types}
    D -->|returns types list| E[set /isaaclab/visualizer = bool result]

    F[set_setting called with\n/isaaclab/visualizer/* key] -->|not max_worlds| C

    G[DirectRLEnvWarp.__init__] --> H{_should_create_ui_window?}
    H -->|sim.has_gui AND\nui_window_class_type != None| I[Create Kit UI Window]
    H -->|otherwise| J[self._window = None]

    K[DirectRLEnvWarp.step] --> L[is_rendering = sim.is_rendering]
    L --> M{sim.is_rendering}
    M --> N[has_gui\nor offscreen_render\nor rtx_sensors setting\nor visualizers\nor xr_enabled]
    N -->|True| O[sim.render called in loop]
    N -->|False| P[skip render]

    Q[BaseEnvWindow.__del__] --> R[ui_window = getattr self ui_window None]
    R -->|not None| S[destroy window]
    R -->|None| T[no-op safe teardown]
Loading

Last reviewed commit: "Fix headless visuali..."

# note: checked here once to avoid multiple checks within the loop
_has_rtx = hasattr(self.sim, "has_rtx_sensors") and self.sim.has_rtx_sensors()
is_rendering = bool(self.sim.settings.get("/isaaclab/visualizer")) or _has_rtx
is_rendering = self.sim.is_rendering
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Potential rendering regression for RTX-sensor-only runs

The old code used a dynamic self.sim.has_rtx_sensors() call to detect active RTX cameras at runtime:

_has_rtx = hasattr(self.sim, "has_rtx_sensors") and self.sim.has_rtx_sensors()
is_rendering = bool(self.sim.settings.get("/isaaclab/visualizer")) or _has_rtx

The new self.sim.is_rendering property checks self.get_setting("/isaaclab/render/rtx_sensors") (a static setting) rather than has_rtx_sensors(). Importantly, simulation_context.py explicitly notes that has_rtx_sensors is not cached because it changes when camera sensors are created dynamically. If the /isaaclab/render/rtx_sensors setting is not updated when cameras are added at runtime, is_rendering could return False even though has_rtx_sensors() would return True, causing the per-step sim.render() call to be skipped.

For this PR to be fully safe for RTX-sensor workflows, it's worth verifying that the setting is reliably set to True whenever has_rtx_sensors() would return True, or alternatively consider including a has_rtx_sensors() guard in is_rendering itself.

Comment on lines +10 to +15
def test_direct_rl_env_warp_ui_window_requires_gui():
env = object.__new__(DirectRLEnvWarp)
env.sim = type("Sim", (), {"has_gui": False})()
env.cfg = type("Cfg", (), {"ui_window_class_type": object})()

assert env._should_create_ui_window() is False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Positive case for _should_create_ui_window not covered

The test only exercises the path where has_gui=False, confirming the method returns False. It doesn't test the complementary case (has_gui=True, ui_window_class_type is not None) to confirm it returns True, nor the case where has_gui=True but ui_window_class_type is None. Adding these assertions would give better confidence that the guard logic is fully correct:

def test_direct_rl_env_warp_ui_window_requires_gui_true():
    env = object.__new__(DirectRLEnvWarp)
    env.sim = type("Sim", (), {"has_gui": True})()
    env.cfg = type("Cfg", (), {"ui_window_class_type": object})()
    assert env._should_create_ui_window() is True

def test_direct_rl_env_warp_ui_window_none_class_type():
    env = object.__new__(DirectRLEnvWarp)
    env.sim = type("Sim", (), {"has_gui": True})()
    env.cfg = type("Cfg", (), {"ui_window_class_type": None})()
    assert env._should_create_ui_window() is False

Comment on lines +767 to +768
if name.startswith("/isaaclab/visualizer/") and name != "/isaaclab/visualizer/max_worlds":
self._sync_legacy_visualizer_setting()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Transient legacy-setting inconsistency when setting explicit before types

When callers update the CLI visualizer state with two sequential set_setting calls — first "/isaaclab/visualizer/explicit" then "/isaaclab/visualizer/types" — the legacy /isaaclab/visualizer key is briefly False between the two calls (because resolve_visualizer_types() sees explicit=True but types="" after the first call). Any code that reads the legacy key in between would get a stale value.

This is unlikely to matter in practice since both settings are typically written atomically before the simulation loop starts, but it could cause confusion in tests or tooling that writes them separately. Consider documenting this ordering assumption, or accepting explicit and types as a single atomic update.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant