diff --git a/source/isaaclab_ov/changelog.d/ovrtx-05-render-var-keys.rst b/source/isaaclab_ov/changelog.d/ovrtx-05-render-var-keys.rst new file mode 100644 index 000000000000..085f4e9e5785 --- /dev/null +++ b/source/isaaclab_ov/changelog.d/ovrtx-05-render-var-keys.rst @@ -0,0 +1,8 @@ +Added +^^^^^ + +* Added compatibility with the OVRTX 0.5 ``frame.render_vars`` API, which keys render vars by the + authored RenderVar prim path (for example ``/Render/Vars/LdrColor``) instead of the source name. + The key form is resolved from the installed ``ovrtx`` version when + :mod:`isaaclab_ov.renderers.ovrtx_compat` is imported; OVRTX 0.4 keeps source-name keys and the + public extras stay pinned to ``ovrtx==0.4.1.364340``. diff --git a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_compat.py b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_compat.py new file mode 100644 index 000000000000..d46d6c00d1f8 --- /dev/null +++ b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_compat.py @@ -0,0 +1,89 @@ +# 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 + +"""Version compatibility between the public OVRTX 0.4 API and OVRTX 0.5 and later. + +OVRTX 0.4 keys ``frame.render_vars`` by render-var source name (``"LdrColor"``), while +0.5 keys it by the authored RenderVar prim path (``"/Render/Vars/LdrColor"``). The +installed version cannot change while the process runs, so the key form is resolved once +at import and published as :data:`RENDER_VAR_FRAME_KEYS`; per-frame code indexes that +mapping instead of re-checking the version. + +The public extras stay pinned to ``ovrtx==0.4.1.364340``; a missing or unparsable install +keeps the 0.4 key form. +""" + +from __future__ import annotations + +import importlib.metadata +import logging +from collections.abc import Mapping +from types import MappingProxyType + +from packaging.version import InvalidVersion, Version + +from .ovrtx_usd import render_var_prim_paths_by_source + +logger = logging.getLogger(__name__) + +# First OVRTX version that keys ``frame.render_vars`` by RenderVar prim path. +_PRIM_PATH_RENDER_VARS_VERSION = Version("0.5") + + +def detect_ovrtx_version() -> Version | None: + """Return the installed ``ovrtx`` version. + + Read from distribution metadata rather than ``ovrtx.__version__`` so it does not + require importing the runtime. An unparsable version is logged and reported as + missing, which keeps the OVRTX 0.4 behavior. + + Returns: + The installed version, or ``None`` when ``ovrtx`` is absent or its version string + cannot be parsed. + """ + try: + raw = importlib.metadata.version("ovrtx") + except importlib.metadata.PackageNotFoundError: + return None + try: + return Version(raw) + except InvalidVersion: + logger.warning("Could not parse ovrtx version %r; assuming the OVRTX 0.4 render-var API.", raw) + return None + + +def uses_prim_path_render_vars(version: Version | None) -> bool: + """Return whether ``version`` keys ``frame.render_vars`` by RenderVar prim path. + + Args: + version: OVRTX version to classify, or ``None`` when OVRTX is unavailable. + + Returns: + Whether ``version`` is OVRTX 0.5 or newer. + """ + return version is not None and version >= _PRIM_PATH_RENDER_VARS_VERSION + + +def build_render_var_frame_keys(version: Version | None) -> Mapping[str, str]: + """Return the ``frame.render_vars`` key for every render-var source under ``version``. + + Args: + version: OVRTX version the keys are built for, or ``None`` when OVRTX is unavailable. + + Returns: + Read-only mapping of render-var source name to frame key: the source name itself on + OVRTX 0.4, or the authored RenderVar prim path on OVRTX 0.5 and later. + """ + prim_paths = render_var_prim_paths_by_source() + if uses_prim_path_render_vars(version): + return MappingProxyType(dict(prim_paths)) + return MappingProxyType({source: source for source in prim_paths}) + + +OVRTX_VERSION: Version | None = detect_ovrtx_version() +"""Installed OVRTX version, or ``None`` when it is unavailable or unparsable.""" + +RENDER_VAR_FRAME_KEYS: Mapping[str, str] = build_render_var_frame_keys(OVRTX_VERSION) +"""Maps render-var source name to its ``frame.render_vars`` key for the installed OVRTX.""" diff --git a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py index 4dda979b528c..aefca66ebbed 100644 --- a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py +++ b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py @@ -85,6 +85,7 @@ decode_stable_id_map, decode_stable_id_semantic_id_map, ) +from .ovrtx_compat import RENDER_VAR_FRAME_KEYS from .ovrtx_renderer_cfg import OVRTXRendererCfg from .ovrtx_renderer_kernels import ( compute_cable_points_world_kernel, @@ -109,10 +110,26 @@ from isaaclab.renderers.camera_render_spec import CameraRenderSpec -# Maps depth render-var sources to compatible output buffers. +# ``frame.render_vars`` keys of the render vars read below. Baked at import from the installed +# OVRTX version, which decides whether frames are keyed by source name or RenderVar prim path. +_LDR_COLOR_VAR = RENDER_VAR_FRAME_KEYS["LdrColor"] +_HDR_COLOR_VAR = RENDER_VAR_FRAME_KEYS["HdrColor"] +_ALBEDO_VAR = RENDER_VAR_FRAME_KEYS["DiffuseAlbedoSD"] +_NORMALS_VAR = RENDER_VAR_FRAME_KEYS["NormalSD"] +_MOTION_VECTORS_VAR = RENDER_VAR_FRAME_KEYS["TargetMotionSD"] +_SEMANTIC_SEGMENTATION_VAR = RENDER_VAR_FRAME_KEYS["SemanticSegmentation"] +_INSTANCE_SEGMENTATION_VAR = RENDER_VAR_FRAME_KEYS["NonStableInstanceSegmentation"] +_SEMANTIC_ID_MAP_VAR = RENDER_VAR_FRAME_KEYS["SemanticIdMap"] +_STABLE_ID_MAP_VAR = RENDER_VAR_FRAME_KEYS["StableIdMap"] +_STABLE_ID_SEMANTIC_ID_MAP_VAR = RENDER_VAR_FRAME_KEYS["StableIdSemanticIdMap"] + +# Map render vars needed to decode the instance-segmentation info dicts. +_INSTANCE_SEGMENTATION_MAP_VARS = (_STABLE_ID_SEMANTIC_ID_MAP_VAR, _STABLE_ID_MAP_VAR, _SEMANTIC_ID_MAP_VAR) + +# Maps depth render vars to compatible output buffers. _DEPTH_VAR_BUFFER_KEYS: dict[str, tuple[str, ...]] = { - "DistanceToImagePlaneSD": ("depth", "distance_to_image_plane"), - "DistanceToCameraSD": ("distance_to_camera",), + RENDER_VAR_FRAME_KEYS["DistanceToImagePlaneSD"]: ("depth", "distance_to_image_plane"), + RENDER_VAR_FRAME_KEYS["DistanceToCameraSD"]: ("distance_to_camera",), } # The resolved integer value is assigned to the ``omni:rtx:minimal:mode`` attribute of the render product. @@ -1136,7 +1153,7 @@ def _map_render_var_to_dlpack(self, render_var: Any) -> Iterator[wp.array]: ``with`` block -- the mapping is released on exit. Args: - render_var: OVRTX ``RenderVarOutput`` to map (``frame.render_vars[name]``). + render_var: OVRTX ``RenderVarOutput`` to map (looked up from ``frame.render_vars``). Yields: The render var's contents as a Warp array, valid for the duration of the context. @@ -1153,7 +1170,7 @@ def _process_id_segmentation_render_var( render_data: OVRTXRenderData, frame, output_buffers: dict, - render_var_name: str, + render_var_key: str, buffer_key: str, colorize: bool, ) -> None: @@ -1167,14 +1184,15 @@ def _process_id_segmentation_render_var( render_data: OVRTX render data for the current frame. frame: OVRTX frame holding the mapped render vars. output_buffers: Destination warp buffers, keyed by data type. - render_var_name: Name of the OVRTX render var to read. + render_var_key: ``frame.render_vars`` key of the OVRTX render var to read. buffer_key: Data type key into ``output_buffers``. colorize: If True, IDs are mapped to RGBA colors; otherwise raw uint32 IDs are copied. """ - if render_var_name not in frame.render_vars or buffer_key not in output_buffers: + render_var = frame.render_vars.get(render_var_key) + if render_var is None or buffer_key not in output_buffers: return - with self._map_render_var_to_dlpack(frame.render_vars[render_var_name]) as tiled_data: + with self._map_render_var_to_dlpack(render_var) as tiled_data: if tiled_data.dtype != wp.uint32: return @@ -1211,10 +1229,11 @@ def _process_semantic_id_map(self, render_data: OVRTXRenderData, frame) -> None: render_data: OVRTX render data for the current frame. frame: OVRTX frame holding the mapped render vars. """ - if "SemanticIdMap" not in frame.render_vars: + semantic_id_map = frame.render_vars.get(_SEMANTIC_ID_MAP_VAR) + if semantic_id_map is None: return - with frame.render_vars["SemanticIdMap"].map(device=Device.CPU) as mapping: + with semantic_id_map.map(device=Device.CPU) as mapping: labels_by_id = decode_semantic_id_map(np.from_dlpack(mapping)) render_data.renderer_info["semantic_segmentation"] = { @@ -1247,19 +1266,19 @@ def _process_instance_segmentation_maps(self, render_data: OVRTXRenderData, fram render_data: OVRTX render data for the current frame. frame: OVRTX frame holding the mapped render vars. """ - required_vars = ("StableIdSemanticIdMap", "StableIdMap", "SemanticIdMap") - missing = [v for v in required_vars if v not in frame.render_vars] + resolved = {key: frame.render_vars.get(key) for key in _INSTANCE_SEGMENTATION_MAP_VARS} + missing = [key for key, render_var in resolved.items() if render_var is None] if missing: raise RuntimeError( f"instance_segmentation was requested but the following render vars are missing from the " f"OVRTX frame: {missing}. Available vars: {list(frame.render_vars.keys())}" ) - with frame.render_vars["StableIdSemanticIdMap"].map(device=Device.CPU) as mapping: + with resolved[_STABLE_ID_SEMANTIC_ID_MAP_VAR].map(device=Device.CPU) as mapping: stable_id_semantic_id_map = decode_stable_id_semantic_id_map(np.from_dlpack(mapping)) - with frame.render_vars["StableIdMap"].map(device=Device.CPU) as mapping: + with resolved[_STABLE_ID_MAP_VAR].map(device=Device.CPU) as mapping: stable_id_to_path = decode_stable_id_map(np.from_dlpack(mapping)) - with frame.render_vars["SemanticIdMap"].map(device=Device.CPU) as mapping: + with resolved[_SEMANTIC_ID_MAP_VAR].map(device=Device.CPU) as mapping: semantic_id_to_labels = decode_semantic_id_map(np.from_dlpack(mapping)) id_to_labels, id_to_semantics = build_instance_id_to_labels_and_semantics( @@ -1384,7 +1403,8 @@ def _process_render_frame(self, render_data: OVRTXRenderData, frame, output_buff # is available, so without this a missing SemanticIdMap on a later frame would leave a stale mapping. render_data.renderer_info.clear() - if "LdrColor" in frame.render_vars: + ldr_color = frame.render_vars.get(_LDR_COLOR_VAR) + if ldr_color is not None: buffer_key = None if render_data.ppisp_pipeline is None and "rgba" in output_buffers: @@ -1398,27 +1418,30 @@ def _process_render_frame(self, render_data: OVRTXRenderData, frame, output_buff break if buffer_key is not None: - with self._map_render_var_to_dlpack(frame.render_vars["LdrColor"]) as tiled_data: + with self._map_render_var_to_dlpack(ldr_color) as tiled_data: self._extract_rgba_tiles(render_data, tiled_data, output_buffers, buffer_key) for depth_var, buffer_keys in _DEPTH_VAR_BUFFER_KEYS.items(): - if depth_var not in frame.render_vars: + depth_render_var = frame.render_vars.get(depth_var) + if depth_render_var is None: continue if not any(buffer_key in output_buffers for buffer_key in buffer_keys): continue - with self._map_render_var_to_dlpack(frame.render_vars[depth_var]) as tiled_depth_data: + with self._map_render_var_to_dlpack(depth_render_var) as tiled_depth_data: if tiled_depth_data.dtype == wp.uint32: tiled_depth_data = wp.from_torch( wp.to_torch(tiled_depth_data).view(torch.float32), dtype=wp.float32 ) self._extract_depth_tiles(render_data, tiled_depth_data, output_buffers, buffer_keys) - if "DiffuseAlbedoSD" in frame.render_vars and "albedo" in output_buffers: - with self._map_render_var_to_dlpack(frame.render_vars["DiffuseAlbedoSD"]) as tiled_albedo_data: + albedo_var = frame.render_vars.get(_ALBEDO_VAR) + if albedo_var is not None and "albedo" in output_buffers: + with self._map_render_var_to_dlpack(albedo_var) as tiled_albedo_data: self._extract_rgba_tiles(render_data, tiled_albedo_data, output_buffers, "albedo", suffix="albedo") - if "HdrColor" in frame.render_vars and "rgb_hdr" in output_buffers: - with self._map_render_var_to_dlpack(frame.render_vars["HdrColor"]) as tiled_hdr_data: + hdr_color = frame.render_vars.get(_HDR_COLOR_VAR) + if hdr_color is not None and "rgb_hdr" in output_buffers: + with self._map_render_var_to_dlpack(hdr_color) as tiled_hdr_data: tiled_hdr_data = self._prepare_ppisp_hdr_source(render_data, tiled_hdr_data, output_buffers) self._extract_hdr_color_tiles(render_data, tiled_hdr_data, output_buffers) @@ -1426,7 +1449,7 @@ def _process_render_frame(self, render_data: OVRTXRenderData, frame, output_buff render_data, frame, output_buffers, - "SemanticSegmentation", + _SEMANTIC_SEGMENTATION_VAR, "semantic_segmentation", self.cfg.colorize_semantic_segmentation, ) @@ -1438,7 +1461,7 @@ def _process_render_frame(self, render_data: OVRTXRenderData, frame, output_buff render_data, frame, output_buffers, - "NonStableInstanceSegmentation", + _INSTANCE_SEGMENTATION_VAR, "instance_segmentation", self.cfg.colorize_instance_segmentation, ) @@ -1447,15 +1470,17 @@ def _process_render_frame(self, render_data: OVRTXRenderData, frame, output_buff if "instance_segmentation" in output_buffers: self._process_instance_segmentation_maps(render_data, frame) - if "NormalSD" in frame.render_vars and "normals" in output_buffers: - with self._map_render_var_to_dlpack(frame.render_vars["NormalSD"]) as tiled_normals_data: + normals_var = frame.render_vars.get(_NORMALS_VAR) + if normals_var is not None and "normals" in output_buffers: + with self._map_render_var_to_dlpack(normals_var) as tiled_normals_data: self._launch_extract_all_tiles(render_data, tiled_normals_data, output_buffers["normals"]) # For motion vectors, extract only the first two (u, v) channels from the tiled buffer. # Note: mirrors the Isaac RTX renderer's handling of the "TargetMotionSD" AOV # (check: https://github.com/isaac-sim/IsaacLab/issues/2003). - if "TargetMotionSD" in frame.render_vars and "motion_vectors" in output_buffers: - with self._map_render_var_to_dlpack(frame.render_vars["TargetMotionSD"]) as tiled_motion_vectors_data: + motion_var = frame.render_vars.get(_MOTION_VECTORS_VAR) + if motion_var is not None and "motion_vectors" in output_buffers: + with self._map_render_var_to_dlpack(motion_var) as tiled_motion_vectors_data: self._launch_extract_all_tiles(render_data, tiled_motion_vectors_data, output_buffers["motion_vectors"]) def _render_legacy(self, render_data: OVRTXRenderData) -> None: diff --git a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_usd.py b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_usd.py index b4a84d997eac..ef10c0144ea1 100644 --- a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_usd.py +++ b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_usd.py @@ -9,14 +9,16 @@ import logging import math +from collections.abc import Mapping +from types import MappingProxyType from pxr import Sdf, Usd, UsdGeom logger = logging.getLogger(__name__) -# Maps camera data types to render-var configs. OVRTX frame vars are keyed by source name, -# so shared sources use one config. +# Maps camera data types to (prim path, prim name, source name). Shared sources use one config. +# OVRTX 0.4 keys ``frame.render_vars`` by source name; 0.5+ keys them by the RenderVar prim path. _RENDER_VAR_BY_DATA_TYPE: dict[str, tuple[str, str, str]] = { "rgb": ("/Render/Vars/LdrColor", "LdrColor", "LdrColor"), "rgba": ("/Render/Vars/LdrColor", "LdrColor", "LdrColor"), @@ -53,6 +55,17 @@ _DEFAULT_RENDER_VAR = _RENDER_VAR_BY_DATA_TYPE["rgb"] +# Segmentation ID-map vars are authored alongside the pixel AOVs, not as camera data types. +_SEGMENTATION_MAP_RENDER_VARS: tuple[tuple[str, str, str], ...] = ( + ("/Render/Vars/StableIdSemanticIdMap", "StableIdSemanticIdMap", "StableIdSemanticIdMap"), + ("/Render/Vars/StableIdMap", "StableIdMap", "StableIdMap"), + ("/Render/Vars/SemanticIdMap", "SemanticIdMap", "SemanticIdMap"), +) + +_RENDER_VAR_PRIM_PATH_BY_SOURCE: Mapping[str, str] = MappingProxyType( + {source: path for path, _, source in (*_RENDER_VAR_BY_DATA_TYPE.values(), *_SEGMENTATION_MAP_RENDER_VARS)} +) + def _validate_data_type_combination(data_types: list[str]) -> None: """Reject data type combinations that a single OVRTX render product cannot serve. @@ -131,15 +144,25 @@ def get_render_var_configs(data_types: list[str]) -> list[tuple[str, str, str]]: # Author the ID-to-label map render vars needed to decode the segmentation info dicts. # instance_segmentation needs StableIdSemanticIdMap + StableIdMap to resolve each pixel to a prim path. if "instance_segmentation" in data_types: - render_vars.append(("/Render/Vars/StableIdSemanticIdMap", "StableIdSemanticIdMap", "StableIdSemanticIdMap")) - render_vars.append(("/Render/Vars/StableIdMap", "StableIdMap", "StableIdMap")) + render_vars.append(_SEGMENTATION_MAP_RENDER_VARS[0]) + render_vars.append(_SEGMENTATION_MAP_RENDER_VARS[1]) # SemanticIdMap resolves the semantic-ID-to-label mapping and is shared by both semantic_segmentation and # instance_segmentation, so it is authored once when either output is requested. if "semantic_segmentation" in data_types or "instance_segmentation" in data_types: - render_vars.append(("/Render/Vars/SemanticIdMap", "SemanticIdMap", "SemanticIdMap")) + render_vars.append(_SEGMENTATION_MAP_RENDER_VARS[2]) return render_vars +def render_var_prim_paths_by_source() -> Mapping[str, str]: + """Return the authored RenderVar prim path of every OVRTX render-var source. + + Returns: + Read-only mapping of render-var source name to the absolute path of the ``RenderVar`` + prim this module authors for it. + """ + return _RENDER_VAR_PRIM_PATH_BY_SOURCE + + def build_render_scope_usd( camera_paths: list[str], render_product_name: str, diff --git a/source/isaaclab_ov/test/test_ovrtx_compat.py b/source/isaaclab_ov/test/test_ovrtx_compat.py new file mode 100644 index 000000000000..8a9bd1139558 --- /dev/null +++ b/source/isaaclab_ov/test/test_ovrtx_compat.py @@ -0,0 +1,108 @@ +# 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 + +"""Tests for OVRTX 0.4 / 0.5 render-var key compatibility.""" + +from __future__ import annotations + +import importlib.metadata +import importlib.util + +import pytest +from packaging.version import Version + +_REQUIRED_MODULES = ("isaaclab_ov", "pxr") +_MISSING_MODULES = [module for module in _REQUIRED_MODULES if importlib.util.find_spec(module) is None] + +pytestmark = [ + pytest.mark.isaacsim_ci, + pytest.mark.skipif( + bool(_MISSING_MODULES), + reason=f"requires optional modules: {', '.join(_MISSING_MODULES)}", + ), +] + +if not _MISSING_MODULES: + from isaaclab_ov.renderers.ovrtx_compat import ( # noqa: E402 + RENDER_VAR_FRAME_KEYS, + build_render_var_frame_keys, + detect_ovrtx_version, + uses_prim_path_render_vars, + ) + from isaaclab_ov.renderers.ovrtx_usd import render_var_prim_paths_by_source # noqa: E402 +else: + RENDER_VAR_FRAME_KEYS = None + build_render_var_frame_keys = None + detect_ovrtx_version = None + render_var_prim_paths_by_source = None + uses_prim_path_render_vars = None + + +def test_detect_ovrtx_version_reads_distribution_metadata(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(importlib.metadata, "version", lambda name: "0.4.1.364340") + assert detect_ovrtx_version() == Version("0.4.1.364340") + + +def test_detect_ovrtx_version_returns_none_when_uninstalled(monkeypatch: pytest.MonkeyPatch): + def _missing(name: str) -> str: + raise importlib.metadata.PackageNotFoundError(name) + + monkeypatch.setattr(importlib.metadata, "version", _missing) + assert detect_ovrtx_version() is None + + +def test_detect_ovrtx_version_returns_none_for_unparseable_version(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(importlib.metadata, "version", lambda name: "internal-build") + assert detect_ovrtx_version() is None + + +@pytest.mark.parametrize( + ("version", "expected"), + [ + (None, False), + (Version("0.4.1.364340"), False), + (Version("0.4.2"), False), + (Version("0.5"), True), + (Version("0.5.0.12345"), True), + (Version("1.0"), True), + ], +) +def test_uses_prim_path_render_vars_switches_at_ovrtx_05(version: Version | None, expected: bool): + assert uses_prim_path_render_vars(version) is expected + + +@pytest.mark.parametrize("version", [None, Version("0.4.1.364340")]) +def test_ovrtx_04_frame_keys_are_source_names(version: Version | None): + keys = build_render_var_frame_keys(version) + assert keys["LdrColor"] == "LdrColor" + assert keys["DiffuseAlbedoSD"] == "DiffuseAlbedoSD" + assert keys["SemanticIdMap"] == "SemanticIdMap" + + +def test_ovrtx_05_frame_keys_are_render_var_prim_paths(): + keys = build_render_var_frame_keys(Version("0.5")) + # The prim path is not always the source name with a prefix, so it is read from the authored paths. + assert keys["LdrColor"] == "/Render/Vars/LdrColor" + assert keys["DiffuseAlbedoSD"] == "/Render/Vars/albedo" + assert keys["DistanceToImagePlaneSD"] == "/Render/Vars/depth" + assert keys["SemanticSegmentation"] == "/Render/Vars/semantic" + assert keys["SemanticIdMap"] == "/Render/Vars/SemanticIdMap" + + +def test_frame_keys_cover_every_authored_render_var(): + """Every source the renderer can read has a key, so lookups cannot silently miss an AOV.""" + sources = set(render_var_prim_paths_by_source()) + assert set(build_render_var_frame_keys(Version("0.5"))) == sources + assert set(build_render_var_frame_keys(None)) == sources + + +def test_installed_frame_keys_match_the_installed_version(): + """The published mapping is baked from the installed OVRTX version at import.""" + assert dict(RENDER_VAR_FRAME_KEYS) == dict(build_render_var_frame_keys(detect_ovrtx_version())) + + +def test_published_frame_keys_are_read_only(): + with pytest.raises(TypeError): + RENDER_VAR_FRAME_KEYS["LdrColor"] = "mutated" # type: ignore[index] diff --git a/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py b/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py index f451ef084d55..487ba5d76461 100644 --- a/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py +++ b/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py @@ -31,6 +31,7 @@ if not _MISSING_MODULES: from isaaclab_ov.renderers import OVRTXRendererCfg # noqa: E402 from isaaclab_ov.renderers import ovrtx_renderer as ovrtx_renderer_module # noqa: E402 + from isaaclab_ov.renderers.ovrtx_compat import RENDER_VAR_FRAME_KEYS # noqa: E402 from isaaclab_ov.renderers.ovrtx_renderer import ( # noqa: E402 _DISABLE_LINUX_CUDA_CPU_SYNC_ENV, OVRTXRenderData, @@ -46,6 +47,7 @@ ovrtx_use_ovstage_enabled = None _DISABLE_LINUX_CUDA_CPU_SYNC_ENV = None _gpu_side_render_var_sync_enabled = None + RENDER_VAR_FRAME_KEYS = None _SPAWN = PinholeCameraCfg( focal_length=24.0, @@ -208,7 +210,7 @@ def map(self, *args, **kwargs): raise AssertionError("PPISP RGBA output must not read OVRTX LdrColor") class Frame: - render_vars = {"LdrColor": FailingRenderVar()} + render_vars = {RENDER_VAR_FRAME_KEYS["LdrColor"]: FailingRenderVar()} renderer = _make_ovrtx_renderer_without_backend() render_data = _make_ovrtx_render_data() @@ -217,6 +219,29 @@ class Frame: renderer._process_render_frame(render_data, Frame(), {"rgba": object()}) +@pytest.mark.parametrize("stale_key", ["LdrColor", "/Render/Vars/LdrColor"]) +def test_ovrtx_process_frame_reads_only_the_installed_ldr_color_key(monkeypatch: pytest.MonkeyPatch, stale_key: str): + """Frames are keyed by source name on OVRTX 0.4 and by prim path on 0.5; only one form is read.""" + installed_key = RENDER_VAR_FRAME_KEYS["LdrColor"] + + mapped = [] + + @contextlib.contextmanager + def fake_map(self, render_var): + mapped.append(render_var) + yield object() + + monkeypatch.setattr(OVRTXRenderer, "_map_render_var_to_dlpack", fake_map) + monkeypatch.setattr(OVRTXRenderer, "_extract_rgba_tiles", lambda *args, **kwargs: None) + + class Frame: + render_vars = {stale_key: "stale", installed_key: "installed"} + + renderer = _make_ovrtx_renderer_without_backend() + renderer._process_render_frame(_make_ovrtx_render_data(), Frame(), {"rgba": object()}) + assert mapped == ["installed"] + + def test_ovrtx_ppisp_hdr_source_is_cloned_to_output_device(monkeypatch): """PPISP HdrColor source is moved to the HDR output buffer device.""" diff --git a/source/isaaclab_ov/test/test_ovrtx_usd.py b/source/isaaclab_ov/test/test_ovrtx_usd.py index a8ce07384e61..1dc05838c820 100644 --- a/source/isaaclab_ov/test/test_ovrtx_usd.py +++ b/source/isaaclab_ov/test/test_ovrtx_usd.py @@ -29,6 +29,7 @@ export_stage_to_string, get_render_var_config, get_render_var_configs, + render_var_prim_paths_by_source, ) from pxr import Sdf, Usd, UsdGeom # noqa: E402 @@ -42,6 +43,7 @@ export_stage_to_string = None get_render_var_config = None get_render_var_configs = None + render_var_prim_paths_by_source = None def _make_multi_env_stage(num_envs: int) -> Usd.Stage: @@ -114,6 +116,18 @@ def test_ovrtx_rgb_hdr_uses_hdr_color_render_var(): assert get_render_var_config(["rgb_hdr"]) == ("/Render/Vars/HdrColor", "HdrColor", "HdrColor") +def test_render_var_prim_paths_cover_every_authored_render_var(): + """Every render-var config this module authors is reachable by its source name.""" + prim_paths = render_var_prim_paths_by_source() + + authored = get_render_var_configs( + ["rgb", "rgb_hdr", "albedo", "depth", "distance_to_camera", "normals", "motion_vectors"] + ) + get_render_var_configs(["semantic_segmentation", "instance_segmentation"]) + + for path, _, source in authored: + assert prim_paths[source] == path + + def test_ovrtx_instance_segmentation_uses_non_stable_instance_segmentation_render_var(): """Requesting instance_segmentation from OVRTX selects the NonStableInstanceSegmentation render var.""" assert get_render_var_config(["instance_segmentation"]) == (