From 01a9e38bc1b9b99d41ed7c776f54b3f083eec0cf Mon Sep 17 00:00:00 2001 From: Matthew Taylor Date: Fri, 16 Oct 2026 12:49:11 -0400 Subject: [PATCH 1/6] Move camera preparation lifecycle into SensorBase --- .../fix-camera-data-physics-dependency.skip | 0 .../isaaclab/sensors/camera/camera.py | 31 ++----------------- .../isaaclab/isaaclab/sensors/sensor_base.py | 15 +++++++++ .../test/sensors/test_opencv_distortion.py | 2 +- .../test/sensors/test_sensor_destructors.py | 1 - .../fix-camera-data-physics-dependency.skip | 0 .../test/sensors/test_contact_sensor.py | 1 + .../test/sensors/test_frame_transformer.py | 1 + .../sensors/test_imu_pva_recorded_launch.py | 1 + .../test/sensors/test_joint_wrench_sensor.py | 1 + .../test/sensors/test_ray_caster_graph.py | 1 + 11 files changed, 23 insertions(+), 31 deletions(-) create mode 100644 source/isaaclab/changelog.d/fix-camera-data-physics-dependency.skip create mode 100644 source/isaaclab_physx/changelog.d/fix-camera-data-physics-dependency.skip diff --git a/source/isaaclab/changelog.d/fix-camera-data-physics-dependency.skip b/source/isaaclab/changelog.d/fix-camera-data-physics-dependency.skip new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/source/isaaclab/isaaclab/sensors/camera/camera.py b/source/isaaclab/isaaclab/sensors/camera/camera.py index 1861aa096152..d45f7240efa9 100644 --- a/source/isaaclab/isaaclab/sensors/camera/camera.py +++ b/source/isaaclab/isaaclab/sensors/camera/camera.py @@ -7,7 +7,6 @@ import logging import sys -import weakref from collections.abc import Sequence from typing import TYPE_CHECKING, Any, Literal, cast @@ -19,7 +18,6 @@ from ... import sim as sim_utils from ...app.logging_utils import force_log_level -from ...physics import PhysicsEvent, PhysicsManager from ...renderers import BaseRenderer, CameraRenderSpec from ...sim.views import FrameView from ...utils.math import ( @@ -208,7 +206,6 @@ def __init__(self, cfg: CameraCfg): RuntimeError: If no camera prim is found at the given path. ValueError: If the provided data types are not supported by the camera or active renderer. """ - self._prepare_renderer_handle = None # perform check on supported data types self._check_supported_data_types(cfg) # initialize base class @@ -647,7 +644,7 @@ def _initialize_impl(self): # Per-camera USD setup must run **before** ``ensure_prepare_stage`` so renderers that snapshot # the stage (ovrtx's ``stage.Export``) capture the overrides in their exported USD. Every camera - # already did this in :meth:`_prepare_renderer_callback`; repeating it is harmless and covers + # already did this in :meth:`_prepare_initialize_callback`; repeating it is harmless and covers # cameras initialized without that callback. cam_paths = tuple(str(p.GetPath()) for p in sim_utils.find_matching_prims(self.cfg.prim_path, self.stage)) render_spec = CameraRenderSpec( @@ -1027,31 +1024,7 @@ def _env_mask_has_any(self, env_mask: wp.array) -> bool: Internal simulation callbacks. """ - def _register_callbacks(self): - """Register camera stage preparation before the ordinary sensor initialization callbacks.""" - super()._register_callbacks() - physics_mgr_cls = sim_utils.SimulationContext.instance().physics_manager - obj_ref = weakref.proxy(self) - - def _prepare(event): - obj_ref._prepare_renderer_callback(event) - - # All cameras must author their USD overrides before the first camera exports the shared stage. - # Sensor initialization runs at order 10. Preserve weak ownership and backend exception forwarding. - self._prepare_renderer_handle = physics_mgr_cls.register_callback( - lambda payload: PhysicsManager.safe_callback_invoke(_prepare, payload, physics_manager=physics_mgr_cls), - PhysicsEvent.PHYSICS_READY, - order=9, - ) - - def _clear_callbacks(self) -> None: - """Release the camera preparation subscription along with the ordinary sensor subscriptions.""" - if self._prepare_renderer_handle is not None: - self._prepare_renderer_handle.deregister() - self._prepare_renderer_handle = None - super()._clear_callbacks() - - def _prepare_renderer_callback(self, event): + def _prepare_initialize_callback(self, event): """Apply this camera's renderer USD overrides before any camera initializes. A renderer that exports the stage when the first camera initializes, such as OVRTX, then sees diff --git a/source/isaaclab/isaaclab/sensors/sensor_base.py b/source/isaaclab/isaaclab/sensors/sensor_base.py index b23b1fc43036..8e9ba2d319c9 100644 --- a/source/isaaclab/isaaclab/sensors/sensor_base.py +++ b/source/isaaclab/isaaclab/sensors/sensor_base.py @@ -55,6 +55,7 @@ def __init__(self, cfg: SensorBaseCfg): cfg: The configuration parameters for the sensor. """ # start with unset callback handles so cleanup is safe if construction fails part way + self._prepare_initialize_handle = None self._initialize_handle = None self._invalidate_initialize_handle = None self._prim_deletion_handle = None @@ -344,6 +345,13 @@ def _invoke(callback_name, event): getattr(obj_ref, callback_name)(event) # Backend-agnostic: PHYSICS_READY initializes sensors; STOP invalidates them. + self._prepare_initialize_handle = physics_mgr_cls.register_callback( + lambda payload: PhysicsManager.safe_callback_invoke( + _invoke, "_prepare_initialize_callback", payload, physics_manager=physics_mgr_cls + ), + PhysicsEvent.PHYSICS_READY, + order=9, + ) self._initialize_handle = physics_mgr_cls.register_callback( lambda payload: PhysicsManager.safe_callback_invoke( _invoke, "_initialize_callback", payload, physics_manager=physics_mgr_cls @@ -372,6 +380,10 @@ def _invoke(callback_name, event): IsaacEvents.PRIM_DELETION, ) + def _prepare_initialize_callback(self, event): + """Prepare shared stage state before any sensor's initialization callback runs.""" + pass + def _initialize_callback(self, event): """Initializes the scene elements. @@ -403,6 +415,9 @@ def _on_prim_deletion(self, event) -> None: def _clear_callbacks(self) -> None: """Clears the callbacks.""" + if self._prepare_initialize_handle is not None: + self._prepare_initialize_handle.deregister() + self._prepare_initialize_handle = None if self._initialize_handle is not None: self._initialize_handle.deregister() self._initialize_handle = None diff --git a/source/isaaclab/test/sensors/test_opencv_distortion.py b/source/isaaclab/test/sensors/test_opencv_distortion.py index 6eb473d085f6..ef53a991fde3 100644 --- a/source/isaaclab/test/sensors/test_opencv_distortion.py +++ b/source/isaaclab/test/sensors/test_opencv_distortion.py @@ -192,7 +192,7 @@ def _camera_for_prims(prims, width=640, height=480, device="cpu"): fake._ALL_INDICES = wp.array(np.arange(len(prims)), dtype=wp.int32, device=device) fake._ALL_ENV_MASK = wp.ones(len(prims), dtype=wp.bool, device=device) # attributes touched by ``__del__``/``_clear_callbacks`` when the fake object is garbage collected - fake._prepare_renderer_handle = None + fake._prepare_initialize_handle = None fake._initialize_handle = None fake._invalidate_initialize_handle = None fake._prim_deletion_handle = None diff --git a/source/isaaclab/test/sensors/test_sensor_destructors.py b/source/isaaclab/test/sensors/test_sensor_destructors.py index 99f910f60927..a5d057d9b8f3 100644 --- a/source/isaaclab/test/sensors/test_sensor_destructors.py +++ b/source/isaaclab/test/sensors/test_sensor_destructors.py @@ -77,7 +77,6 @@ def cleanup(self, _render_data): view = _View() renderer = _Renderer() camera = object.__new__(Camera) - camera._prepare_renderer_handle = None camera._view = view camera._renderer = renderer camera._render_data = None diff --git a/source/isaaclab_physx/changelog.d/fix-camera-data-physics-dependency.skip b/source/isaaclab_physx/changelog.d/fix-camera-data-physics-dependency.skip new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/source/isaaclab_physx/test/sensors/test_contact_sensor.py b/source/isaaclab_physx/test/sensors/test_contact_sensor.py index dd7fe402310c..1531154f1fbe 100644 --- a/source/isaaclab_physx/test/sensors/test_contact_sensor.py +++ b/source/isaaclab_physx/test/sensors/test_contact_sensor.py @@ -212,6 +212,7 @@ def _make_graph_test_sensor(device: str = "cuda:0") -> PhysxContactSensor: sensor._device = device sensor._use_graph = True sensor._compute_graph = None + sensor._prepare_initialize_handle = None sensor._initialize_handle = None sensor._invalidate_initialize_handle = None sensor._prim_deletion_handle = None diff --git a/source/isaaclab_physx/test/sensors/test_frame_transformer.py b/source/isaaclab_physx/test/sensors/test_frame_transformer.py index 2ff57c4c5805..b89aa7c9d0d9 100644 --- a/source/isaaclab_physx/test/sensors/test_frame_transformer.py +++ b/source/isaaclab_physx/test/sensors/test_frame_transformer.py @@ -667,6 +667,7 @@ def _make_frame_transformer(use_recorded_launch: bool = True): sensor._raw_transforms = None sensor._update_cmd = None sensor._use_recorded_launch = use_recorded_launch + sensor._prepare_initialize_handle = None sensor._initialize_handle = None sensor._invalidate_initialize_handle = None sensor._prim_deletion_handle = None diff --git a/source/isaaclab_physx/test/sensors/test_imu_pva_recorded_launch.py b/source/isaaclab_physx/test/sensors/test_imu_pva_recorded_launch.py index 25d035486332..7245e8073a48 100644 --- a/source/isaaclab_physx/test/sensors/test_imu_pva_recorded_launch.py +++ b/source/isaaclab_physx/test/sensors/test_imu_pva_recorded_launch.py @@ -135,6 +135,7 @@ def _make_sensor(sensor_type: str, use_recorded_launch: bool = True): sensor._update_cmd = None sensor._update_env_mask = None sensor._use_recorded_launch = use_recorded_launch + sensor._prepare_initialize_handle = None sensor._initialize_handle = None sensor._invalidate_initialize_handle = None sensor._prim_deletion_handle = None diff --git a/source/isaaclab_physx/test/sensors/test_joint_wrench_sensor.py b/source/isaaclab_physx/test/sensors/test_joint_wrench_sensor.py index 3ae0a6afdb82..dc96a74929ac 100644 --- a/source/isaaclab_physx/test/sensors/test_joint_wrench_sensor.py +++ b/source/isaaclab_physx/test/sensors/test_joint_wrench_sensor.py @@ -274,6 +274,7 @@ def _make_joint_wrench_sensor(use_recorded_launch: bool = True, num_envs: int = sensor._update_cmd = None sensor._use_recorded_launch = use_recorded_launch sensor._physics_sim_view = None + sensor._prepare_initialize_handle = None sensor._initialize_handle = None sensor._invalidate_initialize_handle = None sensor._prim_deletion_handle = None diff --git a/source/isaaclab_physx/test/sensors/test_ray_caster_graph.py b/source/isaaclab_physx/test/sensors/test_ray_caster_graph.py index dfd31fead524..468797cc7ee8 100644 --- a/source/isaaclab_physx/test/sensors/test_ray_caster_graph.py +++ b/source/isaaclab_physx/test/sensors/test_ray_caster_graph.py @@ -78,6 +78,7 @@ def _make_ray_caster(use_graph: bool = True, cache_raw_transforms: bool = True): sensor._transforms_prefetched = False sensor._test_output = wp.zeros(2, dtype=wp.vec3f, device=device) sensor._view = object() + sensor._prepare_initialize_handle = None sensor._initialize_handle = None sensor._invalidate_initialize_handle = None sensor._prim_deletion_handle = None From 921963e7164ff7f9e9a7f0326cc72c246e67c038 Mon Sep 17 00:00:00 2001 From: Matthew Taylor Date: Fri, 16 Oct 2026 12:54:44 -0400 Subject: [PATCH 2/6] Consolidate camera renderer preparation and reuse path sequences --- .../renderers/rtx_camera_overrides.py | 3 +- .../isaaclab/sensors/camera/camera.py | 29 +++++++------------ .../renderers/test_camera_output_contract.py | 2 +- .../fix-camera-data-physics-dependency.skip | 0 .../isaaclab_ov/renderers/ovrtx_renderer.py | 2 +- .../renderers/isaac_rtx_renderer.py | 2 +- 6 files changed, 15 insertions(+), 23 deletions(-) create mode 100644 source/isaaclab_ov/changelog.d/fix-camera-data-physics-dependency.skip diff --git a/source/isaaclab/isaaclab/renderers/rtx_camera_overrides.py b/source/isaaclab/isaaclab/renderers/rtx_camera_overrides.py index 632f07adc8ee..09b63803fdcd 100644 --- a/source/isaaclab/isaaclab/renderers/rtx_camera_overrides.py +++ b/source/isaaclab/isaaclab/renderers/rtx_camera_overrides.py @@ -13,6 +13,7 @@ from __future__ import annotations +from collections.abc import Sequence from typing import Any # Both RTX backends consume these schemas without requiring the schema plugin at authoring time. @@ -27,7 +28,7 @@ ) -def _apply_rtx_exposure_overrides(stage: Any, prim_paths: list[str]) -> None: +def _apply_rtx_exposure_overrides(stage: Any, prim_paths: Sequence[str]) -> None: """Disable RTX camera exposure so downstream processors receive scene-linear HDR. Existing API schemas are preserved. Schema metadata is authored directly so the RTX schema diff --git a/source/isaaclab/isaaclab/sensors/camera/camera.py b/source/isaaclab/isaaclab/sensors/camera/camera.py index d45f7240efa9..afce70a1409b 100644 --- a/source/isaaclab/isaaclab/sensors/camera/camera.py +++ b/source/isaaclab/isaaclab/sensors/camera/camera.py @@ -638,23 +638,12 @@ def _initialize_impl(self): sim_ctx = sim_utils.SimulationContext.instance() if sim_ctx is None: raise RuntimeError("SimulationContext is not initialized.") - # Normally created in ``__init__``; only missing when the camera was built without a simulation. - if self._renderer is None: - self._renderer = sim_ctx.get_or_create_backend(self.cfg.renderer_cfg) - # Per-camera USD setup must run **before** ``ensure_prepare_stage`` so renderers that snapshot # the stage (ovrtx's ``stage.Export``) capture the overrides in their exported USD. Every camera # already did this in :meth:`_prepare_initialize_callback`; repeating it is harmless and covers # cameras initialized without that callback. - cam_paths = tuple(str(p.GetPath()) for p in sim_utils.find_matching_prims(self.cfg.prim_path, self.stage)) - render_spec = CameraRenderSpec( - cfg=self.cfg, - device=str(sim_ctx.device), - num_instances=self._num_envs, - camera_prim_paths=cam_paths, - view_count=self._num_envs, - ) - self._renderer.prepare_cameras(self.stage, render_spec) + render_spec = self._prepare_renderer(self._num_envs) + cam_paths = render_spec.camera_prim_paths # Stage preprocessing must happen before creating the view because the view keeps # references to prims located in the stage. @@ -1032,16 +1021,17 @@ def _prepare_initialize_callback(self, event): """ if self._is_initialized: return + self._prepare_renderer() + + def _prepare_renderer(self, num_views: int | None = None) -> CameraRenderSpec: + """Resolve camera prims and apply renderer overrides before shared-stage export.""" sim_ctx = sim_utils.SimulationContext.instance() if self._renderer is None: self._renderer = sim_ctx.get_or_create_backend(self.cfg.renderer_cfg) - # Matches the environment count that sensor initialization derives next. - clone_plan = sim_ctx.get_clone_plan() - if clone_plan is not None: - num_views = len(clone_plan.topology.world_prototype_layout) - else: - num_views = len(sim_utils.find_matching_prims(self.cfg.prim_path, self.stage)) cam_paths = tuple(str(p.GetPath()) for p in sim_utils.find_matching_prims(self.cfg.prim_path, self.stage)) + if num_views is None: + clone_plan = sim_ctx.get_clone_plan() + num_views = len(clone_plan.topology.world_prototype_layout) if clone_plan is not None else len(cam_paths) render_spec = CameraRenderSpec( cfg=self.cfg, device=str(sim_ctx.device), @@ -1050,6 +1040,7 @@ def _prepare_initialize_callback(self, event): view_count=num_views, ) self._renderer.prepare_cameras(self.stage, render_spec) + return render_spec def _invalidate_initialize_callback(self, event): """Invalidates the scene elements.""" diff --git a/source/isaaclab/test/renderers/test_camera_output_contract.py b/source/isaaclab/test/renderers/test_camera_output_contract.py index 2e07fd64a9ac..3d0f1f748740 100644 --- a/source/isaaclab/test/renderers/test_camera_output_contract.py +++ b/source/isaaclab/test/renderers/test_camera_output_contract.py @@ -296,7 +296,7 @@ class CameraPhysicsManager(PhysicsManager): def prepare_cameras(stage, spec): if "rgb_radiance" in spec.cfg.data_types: - _apply_rtx_exposure_overrides(stage, list(spec.camera_prim_paths)) + _apply_rtx_exposure_overrides(stage, spec.camera_prim_paths) def export_stage(stage, num_envs): if not exported_iso: diff --git a/source/isaaclab_ov/changelog.d/fix-camera-data-physics-dependency.skip b/source/isaaclab_ov/changelog.d/fix-camera-data-physics-dependency.skip new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py index 88e8904ba945..9159f99de4be 100644 --- a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py +++ b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py @@ -462,7 +462,7 @@ def prepare_cameras(self, stage: Any, spec: CameraRenderSpec) -> None: spec.cfg.isp_cfg = resolve_and_normalize(spec.cfg.isp_cfg, stage, camera_prim_path) needs_neutral_exposure = spec.cfg.isp_cfg is not None or "rgb_radiance" in spec.cfg.data_types if needs_neutral_exposure and spec.camera_prim_paths: - _apply_rtx_exposure_overrides(stage, list(spec.camera_prim_paths)) + _apply_rtx_exposure_overrides(stage, spec.camera_prim_paths) def prepare_stage(self, stage: Any, num_envs: int) -> None: """Prepare the USD stage for OVRTX before :meth:`create_render_data`. diff --git a/source/isaaclab_physx/isaaclab_physx/renderers/isaac_rtx_renderer.py b/source/isaaclab_physx/isaaclab_physx/renderers/isaac_rtx_renderer.py index 3507061486cf..da5809f49c92 100644 --- a/source/isaaclab_physx/isaaclab_physx/renderers/isaac_rtx_renderer.py +++ b/source/isaaclab_physx/isaaclab_physx/renderers/isaac_rtx_renderer.py @@ -243,7 +243,7 @@ def prepare_cameras(self, stage: Any, spec: CameraRenderSpec) -> None: get_settings_manager().set("/rtx/rtpt/gaussian/skipTonemapping/enabled", False) needs_neutral_exposure = spec.cfg.isp_cfg is not None or "rgb_radiance" in spec.cfg.data_types if needs_neutral_exposure and spec.camera_prim_paths: - _apply_rtx_exposure_overrides(stage, list(spec.camera_prim_paths)) + _apply_rtx_exposure_overrides(stage, spec.camera_prim_paths) def supported_output_types(self) -> dict[RenderBufferKind, RenderBufferSpec]: """Publish the per-output Replicator layout this RTX backend writes. From 9d9eb647a2ee6a7e500e683297522874b803f269 Mon Sep 17 00:00:00 2001 From: Matthew Taylor Date: Fri, 16 Oct 2026 13:10:57 -0400 Subject: [PATCH 3/6] Consolidate RTX exposure policy and renderer contract tests --- .../renderers/rtx_camera_overrides.py | 17 +++++--- .../renderers/test_camera_output_contract.py | 10 ++--- .../renderers/test_rtx_camera_overrides.py | 22 ++++++++-- .../isaaclab_ov/renderers/ovrtx_renderer.py | 4 +- .../test/test_ovrtx_renderer_contract.py | 16 -------- .../renderers/isaac_rtx_renderer.py | 4 +- .../test_isaac_rtx_renderer_contract.py | 41 +------------------ 7 files changed, 38 insertions(+), 76 deletions(-) diff --git a/source/isaaclab/isaaclab/renderers/rtx_camera_overrides.py b/source/isaaclab/isaaclab/renderers/rtx_camera_overrides.py index 09b63803fdcd..1a8399203410 100644 --- a/source/isaaclab/isaaclab/renderers/rtx_camera_overrides.py +++ b/source/isaaclab/isaaclab/renderers/rtx_camera_overrides.py @@ -13,8 +13,10 @@ from __future__ import annotations -from collections.abc import Sequence -from typing import Any +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from .camera_render_spec import CameraRenderSpec # Both RTX backends consume these schemas without requiring the schema plugin at authoring time. _CAMERA_EXPOSURE_SCHEMAS = ("OmniRtxCameraAutoExposureAPI_1", "OmniRtxCameraExposureAPI_1") @@ -28,20 +30,23 @@ ) -def _apply_rtx_exposure_overrides(stage: Any, prim_paths: Sequence[str]) -> None: - """Disable RTX camera exposure so downstream processors receive scene-linear HDR. +def _apply_rtx_exposure_overrides(stage: Any, spec: CameraRenderSpec) -> None: + """Neutralize RTX exposure for radiance or ISP inputs; preserve standalone HDR exposure. Existing API schemas are preserved. Schema metadata is authored directly so the RTX schema plugin does not need to be loaded yet. Args: stage: USD stage containing the camera prims. - prim_paths: Resolved camera prim paths. + spec: Camera configuration and resolved camera prim paths. """ + if spec.cfg.isp_cfg is None and "rgb_radiance" not in spec.cfg.data_types: + return + from pxr import Sdf value_types = {"Bool": Sdf.ValueTypeNames.Bool, "Float": Sdf.ValueTypeNames.Float} - for prim_path in prim_paths: + for prim_path in spec.camera_prim_paths: prim = stage.GetPrimAtPath(prim_path) if not prim or not prim.IsValid(): continue diff --git a/source/isaaclab/test/renderers/test_camera_output_contract.py b/source/isaaclab/test/renderers/test_camera_output_contract.py index 3d0f1f748740..8e5181a8ff11 100644 --- a/source/isaaclab/test/renderers/test_camera_output_contract.py +++ b/source/isaaclab/test/renderers/test_camera_output_contract.py @@ -294,17 +294,13 @@ class CameraPhysicsManager(PhysicsManager): } exported_iso = [] - def prepare_cameras(stage, spec): - if "rgb_radiance" in spec.cfg.data_types: - _apply_rtx_exposure_overrides(stage, spec.camera_prim_paths) - def export_stage(stage, num_envs): if not exported_iso: exported_iso.append(prims[1].GetAttribute("exposure:iso").Get()) renderer = SimpleNamespace( supported_output_types=lambda: specs, - prepare_cameras=prepare_cameras, + prepare_cameras=_apply_rtx_exposure_overrides, create_render_data=lambda spec: SimpleNamespace(spec=spec), set_outputs=lambda data, outputs: None, cleanup=lambda data: None, @@ -328,7 +324,9 @@ def export_stage(stage, num_envs): cameras = [] for prim, data_types in zip(prims, (["rgb"], ["rgb_radiance"])): camera = Camera.__new__(Camera) - camera.cfg = SimpleNamespace(prim_path=str(prim.GetPath()), data_types=data_types, height=2, width=3) + camera.cfg = SimpleNamespace( + prim_path=str(prim.GetPath()), data_types=data_types, height=2, width=3, isp_cfg=None + ) camera.stage = stage camera._device = "cpu" camera._num_envs = 2 diff --git a/source/isaaclab/test/renderers/test_rtx_camera_overrides.py b/source/isaaclab/test/renderers/test_rtx_camera_overrides.py index 73fab8a08d23..05b32e526510 100644 --- a/source/isaaclab/test/renderers/test_rtx_camera_overrides.py +++ b/source/isaaclab/test/renderers/test_rtx_camera_overrides.py @@ -5,12 +5,20 @@ """Tests for the RTX camera-exposure overrides used for scene-linear color outputs.""" +from types import SimpleNamespace + +import pytest + from pxr import Sdf, Usd, UsdGeom from isaaclab.renderers.rtx_camera_overrides import _apply_rtx_exposure_overrides -def test_apply_rtx_exposure_overrides_resets_authored_exposure_values(): +@pytest.mark.parametrize( + ("data_types", "isp_cfg", "expected_iso"), + [(["rgb_hdr"], None, 800.0), (["rgb_radiance"], None, 0.0), (["rgb"], object(), 0.0)], +) +def test_apply_rtx_exposure_overrides_resets_authored_exposure_values(data_types, isp_cfg, expected_iso): """Scene-linear outputs require neutral RTX-side exposure on the camera prim.""" stage = Usd.Stage.CreateInMemory() cam = UsdGeom.Camera.Define(stage, "/World/Camera") @@ -23,11 +31,19 @@ def test_apply_rtx_exposure_overrides_resets_authored_exposure_values(): prim.CreateAttribute("exposure:time", Sdf.ValueTypeNames.Float).Set(1.0 / 60.0) prim.CreateAttribute("omni:rtx:autoExposure:enabled", Sdf.ValueTypeNames.Bool).Set(True) - _apply_rtx_exposure_overrides(stage, ["/World/Camera"]) + spec = SimpleNamespace( + cfg=SimpleNamespace(data_types=data_types, isp_cfg=isp_cfg), camera_prim_paths=("/World/Camera",) + ) + _apply_rtx_exposure_overrides(stage, spec) + + assert prim.GetAttribute("exposure:iso").Get() == expected_iso + if expected_iso == 800.0: + assert prim.GetAttribute("exposure").Get() == 2.0 + assert prim.GetAttribute("omni:rtx:autoExposure:enabled").Get() is True + return assert prim.GetAttribute("exposure").Get() == 0.0 assert prim.GetAttribute("exposure:fStop").Get() == 1.0 - assert prim.GetAttribute("exposure:iso").Get() == 0.0 assert prim.GetAttribute("exposure:responsivity").Get() == 1.0 assert prim.GetAttribute("exposure:time").Get() == 1.0 assert prim.GetAttribute("omni:rtx:autoExposure:enabled").Get() is False diff --git a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py index 9159f99de4be..8e15439bcce3 100644 --- a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py +++ b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py @@ -460,9 +460,7 @@ def prepare_cameras(self, stage: Any, spec: CameraRenderSpec) -> None: camera_prim_path = spec.camera_prim_paths[0] if spec.camera_prim_paths else None spec.cfg.isp_cfg = resolve_and_normalize(spec.cfg.isp_cfg, stage, camera_prim_path) - needs_neutral_exposure = spec.cfg.isp_cfg is not None or "rgb_radiance" in spec.cfg.data_types - if needs_neutral_exposure and spec.camera_prim_paths: - _apply_rtx_exposure_overrides(stage, spec.camera_prim_paths) + _apply_rtx_exposure_overrides(stage, spec) def prepare_stage(self, stage: Any, num_envs: int) -> None: """Prepare the USD stage for OVRTX before :meth:`create_render_data`. diff --git a/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py b/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py index 96153d1799ad..28f7524242d9 100644 --- a/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py +++ b/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py @@ -570,22 +570,6 @@ def test_ovrtx_hdr_color_fills_requested_hdr_outputs(monkeypatch, data_types): np.testing.assert_array_equal(data.output[name].warp.numpy(), 2.0) -@pytest.mark.parametrize("data_types", [["rgb_hdr"], ["rgb_radiance"]]) -def test_ovrtx_prepare_cameras_neutralizes_exposure_for_radiance(data_types): - """Radiance neutralizes camera exposure; standalone HDR keeps the authored camera settings.""" - from pxr import Sdf, Usd, UsdGeom - - stage = Usd.Stage.CreateInMemory() - camera = UsdGeom.Camera.Define(stage, "/World/Camera").GetPrim() - camera.CreateAttribute("exposure:iso", Sdf.ValueTypeNames.Float).Set(100.0) - spec = types.SimpleNamespace( - cfg=types.SimpleNamespace(data_types=data_types, isp_cfg=None), camera_prim_paths=("/World/Camera",) - ) - _make_ovrtx_renderer_without_backend().prepare_cameras(stage, spec) - - assert camera.GetAttribute("exposure:iso").Get() == (0.0 if "rgb_radiance" in data_types else 100.0) - - def test_ovrtx_set_outputs_wraps_caller_torch_zero_copy(): """OVRTXRenderer.set_outputs publishes warp views over the caller's warp storage.""" renderer = _make_ovrtx_renderer_without_backend() diff --git a/source/isaaclab_physx/isaaclab_physx/renderers/isaac_rtx_renderer.py b/source/isaaclab_physx/isaaclab_physx/renderers/isaac_rtx_renderer.py index da5809f49c92..bd2177944782 100644 --- a/source/isaaclab_physx/isaaclab_physx/renderers/isaac_rtx_renderer.py +++ b/source/isaaclab_physx/isaaclab_physx/renderers/isaac_rtx_renderer.py @@ -241,9 +241,7 @@ def prepare_cameras(self, stage: Any, spec: CameraRenderSpec) -> None: if spec.cfg.isp_cfg is not None or not _HDR_DATA_TYPES.isdisjoint(spec.cfg.data_types): # Gaussian splats otherwise skip tonemapping and never reach the HdrColor AOV. get_settings_manager().set("/rtx/rtpt/gaussian/skipTonemapping/enabled", False) - needs_neutral_exposure = spec.cfg.isp_cfg is not None or "rgb_radiance" in spec.cfg.data_types - if needs_neutral_exposure and spec.camera_prim_paths: - _apply_rtx_exposure_overrides(stage, spec.camera_prim_paths) + _apply_rtx_exposure_overrides(stage, spec) def supported_output_types(self) -> dict[RenderBufferKind, RenderBufferSpec]: """Publish the per-output Replicator layout this RTX backend writes. diff --git a/source/isaaclab_physx/test/renderers/test_isaac_rtx_renderer_contract.py b/source/isaaclab_physx/test/renderers/test_isaac_rtx_renderer_contract.py index 156da3c2a1b9..9b7ef47baa31 100644 --- a/source/isaaclab_physx/test/renderers/test_isaac_rtx_renderer_contract.py +++ b/source/isaaclab_physx/test/renderers/test_isaac_rtx_renderer_contract.py @@ -87,26 +87,6 @@ def test_native_fabric_geometry_needs_no_separate_publication(monkeypatch): provider.get_geometry_points.assert_not_called() -def test_renderer_marks_rtx_sensor_rendering_when_created(monkeypatch): - """Creating the renderer, which cameras do during scene construction, marks RTX sensor rendering.""" - _install_omni_stubs(monkeypatch) - import isaaclab_physx.renderers.isaac_rtx_renderer as rtx_renderer - from isaaclab_physx.renderers.isaac_rtx_renderer_cfg import IsaacRtxRendererCfg - - settings = MagicMock() - settings.get.return_value = False - with ( - patch.object(rtx_renderer, "get_settings_manager", return_value=settings), - patch.object(rtx_renderer, "enable_extension"), - patch.object(rtx_renderer, "apply_isaac_rtx_global_settings"), - patch.object(rtx_renderer, "ensure_rtx_hydra_engine_attached"), - ): - rtx_renderer.IsaacRtxRenderer(IsaacRtxRendererCfg()) - - settings.set.assert_any_call("/isaaclab/render/rtx_sensors", True) - settings.set.assert_any_call("/physics/fabricUpdateTransformations", True) - - @pytest.mark.parametrize( ("data_types", "isp_cfg", "routes_hdr"), [(["rgb"], None, False), (["rgb_hdr"], None, True), (["rgb_radiance"], None, True), (["rgb"], object(), True)], @@ -128,25 +108,6 @@ def test_prepare_cameras_routes_gaussian_hdr_for_hdr_cameras(monkeypatch, data_t assert (hdr_call in settings.set.call_args_list) is routes_hdr -@pytest.mark.parametrize("data_types", [["rgb_hdr"], ["rgb_radiance"]]) -def test_prepare_cameras_neutralizes_exposure_for_radiance(monkeypatch, data_types): - """Radiance neutralizes camera exposure; standalone HDR keeps the authored camera settings.""" - _install_omni_stubs(monkeypatch) - from isaaclab_physx.renderers.isaac_rtx_renderer import IsaacRtxRenderer - - from pxr import Sdf, Usd, UsdGeom - - stage = Usd.Stage.CreateInMemory() - camera = UsdGeom.Camera.Define(stage, "/World/Camera").GetPrim() - camera.CreateAttribute("exposure:iso", Sdf.ValueTypeNames.Float).Set(100.0) - spec = SimpleNamespace( - cfg=SimpleNamespace(data_types=data_types, isp_cfg=None), camera_prim_paths=("/World/Camera",) - ) - IsaacRtxRenderer.__new__(IsaacRtxRenderer).prepare_cameras(stage, spec) - - assert camera.GetAttribute("exposure:iso").Get() == (0.0 if "rgb_radiance" in data_types else 100.0) - - @pytest.mark.parametrize("data_types", [["rgb_radiance"], ["rgb_radiance", "rgb_hdr"]]) def test_radiance_reads_the_hdr_annotator_into_its_output(monkeypatch, data_types): """Radiance and HDR attach one HdrColor annotator and extract it into the camera's own buffers.""" @@ -483,6 +444,8 @@ def _record_global_settings(*_args): rtx_renderer.IsaacRtxRenderer(IsaacRtxRendererCfg()) assert call_order == ["enable", "global_settings"] + settings.set.assert_any_call("/isaaclab/render/rtx_sensors", True) + settings.set.assert_any_call("/physics/fabricUpdateTransformations", True) @pytest.mark.parametrize("configured_value", [None, False, True]) From d6f25744a166ff57b0840689c76839e41bbcbe2c Mon Sep 17 00:00:00 2001 From: Matthew Taylor Date: Fri, 16 Oct 2026 13:30:35 -0400 Subject: [PATCH 4/6] Group sensor initialization callback handles --- .../isaaclab/isaaclab/sensors/sensor_base.py | 36 +++++++++---------- .../test/sensors/test_opencv_distortion.py | 3 +- .../test/sensors/test_contact_sensor.py | 3 +- .../test/sensors/test_frame_transformer.py | 3 +- .../sensors/test_imu_pva_recorded_launch.py | 3 +- .../test/sensors/test_joint_wrench_sensor.py | 3 +- .../test/sensors/test_ray_caster_graph.py | 3 +- 7 files changed, 24 insertions(+), 30 deletions(-) diff --git a/source/isaaclab/isaaclab/sensors/sensor_base.py b/source/isaaclab/isaaclab/sensors/sensor_base.py index 8e9ba2d319c9..1266297bead5 100644 --- a/source/isaaclab/isaaclab/sensors/sensor_base.py +++ b/source/isaaclab/isaaclab/sensors/sensor_base.py @@ -55,8 +55,7 @@ def __init__(self, cfg: SensorBaseCfg): cfg: The configuration parameters for the sensor. """ # start with unset callback handles so cleanup is safe if construction fails part way - self._prepare_initialize_handle = None - self._initialize_handle = None + self._initialize_handles = () self._invalidate_initialize_handle = None self._prim_deletion_handle = None # handle for debug visualization (this is set to a valid handle inside set_debug_vis) @@ -345,19 +344,23 @@ def _invoke(callback_name, event): getattr(obj_ref, callback_name)(event) # Backend-agnostic: PHYSICS_READY initializes sensors; STOP invalidates them. - self._prepare_initialize_handle = physics_mgr_cls.register_callback( - lambda payload: PhysicsManager.safe_callback_invoke( - _invoke, "_prepare_initialize_callback", payload, physics_manager=physics_mgr_cls + self._initialize_handles = ( + physics_mgr_cls.register_callback( + lambda payload: PhysicsManager.safe_callback_invoke( + _invoke, "_prepare_initialize_callback", payload, physics_manager=physics_mgr_cls + ), + PhysicsEvent.PHYSICS_READY, + order=9, ), - PhysicsEvent.PHYSICS_READY, - order=9, ) - self._initialize_handle = physics_mgr_cls.register_callback( - lambda payload: PhysicsManager.safe_callback_invoke( - _invoke, "_initialize_callback", payload, physics_manager=physics_mgr_cls + self._initialize_handles += ( + physics_mgr_cls.register_callback( + lambda payload: PhysicsManager.safe_callback_invoke( + _invoke, "_initialize_callback", payload, physics_manager=physics_mgr_cls + ), + PhysicsEvent.PHYSICS_READY, + order=10, ), - PhysicsEvent.PHYSICS_READY, - order=10, ) self._invalidate_initialize_handle = physics_mgr_cls.register_callback( lambda payload: PhysicsManager.safe_callback_invoke( @@ -415,12 +418,9 @@ def _on_prim_deletion(self, event) -> None: def _clear_callbacks(self) -> None: """Clears the callbacks.""" - if self._prepare_initialize_handle is not None: - self._prepare_initialize_handle.deregister() - self._prepare_initialize_handle = None - if self._initialize_handle is not None: - self._initialize_handle.deregister() - self._initialize_handle = None + for handle in self._initialize_handles: + handle.deregister() + self._initialize_handles = () if self._invalidate_initialize_handle is not None: self._invalidate_initialize_handle.deregister() self._invalidate_initialize_handle = None diff --git a/source/isaaclab/test/sensors/test_opencv_distortion.py b/source/isaaclab/test/sensors/test_opencv_distortion.py index ef53a991fde3..0c7908b9c795 100644 --- a/source/isaaclab/test/sensors/test_opencv_distortion.py +++ b/source/isaaclab/test/sensors/test_opencv_distortion.py @@ -192,8 +192,7 @@ def _camera_for_prims(prims, width=640, height=480, device="cpu"): fake._ALL_INDICES = wp.array(np.arange(len(prims)), dtype=wp.int32, device=device) fake._ALL_ENV_MASK = wp.ones(len(prims), dtype=wp.bool, device=device) # attributes touched by ``__del__``/``_clear_callbacks`` when the fake object is garbage collected - fake._prepare_initialize_handle = None - fake._initialize_handle = None + fake._initialize_handles = () fake._invalidate_initialize_handle = None fake._prim_deletion_handle = None fake._debug_vis_handle = None diff --git a/source/isaaclab_physx/test/sensors/test_contact_sensor.py b/source/isaaclab_physx/test/sensors/test_contact_sensor.py index 1531154f1fbe..9844739c031b 100644 --- a/source/isaaclab_physx/test/sensors/test_contact_sensor.py +++ b/source/isaaclab_physx/test/sensors/test_contact_sensor.py @@ -212,8 +212,7 @@ def _make_graph_test_sensor(device: str = "cuda:0") -> PhysxContactSensor: sensor._device = device sensor._use_graph = True sensor._compute_graph = None - sensor._prepare_initialize_handle = None - sensor._initialize_handle = None + sensor._initialize_handles = () sensor._invalidate_initialize_handle = None sensor._prim_deletion_handle = None sensor._resolve_indices_and_mask = lambda env_ids, mask: mask diff --git a/source/isaaclab_physx/test/sensors/test_frame_transformer.py b/source/isaaclab_physx/test/sensors/test_frame_transformer.py index b89aa7c9d0d9..c3898ec620a1 100644 --- a/source/isaaclab_physx/test/sensors/test_frame_transformer.py +++ b/source/isaaclab_physx/test/sensors/test_frame_transformer.py @@ -667,8 +667,7 @@ def _make_frame_transformer(use_recorded_launch: bool = True): sensor._raw_transforms = None sensor._update_cmd = None sensor._use_recorded_launch = use_recorded_launch - sensor._prepare_initialize_handle = None - sensor._initialize_handle = None + sensor._initialize_handles = () sensor._invalidate_initialize_handle = None sensor._prim_deletion_handle = None diff --git a/source/isaaclab_physx/test/sensors/test_imu_pva_recorded_launch.py b/source/isaaclab_physx/test/sensors/test_imu_pva_recorded_launch.py index 7245e8073a48..fe47301d0b47 100644 --- a/source/isaaclab_physx/test/sensors/test_imu_pva_recorded_launch.py +++ b/source/isaaclab_physx/test/sensors/test_imu_pva_recorded_launch.py @@ -135,8 +135,7 @@ def _make_sensor(sensor_type: str, use_recorded_launch: bool = True): sensor._update_cmd = None sensor._update_env_mask = None sensor._use_recorded_launch = use_recorded_launch - sensor._prepare_initialize_handle = None - sensor._initialize_handle = None + sensor._initialize_handles = () sensor._invalidate_initialize_handle = None sensor._prim_deletion_handle = None # ``_update_buffers_impl`` re-reads scene gravity so runtime randomization stays visible. diff --git a/source/isaaclab_physx/test/sensors/test_joint_wrench_sensor.py b/source/isaaclab_physx/test/sensors/test_joint_wrench_sensor.py index dc96a74929ac..0f50f4abf0d2 100644 --- a/source/isaaclab_physx/test/sensors/test_joint_wrench_sensor.py +++ b/source/isaaclab_physx/test/sensors/test_joint_wrench_sensor.py @@ -274,8 +274,7 @@ def _make_joint_wrench_sensor(use_recorded_launch: bool = True, num_envs: int = sensor._update_cmd = None sensor._use_recorded_launch = use_recorded_launch sensor._physics_sim_view = None - sensor._prepare_initialize_handle = None - sensor._initialize_handle = None + sensor._initialize_handles = () sensor._invalidate_initialize_handle = None sensor._prim_deletion_handle = None diff --git a/source/isaaclab_physx/test/sensors/test_ray_caster_graph.py b/source/isaaclab_physx/test/sensors/test_ray_caster_graph.py index 468797cc7ee8..5f17d7b5f511 100644 --- a/source/isaaclab_physx/test/sensors/test_ray_caster_graph.py +++ b/source/isaaclab_physx/test/sensors/test_ray_caster_graph.py @@ -78,8 +78,7 @@ def _make_ray_caster(use_graph: bool = True, cache_raw_transforms: bool = True): sensor._transforms_prefetched = False sensor._test_output = wp.zeros(2, dtype=wp.vec3f, device=device) sensor._view = object() - sensor._prepare_initialize_handle = None - sensor._initialize_handle = None + sensor._initialize_handles = () sensor._invalidate_initialize_handle = None sensor._prim_deletion_handle = None From 4a5fe21b8dc09ca242b31cdc1dabba3c2be9b72a Mon Sep 17 00:00:00 2001 From: Matthew Taylor Date: Fri, 16 Oct 2026 13:34:23 -0400 Subject: [PATCH 5/6] Own sensor initialization subscriptions with one cleanup handle --- .../isaaclab/isaaclab/sensors/sensor_base.py | 36 +++++++++---------- .../test/sensors/test_opencv_distortion.py | 2 +- .../test/sensors/test_contact_sensor.py | 2 +- .../test/sensors/test_frame_transformer.py | 2 +- .../sensors/test_imu_pva_recorded_launch.py | 2 +- .../test/sensors/test_joint_wrench_sensor.py | 2 +- .../test/sensors/test_ray_caster_graph.py | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/source/isaaclab/isaaclab/sensors/sensor_base.py b/source/isaaclab/isaaclab/sensors/sensor_base.py index 1266297bead5..5998df0ef062 100644 --- a/source/isaaclab/isaaclab/sensors/sensor_base.py +++ b/source/isaaclab/isaaclab/sensors/sensor_base.py @@ -17,6 +17,7 @@ import weakref from abc import ABC, abstractmethod from collections.abc import Callable, Sequence +from contextlib import ExitStack from typing import TYPE_CHECKING, Any import warp as wp @@ -55,7 +56,7 @@ def __init__(self, cfg: SensorBaseCfg): cfg: The configuration parameters for the sensor. """ # start with unset callback handles so cleanup is safe if construction fails part way - self._initialize_handles = () + self._initialize_handle = None self._invalidate_initialize_handle = None self._prim_deletion_handle = None # handle for debug visualization (this is set to a valid handle inside set_debug_vis) @@ -344,24 +345,23 @@ def _invoke(callback_name, event): getattr(obj_ref, callback_name)(event) # Backend-agnostic: PHYSICS_READY initializes sensors; STOP invalidates them. - self._initialize_handles = ( - physics_mgr_cls.register_callback( - lambda payload: PhysicsManager.safe_callback_invoke( - _invoke, "_prepare_initialize_callback", payload, physics_manager=physics_mgr_cls - ), - PhysicsEvent.PHYSICS_READY, - order=9, + self._initialize_handle = ExitStack() + prepare_handle = physics_mgr_cls.register_callback( + lambda payload: PhysicsManager.safe_callback_invoke( + _invoke, "_prepare_initialize_callback", payload, physics_manager=physics_mgr_cls ), + PhysicsEvent.PHYSICS_READY, + order=9, ) - self._initialize_handles += ( - physics_mgr_cls.register_callback( - lambda payload: PhysicsManager.safe_callback_invoke( - _invoke, "_initialize_callback", payload, physics_manager=physics_mgr_cls - ), - PhysicsEvent.PHYSICS_READY, - order=10, + self._initialize_handle.callback(prepare_handle.deregister) + initialize_handle = physics_mgr_cls.register_callback( + lambda payload: PhysicsManager.safe_callback_invoke( + _invoke, "_initialize_callback", payload, physics_manager=physics_mgr_cls ), + PhysicsEvent.PHYSICS_READY, + order=10, ) + self._initialize_handle.callback(initialize_handle.deregister) self._invalidate_initialize_handle = physics_mgr_cls.register_callback( lambda payload: PhysicsManager.safe_callback_invoke( _invoke, "_invalidate_initialize_callback", payload, physics_manager=physics_mgr_cls @@ -418,9 +418,9 @@ def _on_prim_deletion(self, event) -> None: def _clear_callbacks(self) -> None: """Clears the callbacks.""" - for handle in self._initialize_handles: - handle.deregister() - self._initialize_handles = () + if self._initialize_handle is not None: + self._initialize_handle.close() + self._initialize_handle = None if self._invalidate_initialize_handle is not None: self._invalidate_initialize_handle.deregister() self._invalidate_initialize_handle = None diff --git a/source/isaaclab/test/sensors/test_opencv_distortion.py b/source/isaaclab/test/sensors/test_opencv_distortion.py index 0c7908b9c795..6d276bc0f451 100644 --- a/source/isaaclab/test/sensors/test_opencv_distortion.py +++ b/source/isaaclab/test/sensors/test_opencv_distortion.py @@ -192,7 +192,7 @@ def _camera_for_prims(prims, width=640, height=480, device="cpu"): fake._ALL_INDICES = wp.array(np.arange(len(prims)), dtype=wp.int32, device=device) fake._ALL_ENV_MASK = wp.ones(len(prims), dtype=wp.bool, device=device) # attributes touched by ``__del__``/``_clear_callbacks`` when the fake object is garbage collected - fake._initialize_handles = () + fake._initialize_handle = None fake._invalidate_initialize_handle = None fake._prim_deletion_handle = None fake._debug_vis_handle = None diff --git a/source/isaaclab_physx/test/sensors/test_contact_sensor.py b/source/isaaclab_physx/test/sensors/test_contact_sensor.py index 9844739c031b..dd7fe402310c 100644 --- a/source/isaaclab_physx/test/sensors/test_contact_sensor.py +++ b/source/isaaclab_physx/test/sensors/test_contact_sensor.py @@ -212,7 +212,7 @@ def _make_graph_test_sensor(device: str = "cuda:0") -> PhysxContactSensor: sensor._device = device sensor._use_graph = True sensor._compute_graph = None - sensor._initialize_handles = () + sensor._initialize_handle = None sensor._invalidate_initialize_handle = None sensor._prim_deletion_handle = None sensor._resolve_indices_and_mask = lambda env_ids, mask: mask diff --git a/source/isaaclab_physx/test/sensors/test_frame_transformer.py b/source/isaaclab_physx/test/sensors/test_frame_transformer.py index c3898ec620a1..2ff57c4c5805 100644 --- a/source/isaaclab_physx/test/sensors/test_frame_transformer.py +++ b/source/isaaclab_physx/test/sensors/test_frame_transformer.py @@ -667,7 +667,7 @@ def _make_frame_transformer(use_recorded_launch: bool = True): sensor._raw_transforms = None sensor._update_cmd = None sensor._use_recorded_launch = use_recorded_launch - sensor._initialize_handles = () + sensor._initialize_handle = None sensor._invalidate_initialize_handle = None sensor._prim_deletion_handle = None diff --git a/source/isaaclab_physx/test/sensors/test_imu_pva_recorded_launch.py b/source/isaaclab_physx/test/sensors/test_imu_pva_recorded_launch.py index fe47301d0b47..25d035486332 100644 --- a/source/isaaclab_physx/test/sensors/test_imu_pva_recorded_launch.py +++ b/source/isaaclab_physx/test/sensors/test_imu_pva_recorded_launch.py @@ -135,7 +135,7 @@ def _make_sensor(sensor_type: str, use_recorded_launch: bool = True): sensor._update_cmd = None sensor._update_env_mask = None sensor._use_recorded_launch = use_recorded_launch - sensor._initialize_handles = () + sensor._initialize_handle = None sensor._invalidate_initialize_handle = None sensor._prim_deletion_handle = None # ``_update_buffers_impl`` re-reads scene gravity so runtime randomization stays visible. diff --git a/source/isaaclab_physx/test/sensors/test_joint_wrench_sensor.py b/source/isaaclab_physx/test/sensors/test_joint_wrench_sensor.py index 0f50f4abf0d2..3ae0a6afdb82 100644 --- a/source/isaaclab_physx/test/sensors/test_joint_wrench_sensor.py +++ b/source/isaaclab_physx/test/sensors/test_joint_wrench_sensor.py @@ -274,7 +274,7 @@ def _make_joint_wrench_sensor(use_recorded_launch: bool = True, num_envs: int = sensor._update_cmd = None sensor._use_recorded_launch = use_recorded_launch sensor._physics_sim_view = None - sensor._initialize_handles = () + sensor._initialize_handle = None sensor._invalidate_initialize_handle = None sensor._prim_deletion_handle = None diff --git a/source/isaaclab_physx/test/sensors/test_ray_caster_graph.py b/source/isaaclab_physx/test/sensors/test_ray_caster_graph.py index 5f17d7b5f511..dfd31fead524 100644 --- a/source/isaaclab_physx/test/sensors/test_ray_caster_graph.py +++ b/source/isaaclab_physx/test/sensors/test_ray_caster_graph.py @@ -78,7 +78,7 @@ def _make_ray_caster(use_graph: bool = True, cache_raw_transforms: bool = True): sensor._transforms_prefetched = False sensor._test_output = wp.zeros(2, dtype=wp.vec3f, device=device) sensor._view = object() - sensor._initialize_handles = () + sensor._initialize_handle = None sensor._invalidate_initialize_handle = None sensor._prim_deletion_handle = None From 50770ec646648609f0d2757872c858bc48e82aa9 Mon Sep 17 00:00:00 2001 From: Matthew Taylor Date: Fri, 16 Oct 2026 13:43:43 -0400 Subject: [PATCH 6/6] Apply RTX exposure overrides only for radiance output --- .../fix-camera-data-physics-dependency.changed.rst | 2 ++ .../isaaclab/isaaclab/renderers/rtx_camera_overrides.py | 4 ++-- .../isaaclab/test/renderers/test_rtx_camera_overrides.py | 2 +- .../isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py | 8 +++----- .../isaaclab_physx/renderers/isaac_rtx_renderer.py | 8 +++----- 5 files changed, 11 insertions(+), 13 deletions(-) create mode 100644 source/isaaclab/changelog.d/fix-camera-data-physics-dependency.changed.rst diff --git a/source/isaaclab/changelog.d/fix-camera-data-physics-dependency.changed.rst b/source/isaaclab/changelog.d/fix-camera-data-physics-dependency.changed.rst new file mode 100644 index 000000000000..3613cdaa40db --- /dev/null +++ b/source/isaaclab/changelog.d/fix-camera-data-physics-dependency.changed.rst @@ -0,0 +1,2 @@ +* Changed RTX camera exposure overrides to apply only when ``rgb_radiance`` is requested. + Setting ``CameraCfg.isp_cfg`` alone now preserves authored exposure. diff --git a/source/isaaclab/isaaclab/renderers/rtx_camera_overrides.py b/source/isaaclab/isaaclab/renderers/rtx_camera_overrides.py index 1a8399203410..2df6c6194df1 100644 --- a/source/isaaclab/isaaclab/renderers/rtx_camera_overrides.py +++ b/source/isaaclab/isaaclab/renderers/rtx_camera_overrides.py @@ -31,7 +31,7 @@ def _apply_rtx_exposure_overrides(stage: Any, spec: CameraRenderSpec) -> None: - """Neutralize RTX exposure for radiance or ISP inputs; preserve standalone HDR exposure. + """Neutralize RTX exposure only when the camera requests radiance. Existing API schemas are preserved. Schema metadata is authored directly so the RTX schema plugin does not need to be loaded yet. @@ -40,7 +40,7 @@ def _apply_rtx_exposure_overrides(stage: Any, spec: CameraRenderSpec) -> None: stage: USD stage containing the camera prims. spec: Camera configuration and resolved camera prim paths. """ - if spec.cfg.isp_cfg is None and "rgb_radiance" not in spec.cfg.data_types: + if "rgb_radiance" not in spec.cfg.data_types: return from pxr import Sdf diff --git a/source/isaaclab/test/renderers/test_rtx_camera_overrides.py b/source/isaaclab/test/renderers/test_rtx_camera_overrides.py index 05b32e526510..4edbfe6b0cc6 100644 --- a/source/isaaclab/test/renderers/test_rtx_camera_overrides.py +++ b/source/isaaclab/test/renderers/test_rtx_camera_overrides.py @@ -16,7 +16,7 @@ @pytest.mark.parametrize( ("data_types", "isp_cfg", "expected_iso"), - [(["rgb_hdr"], None, 800.0), (["rgb_radiance"], None, 0.0), (["rgb"], object(), 0.0)], + [(["rgb_hdr"], None, 800.0), (["rgb_radiance"], None, 0.0), (["rgb"], object(), 800.0)], ) def test_apply_rtx_exposure_overrides_resets_authored_exposure_values(data_types, isp_cfg, expected_iso): """Scene-linear outputs require neutral RTX-side exposure on the camera prim.""" diff --git a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py index 8e15439bcce3..4225aefc2a38 100644 --- a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py +++ b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py @@ -446,11 +446,9 @@ def prepare_cameras(self, stage: Any, spec: CameraRenderSpec) -> None: When ``spec.cfg.isp_cfg`` is set, resolves it (sentinel discovery + normalization) via :func:`isaaclab_ppisp.resolve_and_normalize` so - :mod:`isaaclab` does not need to know about PPISP. Then pins - ``exposure:*`` to neutral and applies ``OmniRtxCameraExposureAPI_1`` so - the RTX exposure model OVRTX embeds does not compound on top of the - ISP. Exposure is also neutralized when ``rgb_radiance`` is requested. Otherwise, the - camera prim's authored exposure is left alone. + :mod:`isaaclab` does not need to know about PPISP. Camera exposure is neutralized + only when ``rgb_radiance`` is requested; otherwise authored exposure is preserved, + including when an ISP is configured. """ if spec.cfg.isp_cfg is not None: try: diff --git a/source/isaaclab_physx/isaaclab_physx/renderers/isaac_rtx_renderer.py b/source/isaaclab_physx/isaaclab_physx/renderers/isaac_rtx_renderer.py index bd2177944782..bcaeef7c080d 100644 --- a/source/isaaclab_physx/isaaclab_physx/renderers/isaac_rtx_renderer.py +++ b/source/isaaclab_physx/isaaclab_physx/renderers/isaac_rtx_renderer.py @@ -221,11 +221,9 @@ def prepare_cameras(self, stage: Any, spec: CameraRenderSpec) -> None: When ``spec.cfg.isp_cfg`` is set, resolves it (sentinel discovery + normalization) via :func:`isaaclab_ppisp.resolve_and_normalize` so - :mod:`isaaclab` does not need to know about PPISP. Then pins - ``exposure:*`` to neutral and applies ``OmniRtxCameraExposureAPI_1`` so - RTX's physical-camera exposure model does not compound on top of the - ISP. Exposure is also neutralized when ``rgb_radiance`` is requested. Otherwise, the - camera prim's authored exposure is left alone. + :mod:`isaaclab` does not need to know about PPISP. Camera exposure is neutralized + only when ``rgb_radiance`` is requested; otherwise authored exposure is preserved, + including when an ISP is configured. :attr:`~isaaclab.sensors.camera.CameraCfg.background_color` is applied per-render-product in :meth:`create_render_data` via USD attributes.