Skip to content

Refactor viz and recording cfg classes: passive recorder, render callbacks, ViewerCfg removal#6598

Open
matthewtrepte wants to merge 9 commits into
isaac-sim:developfrom
matthewtrepte:mtrepte/refactor-viz-and-recording-cfg-classes
Open

Refactor viz and recording cfg classes: passive recorder, render callbacks, ViewerCfg removal#6598
matthewtrepte wants to merge 9 commits into
isaac-sim:developfrom
matthewtrepte:mtrepte/refactor-viz-and-recording-cfg-classes

Conversation

@matthewtrepte

Copy link
Copy Markdown
Contributor

Summary

This PR replaces the ViewerCfg/ViewportCameraController/recording_hooks stack with a cleaner, backend-agnostic architecture:

  • Removes ViewerCfg from all env cfg bases (DirectRLEnvCfg, ManagerBasedEnvCfg, DirectMARLEnvCfg) and migrates all task envs to sim.default_visualizer_cfg = VisualizerCfg(eye=..., lookat=...) or sim.visualizer_cfgs = [KitVisualizerCfg(...)]
  • Removes ViewportCameraController (243 lines) — camera tracking is now handled directly by KitVisualizer via its origin_type / asset_name / body_name cfg fields
  • Removes recording_hooks module from both isaaclab and isaaclab_newton — physics-backend recording callbacks are now registered via the new SimulationContext.add_render_callback(name, fn, order) public API
  • Passive VideoRecorder: VideoRecorderCfg.eye/lookat removed; the recorder captures whatever the active visualizer renders without repositioning the camera
  • PhysicsManager.video_capture_backend(): new classmethod replaces name-sniffing heuristic for backend selection
  • VideoRecorderCfg exported from isaaclab.envs public namespace
  • Kit+Newton recording fix: when Newton is the physics backend, _select_video_backend skips Kit Replicator (Newton Fabric writes don't notify RTX's scene delegate) and falls back to standalone Newton GL capture with a logged warning

Net diff: 69 files changed, +750 / −745 (≈ net zero lines despite the significant cleanup, because new KitVisualizer camera tracking code replaces the deleted ViewportCameraController).

Test plan

  • source/isaaclab/test/envs/test_video_recorder.py — 17/17 unit tests pass (covers all _select_video_backend dispatch paths including new Newton fallback)
  • source/isaaclab/test/sim/test_simulation_context.py — 29 passed before pre-existing hang on test_timeline_callbacks_with_weakref (unrelated)
  • source/isaaclab/test/envs/test_env_rendering_logic.py — 17/17 pass
  • source/isaaclab/test/sim/test_simulation_context_visualizers.py — 28/28 pass
  • source/isaaclab_tasks/test/core/test_recording_backends.py5/5 integration tests pass (Kit/PhysX, Kit/Newton fallback, Newton GL, PhysX renderer, Newton renderer)

🤖 Generated with Claude Code

…er accesses

Fix critical crash in ManagerBasedRLEnv.__init__ where cfg.viewer.eye/lookat
was accessed after the viewer field was removed from the env cfg base classes.
The recorder is now passive and no longer needs camera position forwarded.

Migrate all envs that silently lost their ViewerCfg without replacement:
g1 locomanipulation SDG, dexsuite reorient, g129 trocar assembly, Spot flat,
cartpole direct, cartpole direct-camera, and cartpole-showcase camera envs
all now set sim.default_visualizer_cfg = VisualizerCfg(eye=..., lookat=...)
in __post_init__, following the pattern of the other migrated tasks.

Fix visualizer_integration_utils.py stale env_cfg.viewer.eye/lookat access
that would crash all cartpole visualizer integration tests; migrated to
env_cfg.sim.default_visualizer_cfg = VisualizerCfg(...) instead.
Export VideoRecorderCfg from isaaclab.envs so the public namespace
reflects the new recorder API; the class lived in utils/ and was not
reachable via lazy_export, breaking the install CI smoke test.

Add five unit tests for add_render_callback / remove_render_callback
in test_simulation_context.py — the new public API introduced by the
re-arch had zero test coverage.

Update stale docstrings in the two headless-video-pump regression tests
that still referenced the deleted recording_hooks module as the
implementation mechanism; the same contract is now wired through
add_render_callback by the physics-backend managers.
… paths

Four integration tests covering every VideoRecorder capture path:
kit-visualizer-source, newton-visualizer-source, physx-renderer-source,
newton-renderer-source. Each creates a CartpoleEnv with render_mode="rgb_array",
steps 10 physics ticks, calls env.render(), and asserts the frame is a
non-trivial (_H, _W, 3) uint8 ndarray with at least 1% non-black pixels.

Previously test_record_video.py only exercised the old gymnasium RecordVideo
wrapper without touching VideoRecorderCfg, and test_video_recorder.py only
used mocks. These tests exercise the real capture stack end-to-end.
The previous Newton visualizer test used NewtonVisualizerCfg (abstract
base, visualizer_type=None) instead of NewtonGLVisualizerCfg
(visualizer_type="newton"). This made it accidentally test the physics
manager fallback rather than the Newton GL visualizer direct path.

Rewrite to cover all three _select_video_backend dispatch paths:
- Path 1 (type="kit"): Kit visualizer with PhysX and with Newton physics
  (cross-backend — Kit capture is renderer-agnostic)
- Path 2 (type="newton"): NewtonGLVisualizerCfg direct framebuffer capture
- Path 3 (fallthrough): NewtonRTXVisualizerCfg (type="newton_rtx") is not
  recognised so falls through to the physics manager; also standalone
  renderer-source tests for PhysX and Newton backends
Three issues found by running the tests:

- Kit Replicator render products are zero-initialized and need RTX warmup
  frames before the buffer populates. Add a poll-until-non-black loop
  (_WARMUP_RENDER_BUDGET=40 sim.render() calls) matching the same pattern
  used by OVRTX tests in rendering_test_utils.py.

- Cartpole against a black background is sparse; lower _MIN_NONZERO_RATIO
  from 0.01 to 0.005 so a valid but thin scene does not false-fail.

- Kit Replicator recording produces a zero buffer when Newton is the active
  physics backend. The PhysX manager registers an ensure_isaac_rtx_render_update
  render callback that primes the render product; Newton does not register an
  equivalent, so the buffer stays zeroed even with Kit visualizer pumping
  app.update(). Mark test_kit_visualizer_newton_source_records_rgb as
  strict xfail to document this known gap.

- NewtonRTXVisualizerCfg is not a factory-registered visualizer type
  ("newton_rtx" raises ValueError). Remove the broken fallthrough test for
  it; path-3 fallthrough logic is already unit-tested in test_video_recorder.py.

Final result: 4 passed, 1 xfailed in 35s.
Newton's Fabric transform writes (via wp.fabricarray Warp kernel) do not
trigger RTX's scene delegate change notifications, so Replicator annotator
buffers stay zeroed regardless of how many app.update() calls are issued.

Fix _select_video_backend to detect Newton physics (video_capture_backend()
== "newton_gl") and skip Kit Replicator in that case. If a Newton GL
visualizer is configured, use its framebuffer directly. If only a Kit
visualizer is configured alongside Newton, log a warning and fall back to
standalone Newton GL capture, which renders Newton's scene correctly.

Kit recording continues to work unchanged with PhysX.

Add two unit tests for the new fallback dispatch and remove the xfail from
test_kit_visualizer_newton_source_records_rgb — all 5 integration tests now
pass (previously 4 passed, 1 xfailed).
@greptile-apps

greptile-apps Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR performs a large architectural cleanup of the visualization and recording stack, replacing ViewerCfg/ViewportCameraController/recording_hooks with a new backend-agnostic design. Camera tracking is moved into KitVisualizer, recording callbacks are registered via a new SimulationContext.add_render_callback() API, and VideoRecorder becomes fully passive (no longer repositions the camera).

  • Removes ViewerCfg from all env cfg bases and migrates task envs to sim.visualizer_cfgs.
  • Removes ViewportCameraController (243 lines); replaces it with origin_type/asset_name/body_name fields in KitVisualizerCfg and tracking logic inside KitVisualizer.step().
  • Replaces recording_hooks module with SimulationContext.add_render_callback().
  • Newton+Kit recording fix: skips Kit Replicator when Newton physics is active and falls back to Newton GL standalone with a logged warning.

Confidence Score: 3/5

The core recording pipeline and render-callback refactor are sound, but the UI camera control panel has functional regressions that affect interactive use.

The recording refactor is well-tested and architecturally clean. The base_env_window.py camera UI has two regressions: the origin-type dropdown no longer repositions the viewport camera immediately for world/env modes, and eye/lookat slider changes during asset-tracking mode are silently undone on the next render step because viz.cfg.eye/lookat are not written back.

source/isaaclab/isaaclab/envs/ui/base_env_window.py — both the origin-type callback and the location callback need fixes before the UI camera controls behave correctly.

Important Files Changed

Filename Overview
source/isaaclab/isaaclab/envs/ui/base_env_window.py UI panel migrated from ViewportCameraController to KitVisualizer cfg mutation, but UI callbacks no longer trigger immediate camera repositioning for world/env modes, and cfg.eye/lookat are not updated so asset-tracking mode snaps back to old offsets.
source/isaaclab_visualizers/isaaclab_visualizers/kit/kit_visualizer.py Camera tracking logic correctly ported into KitVisualizer with per-step asset tracking; initial setup and body index lookup look sound.
source/isaaclab/isaaclab/sim/simulation_context.py New add/remove_render_callback API cleanly replaces the two-system pattern; callbacks are sorted by order and invoked correctly.
source/isaaclab_physx/isaaclab_physx/physics/physx_manager.py Headless video pump self-registers in initialize() via add_render_callback; each new SimulationContext starts with a fresh dict so this is not a leak.
source/isaaclab/isaaclab/envs/utils/video_recorder.py _select_video_backend correctly dispatches PhysX vs Newton paths; Newton+Kit warning and fallback logic is clear.
source/isaaclab/isaaclab/envs/init.pyi VideoRecorderCfg is exported from init.py but omitted from the type stub.
source/isaaclab/isaaclab/envs/common.py ViewerCfg removed cleanly; remaining types are untouched.
source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py Adds video_capture_backend() classmethod returning newton_gl; straightforward addition.
source/isaaclab_visualizers/isaaclab_visualizers/kit/kit_visualizer_cfg.py New origin_type/env_index/asset_name/body_name fields mirror the old ViewerCfg fields correctly.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Env as DirectRLEnv
    participant SimCtx as SimulationContext
    participant PhysxMgr as PhysxManager
    participant KitViz as KitVisualizer
    participant VidRec as VideoRecorder
    Note over Env,VidRec: Initialization
    Env->>SimCtx: __init__() sets _render_callbacks
    SimCtx->>PhysxMgr: initialize(sim)
    PhysxMgr->>SimCtx: add_render_callback(physx_headless_video_pump)
    Env->>KitViz: initialize()
    KitViz->>KitViz: _setup_initial_camera_view()
    Env->>VidRec: VideoRecorder(cfg, scene)
    Note over Env,VidRec: Per-step render loop
    Env->>SimCtx: render()
    SimCtx->>KitViz: step(dt)
    KitViz->>KitViz: _update_asset_tracking_camera()
    loop sorted _render_callbacks
        SimCtx->>PhysxMgr: pump_kit_app_for_headless_video_render_if_needed
    end
    Env->>VidRec: render_rgb_array()
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Env as DirectRLEnv
    participant SimCtx as SimulationContext
    participant PhysxMgr as PhysxManager
    participant KitViz as KitVisualizer
    participant VidRec as VideoRecorder
    Note over Env,VidRec: Initialization
    Env->>SimCtx: __init__() sets _render_callbacks
    SimCtx->>PhysxMgr: initialize(sim)
    PhysxMgr->>SimCtx: add_render_callback(physx_headless_video_pump)
    Env->>KitViz: initialize()
    KitViz->>KitViz: _setup_initial_camera_view()
    Env->>VidRec: VideoRecorder(cfg, scene)
    Note over Env,VidRec: Per-step render loop
    Env->>SimCtx: render()
    SimCtx->>KitViz: step(dt)
    KitViz->>KitViz: _update_asset_tracking_camera()
    loop sorted _render_callbacks
        SimCtx->>PhysxMgr: pump_kit_app_for_headless_video_render_if_needed
    end
    Env->>VidRec: render_rgb_array()
Loading

Reviews (1): Last reviewed commit: "Fix Kit+Newton recording gap: skip Kit R..." | Re-trigger Greptile

Comment on lines 393 to +407
def _set_viewer_origin_type_fn(self, value: str):
"""Sets the origin of the viewport's camera. This is based on the drop-down menu in the UI."""
# Extract the viewport camera controller from environment
vcc = self.env.viewport_camera_controller
if vcc is None:
raise ValueError("Viewport camera controller is not initialized! Please check the rendering mode.")
viz = self._get_kit_visualizer()
if viz is None:
return

# Based on origin type, update the camera view
if value == "World":
vcc.update_view_to_world()
viz.cfg.origin_type = "world"
elif value == "Env":
vcc.update_view_to_env()
viz.cfg.origin_type = "env"
else:
# find which index the asset is
fancy_names = [name.replace("_", " ").title() for name in self._viewer_assets_options]
# store the desired env index
viewer_asset_name = self._viewer_assets_options[fancy_names.index(value)]
# update the camera view
vcc.update_view_to_asset_root(viewer_asset_name)
viz.cfg.origin_type = "asset_root"
viz.cfg.asset_name = viewer_asset_name

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 UI origin-type change no longer repositions the camera

Changing to "World" or "Env" now only mutates viz.cfg.origin_type but never calls _setup_initial_camera_view() or _apply_viewer_origin_to_camera(), so the viewport camera stays frozen at its previous position. The old ViewportCameraController.update_view_to_world() / update_view_to_env() immediately computed the new viewer_origin and called update_view_location(). The same is true for the env-index spinner in _set_viewer_env_index_fn (line 418): setting viz.cfg.env_index has no effect until the next asset-tracking step because KitVisualizer.step() only updates the camera for "asset_root" / "asset_body" origins.

Each branch should additionally call the camera-setup helpers (or a new public method that re-runs the origin resolution and applies it) so that the dropdown and spinner produce the same immediate repositioning users expected before.

Comment on lines 409 to +416
def _set_viewer_location_fn(self, model: omni.ui.SimpleFloatModel):
"""Sets the viewport camera location based on the UI."""
# access the viewport camera controller (for brevity)
vcc = self.env.viewport_camera_controller
if vcc is None:
raise ValueError("Viewport camera controller is not initialized! Please check the rendering mode.")
# obtain the camera locations and set them in the viewpoint camera controller
viz = self._get_kit_visualizer()
if viz is None:
return
eye = [self.ui_window_elements["viewer_eye"][i].get_value_as_float() for i in range(3)]
lookat = [self.ui_window_elements["viewer_lookat"][i].get_value_as_float() for i in range(3)]
# update the camera view
vcc.update_view_location(eye, lookat)
viz.set_camera_view(eye, lookat)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Camera eye/lookat offset not persisted — snaps back during asset tracking

viz.set_camera_view(eye, lookat) moves the viewport camera to the given absolute position, but viz.cfg.eye and viz.cfg.lookat (the relative offsets used by _apply_viewer_origin_to_camera) are never updated. On the very next call to KitVisualizer.step() in "asset_root" or "asset_body" mode, _update_asset_tracking_camera() recomputes the position from the old cfg.eye/cfg.lookat and overwrites whatever the user just set.

The old ViewportCameraController.update_view_location() avoided this by storing the new values in self.default_cam_eye / self.default_cam_lookat before applying them. The fix is to also write back: viz.cfg.eye = tuple(eye); viz.cfg.lookat = tuple(lookat) before calling set_camera_view.

Comment on lines 30 to +31
from . import mdp, ui
from .common import VecEnvObs, VecEnvStepReturn, ViewerCfg
from .common import VecEnvObs, VecEnvStepReturn

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 VideoRecorderCfg is now exported from __init__.py (the test file test_recording_backends.py imports it from isaaclab.envs), but it is absent from the .pyi type stub. Type-checkers and IDE completions will not see it.

Suggested change
from . import mdp, ui
from .common import VecEnvObs, VecEnvStepReturn, ViewerCfg
from .common import VecEnvObs, VecEnvStepReturn
from . import mdp, ui
from .common import VecEnvObs, VecEnvStepReturn
from .utils.video_recorder_cfg import VideoRecorderCfg

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

…ure classes

Remove IsaacsimKitPerspectiveVideo/Cfg and NewtonGlPerspectiveVideo/Cfg.
Recording is now fully internal to env.step() — no gym RecordVideo wrapper
needed. VideoRecorderCfg is backend-agnostic and specifies only what to
record and where to write it, not how to capture.

VideoRecorderCfg fields:
  source: str = "visualizer"   source string (visualizer[:<type>[/tiled]],
                                sensor:<name>[/<data_type>])
  output_dir: str              clip output directory
  fps: int                     output video frame rate
  clip_length: int             env steps per clip
  clip_trigger_step: int       start a new clip every N steps (0 = once)

env cfgs gain video_recorders: list[VideoRecorderCfg] = [] (empty = off)
replacing the single video_recorder: VideoRecorderCfg field. Multiple
entries produce independent simultaneous streams from different sources.

KitVisualizer gains render_rgb_array() via a lazily-created Replicator
annotator on its controlled camera prim. Newton GL visualizer already had
render_rgb_array(). Sensor sources read from env.scene.sensors[name].

VideoRecorder.step() is called in DirectRLEnv, ManagerBasedRLEnv, and
DirectMARLEnv step loops after physics/render and before observations.
@autoweave-bot

Copy link
Copy Markdown

This looks like it'll make simulation control so much smoother! 🗺️✨😌
Explore here →

Subweave map of isaac-sim/isaaclab#6598

Maintainer? Turn off weaves from non-maintainers →
Carefully crafted by Subweave · 🧶 used ~4.1m LLM tokens

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

Labels

documentation Improvements or additions to documentation isaac-mimic Related to Isaac Mimic team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants