Skip to content
Draft
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
3 changes: 2 additions & 1 deletion source/isaaclab/config/extension.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
[package]

# Note: Semantic Versioning is used: https://semver.org/
version = "4.6.5"
version = "4.6.6"


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

4.6.6 (2026-04-17)
~~~~~~~~~~~~~~~~~~~

Changed
^^^^^^^

* Introduced :class:`~isaaclab.renderers.render_context.RenderContext` on
:class:`~isaaclab.sim.simulation_context.SimulationContext` so all
:class:`~isaaclab.sensors.camera.Camera` sensors with compatible
:attr:`~isaaclab.sensors.camera.CameraCfg.renderer_cfg` share one
:class:`~isaaclab.renderers.base_renderer.BaseRenderer` instance for the simulation
lifetime. :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.prepare_stage` runs once.
For Newton and OVRTX backends, :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.update_transforms`
is invoked at most once per physics step (see :meth:`~isaaclab.sim.simulation_context.SimulationContext.get_physics_step_count`).
Mixing incompatible per-camera ``renderer_cfg`` in the same simulation raises :class:`RuntimeError`.

Added
^^^^^

* Added :meth:`~isaaclab.sim.simulation_context.SimulationContext.get_physics_step_count` and
:attr:`~isaaclab.sim.simulation_context.SimulationContext.render_context`.


4.6.5 (2026-04-16)
~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -37,6 +60,7 @@ Changed
Added
^^^^^


* Added :class:`~isaaclab.sim.spawners.meshes.MeshSquareCfg` and
:func:`~isaaclab.sim.spawners.meshes.spawn_mesh_square` for spawning 2D triangle
mesh grids, used as surface deformable bodies (cloth).
Expand Down
3 changes: 3 additions & 0 deletions source/isaaclab/isaaclab/renderers/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ __all__ = [
"BaseRenderer",
"Renderer",
"RendererCfg",
"RenderContext",
"renderer_cfgs_compatible",
]

from .base_renderer import BaseRenderer
from .renderer import Renderer
from .renderer_cfg import RendererCfg
from .render_context import RenderContext, renderer_cfgs_compatible
158 changes: 158 additions & 0 deletions source/isaaclab/isaaclab/renderers/render_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Copyright (c) 2022-2026, The Isaac Lab Project Developers
# (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

"""Simulation-scoped shared renderer for camera sensors."""

from __future__ import annotations

import logging
from typing import Any, cast

from .base_renderer import BaseRenderer
from .renderer import Renderer
from .renderer_cfg import RendererCfg

logger = logging.getLogger(__name__)

# Backends where update_transforms() syncs shared scene state; dedupe once per physics step.
_DEDUPE_TRANSFORM_BACKENDS: frozenset[str] = frozenset(
{"NewtonWarpRenderer", "OVRTXRenderer"}
)


def renderer_cfgs_compatible(a: RendererCfg, b: RendererCfg) -> bool:
"""Return True if two camera renderer configs may share one BaseRenderer.

Args:
a: Renderer configuration from the first camera using this context.
b: Renderer configuration from another camera.

Returns:
Whether both configs use the same concrete class and ``renderer_type``.
"""
if type(a) is not type(b):
return False
return getattr(a, "renderer_type", None) == getattr(b, "renderer_type", None)


class RenderContext:
"""Owns one Renderer / BaseRenderer for all scene cameras.

Every Camera with a compatible ``renderer_cfg`` shares the same backend.
``prepare_stage`` runs once. For Newton and OVRTX, ``update_transforms`` runs at
most once per physics step (see ``physics_step_count``); Isaac RTX uses a no-op
``update_transforms``.

Mixing incompatible ``renderer_cfg`` in one simulation raises RuntimeError.
"""

__slots__ = (
"_renderer",
"_canonical_cfg",
"_stage_prepared",
"_prepared_num_envs",
"_last_transforms_step",
)

def __init__(self) -> None:
self._renderer: BaseRenderer | None = None
self._canonical_cfg: RendererCfg | None = None
self._stage_prepared: bool = False
self._prepared_num_envs: int | None = None
self._last_transforms_step: int | None = None

@property
def renderer(self) -> BaseRenderer | None:
"""Shared backend, or None if no camera requested a renderer yet."""
return self._renderer

def get_renderer(self, cfg: RendererCfg) -> BaseRenderer:
"""Return the shared BaseRenderer, creating it on first use.

Args:
cfg: Renderer configuration from the initializing camera.

Returns:
Shared renderer backend.

Raises:
RuntimeError: If cfg is incompatible with an existing shared renderer.
"""
if self._renderer is None:
self._canonical_cfg = cfg
# Renderer.__new__ returns a BaseRenderer implementation.
self._renderer = cast(BaseRenderer, Renderer(cfg)) # type: ignore[misc]
logger.info(
"Created shared simulation renderer: %s",
type(self._renderer).__name__,
)
return self._renderer
if self._canonical_cfg is None or not renderer_cfgs_compatible(
self._canonical_cfg, cfg
):
ex_t = type(self._canonical_cfg).__name__
ex_r = getattr(self._canonical_cfg, "renderer_type", None)
rq_t = type(cfg).__name__
rq_r = getattr(cfg, "renderer_type", None)
raise RuntimeError(
"All Camera sensors must use the same concrete renderer configuration "
"class and renderer_type when sharing the simulation renderer. "
f"Existing: {ex_t} ({ex_r!r}); this camera requested: {rq_t} ({rq_r!r})."
)
return self._renderer

def ensure_prepare_stage(self, stage: Any, num_envs: int) -> None:
"""Call BaseRenderer.prepare_stage once for this simulation.

Args:
stage: USD stage passed to the backend.
num_envs: Environment count passed to the backend.

Raises:
RuntimeError: If get_renderer was never called, or num_envs disagrees
with a previous successful prepare_stage.
"""
if self._renderer is None:
raise RuntimeError("get_renderer must be called before ensure_prepare_stage.")
if not self._stage_prepared:
self._renderer.prepare_stage(stage, num_envs)
self._stage_prepared = True
self._prepared_num_envs = num_envs
return
if self._prepared_num_envs != num_envs:
raise RuntimeError(
"Shared renderer prepare_stage was already called with a different "
f"num_envs ({self._prepared_num_envs} vs {num_envs})."
)

def maybe_update_transforms(self, physics_step_count: int) -> None:
"""Call update_transforms at most once per physics step when needed.

Isaac RTX uses a no-op; Newton and OVRTX sync shared scene state.

Args:
physics_step_count: Monotonic counter from SimulationContext (see
get_physics_step_count).
"""
if self._renderer is None:
return
backend_name = type(self._renderer).__name__
if backend_name not in _DEDUPE_TRANSFORM_BACKENDS:
self._renderer.update_transforms()
return
if self._last_transforms_step == physics_step_count:
return
self._renderer.update_transforms()
self._last_transforms_step = physics_step_count

def reset_stage_prepare_flag(self) -> None:
"""Allow ensure_prepare_stage to run prepare_stage again (e.g. new USD stage)."""
self._stage_prepared = False
self._prepared_num_envs = None

def reset_transform_cadence(self) -> None:
"""Clear per-step transform dedupe (e.g. after a long pause with no physics)."""
self._last_transforms_step = None
20 changes: 15 additions & 5 deletions source/isaaclab/isaaclab/sensors/camera/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import isaaclab.sim as sim_utils
import isaaclab.utils.sensors as sensor_utils
from isaaclab.app.settings_manager import get_settings_manager
from isaaclab.renderers import BaseRenderer, Renderer
from isaaclab.renderers import BaseRenderer
from isaaclab.sim.views import XformPrimView
from isaaclab.utils import has_kit, to_camel_case
from isaaclab.utils.math import (
Expand Down Expand Up @@ -390,7 +390,8 @@ def reset(self, env_ids: Sequence[int] | None = None, env_mask: wp.array | None
def _initialize_impl(self):
"""Initializes the sensor handles and internal buffers.

This function creates a :class:`~isaaclab.renderers.Renderer` from the configured
This function obtains the simulation-scoped :class:`~isaaclab.renderers.base_renderer.BaseRenderer`
from :attr:`~isaaclab.sim.simulation_context.SimulationContext.render_context` using the configured
:attr:`~isaaclab.sensors.camera.CameraCfg.renderer_cfg` and delegates all render-product
and annotator management to it. It also initializes the internal buffers to store the data.

Expand All @@ -409,12 +410,15 @@ def _initialize_impl(self):
# Initialize parent class
super()._initialize_impl()

self._renderer = Renderer(self.cfg.renderer_cfg)
sim_ctx = sim_utils.SimulationContext.instance()
if sim_ctx is None:
raise RuntimeError("SimulationContext is not initialized.")
self._renderer = sim_ctx.render_context.get_renderer(self.cfg.renderer_cfg)
logger.info("Using renderer: %s", type(self._renderer).__name__)

# Stage preprocessing must happen before creating the view because the view keeps
# references to prims located in the stage.
self._renderer.prepare_stage(self.stage, self._num_envs)
sim_ctx.render_context.ensure_prepare_stage(self.stage, self._num_envs)

# Create a view for the sensor with Fabric enabled for fast pose queries.
# TODO: remove sync_usd_on_fabric_write=True once the GPU Fabric sync bug is fixed.
Expand Down Expand Up @@ -459,7 +463,9 @@ def _update_buffers_impl(self, env_mask: wp.array):
if self.cfg.update_latest_camera_pose:
self._update_poses(env_ids)

self._renderer.update_transforms()
sim_ctx = sim_utils.SimulationContext.instance()
if sim_ctx is not None:
sim_ctx.render_context.maybe_update_transforms(sim_ctx.get_physics_step_count())
self._renderer.render(self._render_data)

self._renderer.read_output(self._render_data, self._data)
Expand Down Expand Up @@ -630,6 +636,10 @@ def _update_poses(self, env_ids: Sequence[int]):

def _invalidate_initialize_callback(self, event):
"""Invalidates the scene elements."""
if self._renderer is not None and self._render_data is not None:
self._renderer.cleanup(self._render_data)
self._render_data = None
self._renderer = None
# call parent
super()._invalidate_initialize_callback(event)
# set all existing views to None to invalidate them
Expand Down
13 changes: 13 additions & 0 deletions source/isaaclab/isaaclab/sim/simulation_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
)
from isaaclab.sim.utils import create_new_stage
from isaaclab.utils.version import has_kit
from isaaclab.renderers.render_context import RenderContext
from isaaclab.visualizers.base_visualizer import BaseVisualizer

from .simulation_cfg import SimulationCfg
Expand Down Expand Up @@ -189,6 +190,9 @@ def __init__(self, cfg: SimulationCfg | None = None):
# Monotonic physics-step counter used by camera sensors for
self._physics_step_count: int = 0

# Shared renderer for all Camera sensors (compatible renderer_cfg only).
self._render_context = RenderContext()

type(self)._instance = self # Mark as valid singleton only after successful init

def _apply_render_cfg_settings(self) -> None:
Expand Down Expand Up @@ -352,6 +356,15 @@ def get_physics_dt(self) -> float:
"""Returns the physics time step."""
return self.physics_manager.get_physics_dt()

def get_physics_step_count(self) -> int:
"""Return the monotonic physics step counter (incremented each :meth:`step`)."""
return self._physics_step_count

@property
def render_context(self) -> RenderContext:
"""Shared :class:`~isaaclab.renderers.render_context.RenderContext` for camera renderers."""
return self._render_context

def _create_default_visualizer_configs(self, requested_visualizers: list[str]) -> list:
"""Create default visualizer configs for requested types.

Expand Down
Loading