Skip to content

Commit 7eba41b

Browse files
author
Kelly Guo
committed
Add render_enabled property to environment step API
Add a mutable render_enabled property to all environment base classes (ManagerBasedEnv, ManagerBasedRLEnv, DirectRLEnv, DirectMARLEnv) that controls whether rendering occurs during step(). Setting env.render_enabled = False before calling step() skips all rendering calls (GUI updates, RTX sensor rendering, post-reset re-renders) while physics simulation continues normally. The property can be toggled between steps for per-step control. This approach preserves the standard gymnasium.Env.step() signature (no extra kwargs), avoiding the need for downstream RL libraries to add Isaac Lab-specific handling.
1 parent 09fe938 commit 7eba41b

6 files changed

Lines changed: 192 additions & 6 deletions

File tree

source/isaaclab/docs/CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ Added
99

1010
* Added :meth:`~isaaclab.utils.wrench_composer.WrenchComposer.add_raw_buffers_from` to merge one composer's raw
1111
input buffers into another.
12+
* Added ``render_enabled`` property to all environment base classes
13+
(:class:`~isaaclab.envs.ManagerBasedEnv`, :class:`~isaaclab.envs.ManagerBasedRLEnv`,
14+
:class:`~isaaclab.envs.DirectRLEnv`, :class:`~isaaclab.envs.DirectMARLEnv`).
15+
Setting ``env.render_enabled = False`` before calling ``step()`` skips all rendering calls
16+
(GUI updates, RTX sensor rendering, post-reset re-renders) while physics simulation continues
17+
normally. The property can be toggled between steps for per-step control. Defaults to ``True``
18+
for full backward compatibility.
1219

1320
Changed
1421
^^^^^^^

source/isaaclab/isaaclab/envs/direct_marl_env.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ def _init_sim(self, render_mode: str | None = None, **kwargs):
217217
# initialize data and constants
218218
# -- counter for simulation steps
219219
self._sim_step_counter = 0
220+
# -- whether step() performs rendering (set to False to skip all render calls)
221+
self.render_enabled: bool = True
220222
# -- counter for curriculum
221223
self.common_step_counter = 0
222224
# -- init buffers
@@ -391,6 +393,8 @@ def step(self, actions: dict[AgentID, ActionType]) -> EnvStepReturn:
391393
5. Apply interval events if they are enabled.
392394
6. Compute observations.
393395
396+
Rendering can be controlled per-step via :attr:`render_enabled`.
397+
394398
Args:
395399
actions: The actions to apply on the environment (keyed by the agent ID).
396400
Shape of individual tensors is (num_envs, action_dim).
@@ -411,7 +415,7 @@ def step(self, actions: dict[AgentID, ActionType]) -> EnvStepReturn:
411415

412416
# check if we need to do rendering within the physics loop
413417
# note: uses cached property to avoid settings lookup every step
414-
is_rendering = self.sim.is_rendering
418+
is_rendering = self.render_enabled and self.sim.is_rendering
415419

416420
# perform physics stepping
417421
for _ in range(self.cfg.decimation):

source/isaaclab/isaaclab/envs/direct_rl_env.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ def _init_sim(self, render_mode: str | None = None, **kwargs):
223223
# initialize data and constants
224224
# -- counter for simulation steps
225225
self._sim_step_counter = 0
226+
# -- whether step() performs rendering (set to False to skip all render calls)
227+
self.render_enabled: bool = True
226228
# -- counter for curriculum
227229
self.common_step_counter = 0
228230
# -- init buffers
@@ -385,6 +387,8 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn:
385387
5. Apply interval events if they are enabled.
386388
6. Compute observations.
387389
390+
Rendering can be controlled per-step via :attr:`render_enabled`.
391+
388392
Args:
389393
action: The actions to apply on the environment. Shape is (num_envs, action_dim).
390394
@@ -401,7 +405,7 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn:
401405

402406
# check if we need to do rendering within the physics loop
403407
# note: uses cached property to avoid settings lookup every step
404-
is_rendering = self.sim.is_rendering
408+
is_rendering = self.render_enabled and self.sim.is_rendering
405409

406410
# perform physics stepping
407411
for _ in range(self.cfg.decimation):
@@ -434,7 +438,7 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn:
434438
if len(reset_env_ids) > 0:
435439
self._reset_idx(reset_env_ids)
436440
# 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:
441+
if is_rendering and self.has_rtx_sensors and self.cfg.num_rerenders_on_reset > 0:
438442
for _ in range(self.cfg.num_rerenders_on_reset):
439443
self.sim.render()
440444

source/isaaclab/isaaclab/envs/manager_based_env.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ def _init_sim(self):
153153
# counter for simulation steps
154154
self._sim_step_counter = 0
155155

156+
# -- whether step() performs rendering (set to False to skip all render calls)
157+
self.render_enabled: bool = True
158+
156159
# allocate dictionary to store metrics
157160
self.extras = {}
158161

@@ -494,6 +497,8 @@ def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]:
494497
simulation steps per environment step) and the :attr:`ManagerBasedEnvCfg.sim.dt` (physics time-step).
495498
Based on these parameters, the environment time-step is computed as the product of the two.
496499
500+
Rendering can be controlled per-step via :attr:`render_enabled`.
501+
497502
Args:
498503
action: The actions to apply on the environment. Shape is (num_envs, action_dim).
499504
@@ -507,7 +512,7 @@ def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]:
507512

508513
# check if we need to do rendering within the physics loop
509514
# note: uses cached property to avoid settings lookup every step
510-
is_rendering = self.sim.is_rendering
515+
is_rendering = self.render_enabled and self.sim.is_rendering
511516

512517
# perform physics stepping
513518
for _ in range(self.cfg.decimation):

source/isaaclab/isaaclab/envs/manager_based_rl_env.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn:
171171
6. Compute the observations.
172172
7. Return the observations, rewards, resets and extras.
173173
174+
Rendering can be controlled per-step via :attr:`render_enabled`.
175+
174176
Args:
175177
action: The actions to apply on the environment. Shape is (num_envs, action_dim).
176178
@@ -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 = self.render_enabled 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/test/envs/test_env_rendering_logic.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,167 @@ 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 setting render_enabled=False 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_enabled=False for several steps
299+
env.render_enabled = False
300+
for i in range(10):
301+
env.step(action=actions)
302+
303+
# Physics should still advance normally
304+
_, num_physics_steps = get_physics_stats()
305+
assert num_physics_steps == (i + 1) * env.cfg.decimation, "Physics steps mismatch with render_enabled=False"
306+
307+
# No rendering should have occurred
308+
_, num_render_steps = get_render_stats()
309+
assert num_render_steps == 0, f"Expected 0 render steps with render_enabled=False, got {num_render_steps}"
310+
311+
finally:
312+
if viz is not None and original_step is not None:
313+
viz.step = original_step
314+
if physics_handle is not None:
315+
physics_handle.deregister()
316+
if env is not None:
317+
env.close()
318+
else:
319+
SimulationContext.clear_instance()
320+
321+
322+
@pytest.mark.parametrize("env_type", ["manager_based_env", "manager_based_rl_env", "direct_rl_env"])
323+
def test_env_render_flag_mixed_steps(env_type, physics_callback, render_callback):
324+
"""Test that render_enabled can be toggled between steps and rendering counts are correct."""
325+
physics_cb, get_physics_stats = physics_callback
326+
render_cb, get_render_stats = render_callback
327+
328+
env = None
329+
physics_handle = None
330+
original_step = None
331+
viz = None
332+
333+
try:
334+
# create a new stage
335+
sim_utils.create_new_stage()
336+
337+
# create environment with render_interval=1 so every decimation step renders
338+
if env_type == "manager_based_env":
339+
env = create_manager_based_env(render_interval=1)
340+
elif env_type == "manager_based_rl_env":
341+
env = create_manager_based_rl_env(render_interval=1)
342+
else:
343+
env = create_direct_rl_env(render_interval=1)
344+
345+
# enable the flag to render the environment
346+
env.sim.set_setting("/isaaclab/render/rtx_sensors", True)
347+
348+
# disable the app from shutting down when the environment is closed
349+
env.sim._app_control_on_stop_handle = None # type: ignore
350+
351+
# Reset to initialize visualizers
352+
env.reset()
353+
354+
# Ensure the default Kit visualizer is active for rendering callbacks.
355+
assert isinstance(env.sim.visualizers[0], KitVisualizer)
356+
357+
# add physics callback
358+
physics_handle = env.sim.physics_manager.register_callback(
359+
physics_cb, IsaacEvents.POST_PHYSICS_STEP, name="physics_step"
360+
)
361+
362+
# Wrap visualizer step to track render calls
363+
viz = env.sim.visualizers[0]
364+
original_step = viz.step
365+
render_dt = env.cfg.sim.dt * env.cfg.sim.render_interval
366+
367+
def wrapped_step(dt):
368+
original_step(dt)
369+
render_cb(render_dt)
370+
371+
viz.step = wrapped_step
372+
373+
# create a zero action tensor for stepping the environment
374+
actions = torch.zeros((env.num_envs, 0), device=env.device)
375+
376+
expected_render_steps = 0
377+
378+
# Step 5 times with render_enabled=True, then 5 with render_enabled=False
379+
for i in range(10):
380+
should_render = i < 5
381+
env.render_enabled = should_render
382+
env.step(action=actions)
383+
384+
# Physics always advances
385+
_, num_physics_steps = get_physics_stats()
386+
assert num_physics_steps == (i + 1) * env.cfg.decimation, "Physics steps mismatch in mixed test"
387+
388+
# Rendering only happens in the first 5 steps
389+
if should_render:
390+
expected_render_steps += env.cfg.decimation # render_interval=1, so renders every decimation step
391+
392+
_, num_render_steps = get_render_stats()
393+
assert num_render_steps == expected_render_steps, (
394+
f"Render steps mismatch at step {i}: expected {expected_render_steps}, got {num_render_steps}"
395+
)
396+
397+
finally:
398+
if viz is not None and original_step is not None:
399+
viz.step = original_step
400+
if physics_handle is not None:
401+
physics_handle.deregister()
402+
if env is not None:
403+
env.close()
404+
else:
405+
SimulationContext.clear_instance()

0 commit comments

Comments
 (0)