Fix --video producing black frames without --viz kit (issue #5052)#5056
Fix --video producing black frames without --viz kit (issue #5052)#5056pascal-roth wants to merge 3 commits into
Conversation
SimulationContext.render() was not calling omni.kit.app.get_app().update() when running with Isaac Sim (Kit) and no active visualizer pumped the Kit app loop. This meant replicator render products (used by gym.wrappers.RecordVideo via render_mode="rgb_array") were never refreshed, producing black video frames. The fix calls app.update() in render() when has_kit() is True and no visualizer with pumps_app_update() == True is active. KitVisualizer already calls app.update() in its own step(), so we skip the call to avoid double-rendering when --viz kit is used. Adds two regression tests to verify: 1. render() calls app.update() when no visualizer pumps the Kit loop 2. render() does not call app.update() when a visualizer already does Fixes #5052
|
Hi @pascal-roth . Thanks for this! I tested it out. It does fix the blank videos, but it seems the camera position is not updated properly according to the |
|
Thanks for testing and reporting this, @nblauch! The camera perspective mismatch you're seeing (headless Good news: that exact problem is already being fixed in PR #5011 (Add perspective video recording via Newton GL viewer for Kitless backends; Fix for Kitfull backends camera position). That PR explicitly reads To avoid duplicating the fix here and keeping both diffs focused, I'd suggest we track resolution in #5011. Once that lands, the Let me know if the scope there differs from what you experienced! |
|
@matthewtrepte please also review this along with the other related issues around headless visualization |
There was a problem hiding this comment.
🤖 Isaac Lab Review Bot
Summary: The fix correctly identifies the root cause — replicator render products need app.update() to refresh their buffers. However, the current implementation has a critical bug: it calls app.update() without first disabling /app/player/playSimulations, which risks double-stepping physics when the timeline is active. KitVisualizer.step() carefully brackets its app.update() call with playSimulations=False/True — this new code path must do the same. Tests and changelog are solid otherwise.
Architecture: Low-risk surface area (1 method touched), but the app.update() call is in the hot path for every render() invocation and has correctness + performance implications that need addressing.
| app = omni.kit.app.get_app() | ||
| if app is not None and app.is_running(): | ||
| app.update() | ||
| except (ImportError, AttributeError): |
There was a problem hiding this comment.
🔴 Critical: Missing playSimulations=False guard — potential double physics step
Calling app.update() while the timeline is playing and /app/player/playSimulations is True (the default) will cause Kit to pump its internal physics loop, resulting in a double physics step per frame. This is exactly what KitVisualizer.step() guards against:
# KitVisualizer.step() does this correctly:
settings.set_bool('/app/player/playSimulations', False)
app.update()
settings.set_bool('/app/player/playSimulations', True)The fix here should mirror that pattern:
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():
self.set_setting('/app/player/playSimulations', False)
app.update()
self.set_setting('/app/player/playSimulations', True)
except (ImportError, AttributeError):
passWithout this, every env.step() → sim.step() → render() cycle runs physics twice, causing 2× simulation speed, energy non-conservation, and incorrect RL rewards.
There was a problem hiding this comment.
yes we should have the playSimulations guard. could this also be moved to the kit visualizer script so that we don't have kit-specific logic in the simulation context?
|
|
||
| # 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. |
There was a problem hiding this comment.
app.update() on every render
app.update() is expensive — it pumps the entire Kit event loop (USD hydra, viewport compositing, extension ticks, etc.). This now runs on every render() call, even when:
- No video recording is active (
RecordVideowrapper not used) - No replicator render products exist
- The user is just doing headless RL training with
--videoflag absent
Consider gating this more tightly. At minimum, checking self._has_offscreen_render or self.get_setting('/isaaclab/render/rtx_sensors') would limit the call to cases where render products actually exist. The has_kit() check alone is too broad — Kit is always present in Isaac Sim mode, even for pure headless training.
Alternatively, a dedicated "needs_app_pump" flag set when RecordVideo or camera sensors are initialized would be more precise and avoid per-frame setting lookups.
| # (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): |
There was a problem hiding this comment.
🟡 Nit: redundant import + get_app() when has_kit() already confirmed Kit is running
has_kit() (in version.py) already calls sys.modules.get('omni.kit.app') → mod.get_app() and confirms the result is not None. Re-importing and re-calling get_app() here is redundant. You could simplify:
import sys
mod = sys.modules['omni.kit.app'] # guaranteed present by has_kit()
app = mod.get_app() # guaranteed non-None by has_kit()
# app.is_running() is still worth checking
if app.is_running():
app.update()Not blocking, but it would make the code cleaner and avoid the misleading except ImportError.
|
|
||
| mock_app = MagicMock() | ||
| mock_app.is_running.return_value = True | ||
|
|
There was a problem hiding this comment.
🟡 Test gap: mock patches omni.kit.app.get_app but has_kit() is not mocked
The has_kit() function checks sys.modules.get('omni.kit.app') and calls the real get_app() — it doesn't use the patched version from unittest.mock.patch('omni.kit.app.get_app', ...). In CI where Kit IS running, has_kit() returns True from the real module, so this test passes. But in a kitless test environment, has_kit() would return False and the patched get_app would never be reached, making the assertion vacuously pass.
Consider also patching has_kit to make the test robust in all environments:
with patch('isaaclab.sim.simulation_context.has_kit', return_value=True), \
patch('omni.kit.app.get_app', return_value=mock_app):
sim.render()Since these tests are gated by @pytest.mark.isaacsim_ci, this is not a blocker — but it's worth noting for maintainability.
| 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] |
There was a problem hiding this comment.
🟡 Test improvement: verify update_visualizers interaction with mock visualizer
This test injects a mock visualizer into _visualizers but doesn't fully account for what update_visualizers() does with it. update_visualizers() calls viz.is_closed, viz.is_running(), viz.is_rendering_paused(), viz.is_training_paused(), and viz.step(dt) — all of which are mocked. However, update_scene_data_provider() is also called, which accesses _scene_data_provider. If a future refactor changes update_visualizers' behavior, this test could break in non-obvious ways.
Consider also asserting that mock_viz.step.assert_called() to verify the visualizer's step() method (which is where KitVisualizer does its own app.update()) was actually invoked, confirming the intended code path.
| mock_app.update.assert_not_called() | ||
|
|
||
| sim._visualizers = [] | ||
|
|
There was a problem hiding this comment.
🟢 Good: Cleaning up sim._visualizers = [] after the test to avoid leaking mock state into the teardown's clear_instance(). Defensive and correct.
| Changelog | ||
| --------- | ||
|
|
||
| 4.5.17 (2026-03-18) |
There was a problem hiding this comment.
🟢 Changelog format looks correct — follows the existing pattern (version, date, section header, bullet). Minor note: the date 2026-03-18 is in the past relative to the PR creation — just verify this is intentional per your release process.
|
@michaellin6 confirmed that this is still an issue on latest develop @AntoineRichard for viz |
# Description <!-- Thank you for your interest in sending a pull request. Please make sure to check the contribution guidelines. Link: https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html 💡 Please try to keep PRs small and focused. Large PRs are harder to review and merge. --> Tests - Add visualizer tests which load cartpole scene and check the viewport isn't black or frozen - Add regression tests for visualization pumping from Pascal's change - #5056 Newton Visualizer - Fix "Pause/Resume Rendering" button - Change "Pause/Resume Training" to "Pause/Resume Simulation" Kit Visualizer - Fix recording in headless mode - Resolve overlapping camera cfg behavior in ViewerCfg and KitVisualizerCfg (will likely need another PR + design to streamline the cfgs) Rerun Visualizer - Fix issue when launching rerun + newton physics without KitVisualizer RTX Renderer - Fix stale image issue after resets which require Kit Visualizer's continuous app updates to avoid - Fix strange wrist camera orientation offset issue Else - Rename camera_position/camera_target_position fields to eye/lookat across the board <!-- As a practice, it is recommended to open an issue to have discussions on the proposed pull request. This makes it easier for the community to keep track of what is being developed or added, and if a given feature is demanded by more than one party. --> ## Type of change <!-- As you go through the list, delete the ones that are not applicable. --> - Bug fix (non-breaking change which fixes an issue) - New feature (non-breaking change which adds functionality) - Breaking change (existing functionality will not work without user modification) - Documentation update ## Screenshots Please attach before and after screenshots of the change if applicable. <!-- Example: | Before | After | | ------ | ----- | | _gif/png before_ | _gif/png after_ | To upload images to a PR -- simply drag and drop an image while in edit mode and it should upload the image directly. You can then paste that source into the above before/after sections. --> ## Checklist - [ ] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [ ] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there <!-- As you go through the checklist above, you can mark something as done by putting an x character in it For example, - [x] I have done this task - [ ] I have not done this task -->
# Description <!-- Thank you for your interest in sending a pull request. Please make sure to check the contribution guidelines. Link: https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html 💡 Please try to keep PRs small and focused. Large PRs are harder to review and merge. --> Tests - Add visualizer tests which load cartpole scene and check the viewport isn't black or frozen - Add regression tests for visualization pumping from Pascal's change - isaac-sim#5056 Newton Visualizer - Fix "Pause/Resume Rendering" button - Change "Pause/Resume Training" to "Pause/Resume Simulation" Kit Visualizer - Fix recording in headless mode - Resolve overlapping camera cfg behavior in ViewerCfg and KitVisualizerCfg (will likely need another PR + design to streamline the cfgs) Rerun Visualizer - Fix issue when launching rerun + newton physics without KitVisualizer RTX Renderer - Fix stale image issue after resets which require Kit Visualizer's continuous app updates to avoid - Fix strange wrist camera orientation offset issue Else - Rename camera_position/camera_target_position fields to eye/lookat across the board <!-- As a practice, it is recommended to open an issue to have discussions on the proposed pull request. This makes it easier for the community to keep track of what is being developed or added, and if a given feature is demanded by more than one party. --> ## Type of change <!-- As you go through the list, delete the ones that are not applicable. --> - Bug fix (non-breaking change which fixes an issue) - New feature (non-breaking change which adds functionality) - Breaking change (existing functionality will not work without user modification) - Documentation update ## Screenshots Please attach before and after screenshots of the change if applicable. <!-- Example: | Before | After | | ------ | ----- | | _gif/png before_ | _gif/png after_ | To upload images to a PR -- simply drag and drop an image while in edit mode and it should upload the image directly. You can then paste that source into the above before/after sections. --> ## Checklist - [ ] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [ ] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there <!-- As you go through the checklist above, you can mark something as done by putting an x character in it For example, - [x] I have done this task - [ ] I have not done this task -->
Summary
SimulationContext.render()was not callingomni.kit.app.get_app().update()when no active visualizer pumped the Kit app loop. Replicator render products (used bygym.wrappers.RecordVideoviarender_mode="rgb_array") were never refreshed, producing black video frames.app.update()inrender()whenhas_kit()isTrueand no visualizer withpumps_app_update() == Trueis active.KitVisualizeralready callsapp.update()in its ownstep(), so we skip the call to avoid double-rendering when--viz kitis used.test_simulation_context.py— one verifyingapp.update()IS called without a visualizer, one verifying it is NOT called when a visualizer already pumps the Kit loop.Test plan
test_render_pumps_app_update_without_visualizer— fails without fix, passes with fixtest_render_skips_app_update_when_visualizer_pumps_it— passes with fix (no double-rendering)./isaaclab.sh -f)./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Velocity-Rough-Anymal-C-v0 --videoshould produce correct (non-black) videoCloses #5052
🤖 Generated with Claude Code