Skip to content
Merged
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
8 changes: 8 additions & 0 deletions source/isaaclab_ov/changelog.d/ovrtx-05-render-var-keys.rst
Original file line number Diff line number Diff line change
@@ -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``.
89 changes: 89 additions & 0 deletions source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_compat.py
Original file line number Diff line number Diff line change
@@ -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."""
83 changes: 54 additions & 29 deletions source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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

Expand Down Expand Up @@ -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"] = {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand All @@ -1398,35 +1418,38 @@ 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)

self._process_id_segmentation_render_var(
render_data,
frame,
output_buffers,
"SemanticSegmentation",
_SEMANTIC_SEGMENTATION_VAR,
"semantic_segmentation",
self.cfg.colorize_semantic_segmentation,
)
Expand All @@ -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,
)
Expand All @@ -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:
Expand Down
33 changes: 28 additions & 5 deletions source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_usd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading