Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion source/isaaclab/config/extension.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]

# Note: Semantic Versioning is used: https://semver.org/
version = "4.5.16"
version = "4.5.17"

# Description
title = "Isaac Lab framework for Robot Learning"
Expand Down
11 changes: 11 additions & 0 deletions source/isaaclab/docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Changelog
---------

4.5.17 (2026-03-18)
~~~~~~~~~~~~~~~~~~~

Fixed
^^^^^

* Fixed :meth:`~isaaclab.sim.SimulationContext.render` not calling ``app.update()`` when
running with Isaac Sim (Kit) and no active visualizer pumps the Kit app loop. This caused
``--video`` recording to produce black frames when not using ``--viz kit``.


4.5.16 (2026-03-10)
~~~~~~~~~~~~~~~~~~~

Expand Down
15 changes: 15 additions & 0 deletions source/isaaclab/isaaclab/sim/simulation_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,21 @@ def render(self, mode: int | None = None) -> None:
for callback in self._render_callbacks.values():
callback(None) # Pass None as event data

# When running with Isaac Sim (Kit) and no active visualizer already pumps the Kit
# app loop, call app.update() so the viewport and replicator render products
# (used e.g. by gym.wrappers.RecordVideo with render_mode="rgb_array") are refreshed.
# KitVisualizer.pumps_app_update() returns True and calls app.update() in its own
# step(), so we skip this call to avoid double-rendering in that case.
if has_kit() and not any(v.pumps_app_update() for v in self._visualizers):
try:
import omni.kit.app

app = omni.kit.app.get_app()
if app is not None and app.is_running():
app.update()
except (ImportError, AttributeError):
pass

def update_visualizers(self, dt: float) -> None:
"""Update visualizers without triggering renderer/GUI."""
if not self._visualizers:
Expand Down
61 changes: 61 additions & 0 deletions source/isaaclab/test/sim/test_simulation_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,67 @@ def test_render():
assert sim.is_playing()


@pytest.mark.isaacsim_ci
def test_render_pumps_app_update_without_visualizer():
"""Regression test for issue #5052: render() must call app.update() when no visualizer pumps the Kit loop.

Without this call, replicator render products (used by gym.wrappers.RecordVideo for
rgb_array rendering) are never updated, producing black video frames.
"""
from unittest.mock import MagicMock, patch

cfg = SimulationCfg(dt=0.01)
sim = SimulationContext(cfg)
sim.reset()

mock_app = MagicMock()
mock_app.is_running.return_value = True

with patch("omni.kit.app.get_app", return_value=mock_app):
sim.render()

# app.update() must be called when no visualizer pumps the Kit app loop
mock_app.update.assert_called_once()


@pytest.mark.isaacsim_ci
def test_render_skips_app_update_when_visualizer_pumps_it():
"""Regression test: render() must NOT call app.update() when a visualizer already does.

A visualizer that returns ``pumps_app_update() == True`` (e.g. KitVisualizer) calls
``app.update()`` in its own ``step()``, so ``SimulationContext.render()`` must not
call it again to avoid double-rendering.
"""
from unittest.mock import MagicMock, patch

from isaaclab.visualizers.base_visualizer import BaseVisualizer

cfg = SimulationCfg(dt=0.01)
sim = SimulationContext(cfg)
sim.reset()

# Inject a mock visualizer that pumps the app update
mock_viz = MagicMock(spec=BaseVisualizer)
mock_viz.pumps_app_update.return_value = True
mock_viz.is_closed = False
mock_viz.is_running.return_value = True
mock_viz.is_rendering_paused.return_value = False
mock_viz.is_training_paused.return_value = False
mock_viz.get_rendering_dt.return_value = None
sim._visualizers = [mock_viz]

mock_app = MagicMock()
mock_app.is_running.return_value = True

with patch("omni.kit.app.get_app", return_value=mock_app):
sim.render()

# app.update() must NOT be called since the visualizer already pumps it
mock_app.update.assert_not_called()

sim._visualizers = []


"""
Stage Operations Tests
"""
Expand Down