Skip to content

Commit 4dd21ae

Browse files
committed
Add render flag to environment step() API
Expose a render: bool = True parameter on the step() method of all environment base classes (ManagerBasedEnv, ManagerBasedRLEnv, DirectRLEnv, DirectMARLEnv) and the MARL utility wrappers. When render=False is passed, all rendering calls are skipped: - GUI / RTX sensor renders inside the decimation loop - Post-reset re-renders for RTX sensors Physics simulation continues normally regardless of the flag. This allows user workflows that do not need rendering every step (e.g. headless RL training, fast rollouts) to opt out per-step. Also adds unit tests for render=False and mixed render flag stepping.
1 parent df2644c commit 4dd21ae

8 files changed

Lines changed: 199 additions & 15 deletions

File tree

source/isaaclab/config/extension.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22

33
# Note: Semantic Versioning is used: https://semver.org/
4-
version = "4.6.8"
4+
version = "4.6.9"
55

66
# Description
77
title = "Isaac Lab framework for Robot Learning"

source/isaaclab/docs/CHANGELOG.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
Changelog
22
---------
33

4+
4.6.9 (2026-04-21)
5+
~~~~~~~~~~~~~~~~~~
6+
7+
Added
8+
^^^^^
9+
10+
* Added ``render`` flag to the ``step()`` method of all environment base classes
11+
(:class:`~isaaclab.envs.ManagerBasedEnv`, :class:`~isaaclab.envs.ManagerBasedRLEnv`,
12+
:class:`~isaaclab.envs.DirectRLEnv`, :class:`~isaaclab.envs.DirectMARLEnv`).
13+
Passing ``render=False`` skips all rendering calls (GUI updates, RTX sensor rendering,
14+
post-reset re-renders) while physics simulation continues normally. Defaults to ``True``
15+
for full backward compatibility.
16+
17+
418
4.6.8 (2026-04-21)
519
~~~~~~~~~~~~~~~~~~
620

source/isaaclab/isaaclab/envs/direct_marl_env.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ def reset(
373373
# return observations
374374
return self.obs_dict, self.extras
375375

376-
def step(self, actions: dict[AgentID, ActionType]) -> EnvStepReturn:
376+
def step(self, actions: dict[AgentID, ActionType], render: bool = True) -> EnvStepReturn:
377377
"""Execute one time-step of the environment's dynamics.
378378
379379
The environment steps forward at a fixed time-step, while the physics simulation is decimated at a
@@ -394,6 +394,8 @@ def step(self, actions: dict[AgentID, ActionType]) -> EnvStepReturn:
394394
Args:
395395
actions: The actions to apply on the environment (keyed by the agent ID).
396396
Shape of individual tensors is (num_envs, action_dim).
397+
render: Whether to render during this step. When False, all rendering calls
398+
(GUI updates, RTX sensor rendering) are skipped. Defaults to True.
397399
398400
Returns:
399401
A tuple containing the observations, rewards, resets (terminated and truncated) and
@@ -411,7 +413,7 @@ def step(self, actions: dict[AgentID, ActionType]) -> EnvStepReturn:
411413

412414
# check if we need to do rendering within the physics loop
413415
# note: uses cached property to avoid settings lookup every step
414-
is_rendering = self.sim.is_rendering
416+
is_rendering = render and self.sim.is_rendering
415417

416418
# perform physics stepping
417419
for _ in range(self.cfg.decimation):

source/isaaclab/isaaclab/envs/direct_rl_env.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ def reset(self, seed: int | None = None, options: dict[str, Any] | None = None)
367367
# return observations
368368
return self._get_observations(), self.extras
369369

370-
def step(self, action: torch.Tensor) -> VecEnvStepReturn:
370+
def step(self, action: torch.Tensor, render: bool = True) -> VecEnvStepReturn:
371371
"""Execute one time-step of the environment's dynamics.
372372
373373
The environment steps forward at a fixed time-step, while the physics simulation is decimated at a
@@ -387,6 +387,8 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn:
387387
388388
Args:
389389
action: The actions to apply on the environment. Shape is (num_envs, action_dim).
390+
render: Whether to render during this step. When False, all rendering calls
391+
(GUI updates, RTX sensor rendering) are skipped. Defaults to True.
390392
391393
Returns:
392394
A tuple containing the observations, rewards, resets (terminated and truncated) and extras.
@@ -401,7 +403,7 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn:
401403

402404
# check if we need to do rendering within the physics loop
403405
# note: uses cached property to avoid settings lookup every step
404-
is_rendering = self.sim.is_rendering
406+
is_rendering = render and self.sim.is_rendering
405407

406408
# perform physics stepping
407409
for _ in range(self.cfg.decimation):
@@ -434,7 +436,7 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn:
434436
if len(reset_env_ids) > 0:
435437
self._reset_idx(reset_env_ids)
436438
# if sensors are added to the scene, make sure we render to reflect changes in reset
437-
if self.has_rtx_sensors and self.cfg.num_rerenders_on_reset > 0:
439+
if is_rendering and self.has_rtx_sensors and self.cfg.num_rerenders_on_reset > 0:
438440
for _ in range(self.cfg.num_rerenders_on_reset):
439441
self.sim.render()
440442

source/isaaclab/isaaclab/envs/manager_based_env.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,7 @@ def reset_to(
485485
# return observations
486486
return self.obs_buf, self.extras
487487

488-
def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]:
488+
def step(self, action: torch.Tensor, render: bool = True) -> tuple[VecEnvObs, dict]:
489489
"""Execute one time-step of the environment's dynamics.
490490
491491
The environment steps forward at a fixed time-step, while the physics simulation is
@@ -496,6 +496,8 @@ def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]:
496496
497497
Args:
498498
action: The actions to apply on the environment. Shape is (num_envs, action_dim).
499+
render: Whether to render during this step. When False, all rendering calls
500+
(GUI updates, RTX sensor rendering) are skipped. Defaults to True.
499501
500502
Returns:
501503
A tuple containing the observations and extras.
@@ -507,7 +509,7 @@ def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]:
507509

508510
# check if we need to do rendering within the physics loop
509511
# note: uses cached property to avoid settings lookup every step
510-
is_rendering = self.sim.is_rendering
512+
is_rendering = render and self.sim.is_rendering
511513

512514
# perform physics stepping
513515
for _ in range(self.cfg.decimation):

source/isaaclab/isaaclab/envs/manager_based_rl_env.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def setup_manager_visualizers(self):
158158
Operations - MDP
159159
"""
160160

161-
def step(self, action: torch.Tensor) -> VecEnvStepReturn:
161+
def step(self, action: torch.Tensor, render: bool = True) -> VecEnvStepReturn:
162162
"""Execute one time-step of the environment's dynamics and reset terminated environments.
163163
164164
Unlike the :class:`ManagerBasedEnv.step` class, the function performs the following operations:
@@ -173,6 +173,8 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn:
173173
174174
Args:
175175
action: The actions to apply on the environment. Shape is (num_envs, action_dim).
176+
render: Whether to render during this step. When False, all rendering calls
177+
(GUI updates, RTX sensor rendering) are skipped. Defaults to True.
176178
177179
Returns:
178180
A tuple containing the observations, rewards, resets (terminated and truncated) and extras.
@@ -184,7 +186,7 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn:
184186

185187
# check if we need to do rendering within the physics loop
186188
# note: uses cached property to avoid settings lookup every step
187-
is_rendering = self.sim.is_rendering
189+
is_rendering = render and self.sim.is_rendering
188190

189191
# perform physics stepping
190192
for _ in range(self.cfg.decimation):
@@ -229,7 +231,7 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn:
229231
self._reset_idx(reset_env_ids)
230232

231233
# if sensors are added to the scene, make sure we render to reflect changes in reset
232-
if self.has_rtx_sensors and self.cfg.num_rerenders_on_reset > 0:
234+
if is_rendering and self.has_rtx_sensors and self.cfg.num_rerenders_on_reset > 0:
233235
for _ in range(self.cfg.num_rerenders_on_reset):
234236
self.sim.render()
235237

source/isaaclab/isaaclab/envs/utils/marl.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def reset(self, seed: int | None = None, options: dict[str, Any] | None = None)
9797

9898
return obs, extras
9999

100-
def step(self, action: torch.Tensor) -> VecEnvStepReturn:
100+
def step(self, action: torch.Tensor, render: bool = True) -> VecEnvStepReturn:
101101
# split single-agent actions to build the multi-agent ones
102102
# FIXME: This implementation assumes the spaces are fundamental ones. Fix it to support composite spaces
103103
index = 0
@@ -108,7 +108,7 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn:
108108
index += delta
109109

110110
# step the environment
111-
obs, rewards, terminated, time_outs, extras = self.env.step(_actions)
111+
obs, rewards, terminated, time_outs, extras = self.env.step(_actions, render=render)
112112

113113
# use environment state as observation
114114
if self._state_as_observation:
@@ -233,7 +233,7 @@ def reset(
233233

234234
return obs, extras
235235

236-
def step(self, actions: dict[AgentID, ActionType]) -> EnvStepReturn:
236+
def step(self, actions: dict[AgentID, ActionType], render: bool = True) -> EnvStepReturn:
237237
# split agent actions to build the multi-agent ones
238238
# FIXME: This implementation assumes the spaces are fundamental ones. Fix it to support composite spaces
239239
index = 0
@@ -244,7 +244,7 @@ def step(self, actions: dict[AgentID, ActionType]) -> EnvStepReturn:
244244
index += delta
245245

246246
# step the environment
247-
obs, rewards, terminated, time_outs, extras = self.env.step(_actions)
247+
obs, rewards, terminated, time_outs, extras = self.env.step(_actions, render=render)
248248

249249
# use environment state as observation
250250
if self._state_as_observation:

source/isaaclab/test/envs/test_env_rendering_logic.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,165 @@ def wrapped_step(dt):
239239
else:
240240
# If env creation failed, still clear the singleton
241241
SimulationContext.clear_instance()
242+
243+
244+
@pytest.mark.parametrize("env_type", ["manager_based_env", "manager_based_rl_env", "direct_rl_env"])
245+
def test_env_render_false_skips_rendering(env_type, physics_callback, render_callback):
246+
"""Test that passing render=False to step() skips all rendering while physics continues."""
247+
physics_cb, get_physics_stats = physics_callback
248+
render_cb, get_render_stats = render_callback
249+
250+
env = None
251+
physics_handle = None
252+
original_step = None
253+
viz = None
254+
255+
try:
256+
# create a new stage
257+
sim_utils.create_new_stage()
258+
259+
# create environment with render_interval=1 so rendering would happen every physics step
260+
if env_type == "manager_based_env":
261+
env = create_manager_based_env(render_interval=1)
262+
elif env_type == "manager_based_rl_env":
263+
env = create_manager_based_rl_env(render_interval=1)
264+
else:
265+
env = create_direct_rl_env(render_interval=1)
266+
267+
# enable the flag to render the environment
268+
env.sim.set_setting("/isaaclab/render/rtx_sensors", True)
269+
270+
# disable the app from shutting down when the environment is closed
271+
env.sim._app_control_on_stop_handle = None # type: ignore
272+
273+
# Reset to initialize visualizers
274+
env.reset()
275+
276+
# Ensure the default Kit visualizer is active for rendering callbacks.
277+
assert isinstance(env.sim.visualizers[0], KitVisualizer)
278+
279+
# add physics callback
280+
physics_handle = env.sim.physics_manager.register_callback(
281+
physics_cb, IsaacEvents.POST_PHYSICS_STEP, name="physics_step"
282+
)
283+
284+
# Wrap visualizer step to track render calls
285+
viz = env.sim.visualizers[0]
286+
original_step = viz.step
287+
render_dt = env.cfg.sim.dt * env.cfg.sim.render_interval
288+
289+
def wrapped_step(dt):
290+
original_step(dt)
291+
render_cb(render_dt)
292+
293+
viz.step = wrapped_step
294+
295+
# create a zero action tensor for stepping the environment
296+
actions = torch.zeros((env.num_envs, 0), device=env.device)
297+
298+
# Step with render=False for several steps
299+
for i in range(10):
300+
env.step(action=actions, render=False)
301+
302+
# Physics should still advance normally
303+
_, num_physics_steps = get_physics_stats()
304+
assert num_physics_steps == (i + 1) * env.cfg.decimation, "Physics steps mismatch with render=False"
305+
306+
# No rendering should have occurred
307+
_, num_render_steps = get_render_stats()
308+
assert num_render_steps == 0, f"Expected 0 render steps with render=False, got {num_render_steps}"
309+
310+
finally:
311+
if viz is not None and original_step is not None:
312+
viz.step = original_step
313+
if physics_handle is not None:
314+
physics_handle.deregister()
315+
if env is not None:
316+
env.close()
317+
else:
318+
SimulationContext.clear_instance()
319+
320+
321+
@pytest.mark.parametrize("env_type", ["manager_based_env", "manager_based_rl_env", "direct_rl_env"])
322+
def test_env_render_flag_mixed_steps(env_type, physics_callback, render_callback):
323+
"""Test that render flag can be toggled between steps and rendering counts are correct."""
324+
physics_cb, get_physics_stats = physics_callback
325+
render_cb, get_render_stats = render_callback
326+
327+
env = None
328+
physics_handle = None
329+
original_step = None
330+
viz = None
331+
332+
try:
333+
# create a new stage
334+
sim_utils.create_new_stage()
335+
336+
# create environment with render_interval=1 so every decimation step renders
337+
if env_type == "manager_based_env":
338+
env = create_manager_based_env(render_interval=1)
339+
elif env_type == "manager_based_rl_env":
340+
env = create_manager_based_rl_env(render_interval=1)
341+
else:
342+
env = create_direct_rl_env(render_interval=1)
343+
344+
# enable the flag to render the environment
345+
env.sim.set_setting("/isaaclab/render/rtx_sensors", True)
346+
347+
# disable the app from shutting down when the environment is closed
348+
env.sim._app_control_on_stop_handle = None # type: ignore
349+
350+
# Reset to initialize visualizers
351+
env.reset()
352+
353+
# Ensure the default Kit visualizer is active for rendering callbacks.
354+
assert isinstance(env.sim.visualizers[0], KitVisualizer)
355+
356+
# add physics callback
357+
physics_handle = env.sim.physics_manager.register_callback(
358+
physics_cb, IsaacEvents.POST_PHYSICS_STEP, name="physics_step"
359+
)
360+
361+
# Wrap visualizer step to track render calls
362+
viz = env.sim.visualizers[0]
363+
original_step = viz.step
364+
render_dt = env.cfg.sim.dt * env.cfg.sim.render_interval
365+
366+
def wrapped_step(dt):
367+
original_step(dt)
368+
render_cb(render_dt)
369+
370+
viz.step = wrapped_step
371+
372+
# create a zero action tensor for stepping the environment
373+
actions = torch.zeros((env.num_envs, 0), device=env.device)
374+
375+
expected_render_steps = 0
376+
377+
# Step 5 times with render=True, then 5 with render=False
378+
for i in range(10):
379+
should_render = i < 5
380+
env.step(action=actions, render=should_render)
381+
382+
# Physics always advances
383+
_, num_physics_steps = get_physics_stats()
384+
assert num_physics_steps == (i + 1) * env.cfg.decimation, "Physics steps mismatch in mixed test"
385+
386+
# Rendering only happens in the first 5 steps
387+
if should_render:
388+
expected_render_steps += env.cfg.decimation # render_interval=1, so renders every decimation step
389+
390+
_, num_render_steps = get_render_stats()
391+
assert num_render_steps == expected_render_steps, (
392+
f"Render steps mismatch at step {i}: expected {expected_render_steps}, got {num_render_steps}"
393+
)
394+
395+
finally:
396+
if viz is not None and original_step is not None:
397+
viz.step = original_step
398+
if physics_handle is not None:
399+
physics_handle.deregister()
400+
if env is not None:
401+
env.close()
402+
else:
403+
SimulationContext.clear_instance()

0 commit comments

Comments
 (0)