# 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
"""OVRTX Renderer implementation.
How it fits together
--------------------
- **ovrtx_renderer.py** (this file): Orchestrates the pipeline. Owns the OVRTX Renderer,
USD loading/cloning, camera and object bindings, and output buffers. Each frame it:
updates camera/object transforms (using kernels), steps the renderer, then extracts
tiles from the tiled framebuffer (kernels).
- **ovrtx_renderer_kernels.py**: Warp GPU kernels for OVRTX rendering pipeline.
- **ovrtx_usd.py**: USD helpers for OVRTX: render var config, camera injection, etc.
"""
from __future__ import annotations
import contextlib
import logging
import math
import os
import re
import sys
import weakref
from collections.abc import Iterator, Sequence
from pathlib import Path
from typing import TYPE_CHECKING, Any, NoReturn, cast
logger = logging.getLogger(__name__)
import numpy as np
import ovstage
import torch
import warp as wp
import isaaclab.utils.warp # noqa: F401 # initializes Warp runtime
# The ovrtx C library links to its own version of the USD libraries. Having
# the pxr Python package available can cause the C library to load an
# incompatible version of libusd, potentially leading to undefined behavior.
# By setting OVRTX_SKIP_USD_CHECK, we prevent the C library from loading the pxr Python package.
os.environ["OVRTX_SKIP_USD_CHECK"] = "1"
try:
from ovrtx import (
BindingFlag,
DataAccess,
Device,
PrimMode,
Renderer,
RendererConfig,
Semantic,
TextureStreamingMode,
)
except ModuleNotFoundError as exc:
if exc.name != "ovrtx":
raise
raise ModuleNotFoundError(
"The OVRTX renderer requires the optional 'ovrtx' runtime wheel, which is not installed. "
"Run your command with: uv run --extra ovrtx <command> "
"(or, manually: python -m pip install 'ovrtx==0.4.1.364340')."
) from exc
from isaaclab.cloner import ClonePlan
from isaaclab.cloner import query as clone_query
from isaaclab.renderers import BaseRenderer, RenderBufferKind, RenderBufferSpec
from isaaclab.sim import SimulationContext
from isaaclab.utils.warp.warp_math import convert_camera_frame_orientation_convention_wp
from isaaclab_ov.stage import (
create_ovstage,
points_tensor_from_warp,
xform_tensor_from_numpy,
xform_tensor_from_warp,
)
from .ovrtx_annotator_utils import (
build_instance_id_to_labels_and_semantics,
build_semantic_id_to_labels,
decode_semantic_id_map,
decode_stable_id_map,
decode_stable_id_semantic_id_map,
)
from .ovrtx_renderer_cfg import OVRTXRendererCfg
from .ovrtx_renderer_kernels import (
compute_cable_points_world_kernel,
create_camera_transforms_kernel,
extract_all_tiles_kernel,
generate_random_colors_from_ids_kernel,
sync_newton_transforms_kernel,
)
from .ovrtx_shader_cache import redirect_shader_cache
from .ovrtx_usd import (
build_render_product_as_string,
create_scene_partition_attributes,
export_stage_to_string,
)
from .visual_materials import OVRTXVisualMaterialWriter
if TYPE_CHECKING:
from isaaclab_ppisp import PpispPipeline
from isaaclab.renderers.base_renderer import VisualMaterialBatch
from isaaclab.sensors.camera.camera_data import CameraData
from isaaclab.utils.warp import ProxyArray
from isaaclab.renderers.camera_render_spec import CameraRenderSpec
# Maps depth render-var sources to compatible output buffers.
_DEPTH_VAR_BUFFER_KEYS: dict[str, tuple[str, ...]] = {
"DistanceToImagePlaneSD": ("depth", "distance_to_image_plane"),
"DistanceToCameraSD": ("distance_to_camera",),
}
# The resolved integer value is assigned to the ``omni:rtx:minimal:mode`` attribute of the render product.
_RTX_MINIMAL_MODES = {
RenderBufferKind.SIMPLE_SHADING_CONSTANT_DIFFUSE.value: 1,
RenderBufferKind.SIMPLE_SHADING_DIFFUSE_MDL.value: 2,
RenderBufferKind.SIMPLE_SHADING_FULL_MDL.value: 3,
}
_PPISP_IMPORT_ERROR_MESSAGE = (
"isaaclab_ppisp is required when CameraCfg.isp_cfg is set. "
"It ships with the Isaac Lab wheel (`pip install isaaclab`); otherwise install the "
"isaaclab-ppisp extension from the Isaac Lab source checkout."
)
_READ_GPU_TRANSFORMS_ENV = "ISAAC_LAB_OVRTX_READ_GPU_TRANSFORMS"
# Runtime environment variable used to enable the ovstage code path for ovrtx.
_USE_OVSTAGE_ENV = "ISAAC_LAB_OVRTX_USE_OVSTAGE"
# Opts Linux out of the host wait, onto the same GPU-side ordering every other platform uses.
# See :meth:`OVRTXRenderer._map_render_var_to_dlpack`.
_DISABLE_LINUX_CUDA_CPU_SYNC_ENV = "ISAAC_LAB_OVRTX_DISABLE_LINUX_CUDA_CPU_SYNC"
def ovrtx_use_ovstage_enabled() -> bool:
"""Return whether the ovstage scene-ownership path should be used.
Enabled by ``ISAAC_LAB_OVRTX_USE_OVSTAGE=1``. Defaults to ``0`` so existing deployments are
unaffected until ovstage is explicitly opted into.
Raises:
ValueError: If the environment variable is set to anything other than ``0`` or ``1``.
"""
value = os.environ.get(_USE_OVSTAGE_ENV, "0").strip()
if value not in {"0", "1"}:
raise ValueError(f"Invalid value for environment variable `{_USE_OVSTAGE_ENV}`: {value}. Expected 0 or 1.")
return value == "1"
def _raise_missing_ppisp_error(exc: ModuleNotFoundError) -> NoReturn:
# Only translate missing isaaclab_ppisp imports into the optional-dependency hint;
# unrelated missing modules should surface unchanged for easier debugging.
if exc.name != "isaaclab_ppisp" and not (exc.name and exc.name.startswith("isaaclab_ppisp.")):
raise exc
raise ModuleNotFoundError(_PPISP_IMPORT_ERROR_MESSAGE, name="isaaclab_ppisp") from exc
def _read_gpu_transforms_enabled() -> bool:
"""Return whether OVRTX should read GPU transforms from its internal transform cache."""
value = os.environ.get(_READ_GPU_TRANSFORMS_ENV, "1").strip()
if value not in {"0", "1"}:
raise ValueError(
f"Invalid value for environment variable `{_READ_GPU_TRANSFORMS_ENV}`: {value}. Expected 0 or 1."
)
return value == "1"
def _gpu_side_render_var_sync_enabled() -> bool:
"""Return whether a render-var mapping is ordered by a GPU-side wait rather than a host wait.
See :meth:`OVRTXRenderer._map_render_var_to_dlpack` for why Linux is the exception, and
:data:`_DISABLE_LINUX_CUDA_CPU_SYNC_ENV` for opting out of it.
Raises:
ValueError: If the environment variable is set to anything other than ``0`` or ``1``.
"""
if not sys.platform.startswith("linux"):
return True
value = os.environ.get(_DISABLE_LINUX_CUDA_CPU_SYNC_ENV, "0").strip()
if value not in {"0", "1"}:
raise ValueError(
f"Invalid value for environment variable `{_DISABLE_LINUX_CUDA_CPU_SYNC_ENV}`: {value}. Expected 0 or 1."
)
return value == "1"
def _resolve_rtx_minimal_mode(data_types: list[str]) -> int | None:
"""Resolve the RTX minimal mode from data types.
RTX minimal mode is used to control the rendering quality. The higher the mode, the higher the quality.
If multiple simple shading data types are requested, the first one in the list is used and a warning is logged.
If no simple shading data types are requested, None is returned.
Args:
data_types: List of data types.
Returns:
The resolved RTX minimal mode if simple shading data types are requested, otherwise None.
"""
filtered_data_types = [data_type for data_type in data_types if data_type in _RTX_MINIMAL_MODES]
if not filtered_data_types:
return None
if len(filtered_data_types) > 1:
logger.warning(
"Multiple simple shading data types requested (%s). Using the first in the list (%s).",
filtered_data_types,
filtered_data_types[0],
)
return _RTX_MINIMAL_MODES[filtered_data_types[0]]
def _write_file(output_dir: Path, file_name: str, content: str) -> None:
"""Write ``content`` to ``output_dir / file_name``.
Creates ``output_dir`` and any missing parents when needed.
Args:
output_dir: Directory that receives the file.
file_name: Base name of the file to write.
content: Text content written with UTF-8 encoding.
"""
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / file_name
with open(output_path, "w", encoding="utf-8") as file:
file.write(content)
logger.info("Wrote USD file: %s", output_path)
class OVRTXRenderData:
"""OVRTX-specific RenderData. Holds warp output buffers sized from :class:`CameraRenderSpec`."""
def __init__(self, spec: CameraRenderSpec, device):
"""Create render data from a camera render specification."""
self.width = spec.cfg.width
self.height = spec.cfg.height
self.num_envs = spec.num_instances
self.data_types = spec.cfg.data_types if spec.cfg.data_types else ["rgb"]
self.num_cols = math.ceil(math.sqrt(self.num_envs))
self.num_rows = math.ceil(self.num_envs / self.num_cols)
self.warp_buffers: dict[str, wp.array] = {}
# Per-output metadata collected during render() and copied into CameraData.info by read_output().
# Populated for "semantic_segmentation" (with an "idToLabels" mapping) and
# "instance_segmentation" (with "idToLabels" and "idToSemantics" mappings).
self.renderer_info: dict[str, Any] = {}
# Post-render PPISP pipeline composed when ``spec.cfg.isp_cfg`` is set.
# ``isp_cfg`` is already fully normalized by ``prepare_cameras`` by the time it reaches here.
self.ppisp_pipeline: PpispPipeline | None = None
if spec.cfg.isp_cfg is not None:
try:
from isaaclab_ppisp import PpispPipeline
except ModuleNotFoundError as exc:
_raise_missing_ppisp_error(exc)
self.ppisp_pipeline = PpispPipeline(spec.cfg.isp_cfg)
[docs]
class OVRTXRenderer(BaseRenderer):
"""OVRTX Renderer implementation using the ovrtx library.
This renderer uses the ovrtx library for high-fidelity RTX-based rendering,
providing ray-traced rendering capabilities for Isaac Lab environments.
"""
cfg: OVRTXRendererCfg
def supported_output_types(self) -> dict[RenderBufferKind, RenderBufferSpec]:
"""Publish the per-output layout this OVRTX backend writes.
See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.supported_output_types`."""
instance_seg_spec = (
RenderBufferSpec(4, wp.uint8) if self.cfg.colorize_instance_segmentation else RenderBufferSpec(1, wp.int32)
)
semantic_seg_spec = (
RenderBufferSpec(4, wp.uint8) if self.cfg.colorize_semantic_segmentation else RenderBufferSpec(1, wp.int32)
)
return {
RenderBufferKind.RGBA: RenderBufferSpec(4, wp.uint8),
RenderBufferKind.RGB: RenderBufferSpec(3, wp.uint8),
RenderBufferKind.RGB_HDR: RenderBufferSpec(3, wp.float32),
RenderBufferKind.ALBEDO: RenderBufferSpec(4, wp.uint8),
RenderBufferKind.SIMPLE_SHADING_CONSTANT_DIFFUSE: RenderBufferSpec(3, wp.uint8),
RenderBufferKind.SIMPLE_SHADING_DIFFUSE_MDL: RenderBufferSpec(3, wp.uint8),
RenderBufferKind.SIMPLE_SHADING_FULL_MDL: RenderBufferSpec(3, wp.uint8),
RenderBufferKind.SEMANTIC_SEGMENTATION: semantic_seg_spec,
RenderBufferKind.INSTANCE_SEGMENTATION: instance_seg_spec,
RenderBufferKind.DEPTH: RenderBufferSpec(1, wp.float32),
RenderBufferKind.DISTANCE_TO_IMAGE_PLANE: RenderBufferSpec(1, wp.float32),
RenderBufferKind.DISTANCE_TO_CAMERA: RenderBufferSpec(1, wp.float32),
RenderBufferKind.NORMALS: RenderBufferSpec(3, wp.float32),
RenderBufferKind.MOTION_VECTORS: RenderBufferSpec(2, wp.float32),
}
[docs]
def __init__(self, cfg: OVRTXRendererCfg):
self.cfg = cfg
self._device = "cuda:0" # default; overridden by create_render_data(spec)
# Resolved by create_render_data(spec); every render-product device id and CUDA sync stream
# derives from this one cached device so a bare "cuda" cannot be re-interpreted per call site.
self._warp_device: wp.Device | None = None
self._render_product_paths = []
self._object_newton_indices: wp.array | None = None
self._object_scales: wp.array | None = None
self._object_scales_by_path: dict[str, tuple[float, float, float]] = {}
self._deformable_particle_offsets: list[int] = []
self._deformable_particle_counts: list[int] = []
self._particle_visual_offsets: list[int] = []
self._particle_visual_counts: list[int] = []
self._cable_segment_counts: list[int] = []
self._cable_max_points: int = 0
self._cable_shape_ids: wp.array | None = None
self._cable_offsets: wp.array | None = None
self._cable_counts: wp.array | None = None
self._cable_points: wp.array | None = None
self._initialized_scene = False
self._exported_usd_string: str | None = None
self._camera_rel_path: str | None = None
self._output_id_color_buffers: dict[str, wp.array] = {}
self._clone_plan: ClonePlan | None = None
self._visual_material_writer_ref: weakref.ReferenceType[OVRTXVisualMaterialWriter] | None = None
self._use_ovstage = ovrtx_use_ovstage_enabled()
self._init_fields()
logger.info("Creating OVRTX renderer...")
config = RendererConfig(
log_file_path=self.cfg.log_file_path,
log_level=self.cfg.log_level,
read_gpu_transforms=_read_gpu_transforms_enabled(),
keep_system_alive=True,
suppress_deprecation_warnings=True,
texture_streaming_mode=TextureStreamingMode.SYNCHRONOUS,
)
# Takes the config because the redirect can be what first loads the ovrtx
# library, and initialization only happens once, so it has to see the
# same config the renderer below is built with.
redirect_shader_cache(config)
self._renderer = Renderer(config)
if not self._renderer:
raise RuntimeError(
"Failed to create OVRTX Renderer; the underlying ovrtx.Renderer constructor returned a falsy"
" value. Check that ovrtx is installed correctly and its native dependencies are available."
)
logger.info("OVRTX renderer created successfully")
def _create_visual_material_writer(self, batches: tuple[VisualMaterialBatch, ...]) -> OVRTXVisualMaterialWriter:
if not self._initialized_scene:
raise RuntimeError("OVRTX must ingest its detached scene before material writes are compiled.")
writer = OVRTXVisualMaterialWriter(self, batches)
self._visual_material_writer_ref = weakref.ref(writer)
return writer
@property
def visual_material_writer(self):
"""Return the detached-scene material-writer factory."""
return self._create_visual_material_writer
def prepare_cameras(self, stage: Any, spec: CameraRenderSpec) -> None:
if spec.cfg.isp_cfg is None:
return
try:
from isaaclab_ppisp import apply_rtx_exposure_overrides, resolve_and_normalize
except ModuleNotFoundError as exc:
_raise_missing_ppisp_error(exc)
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)
if spec.cfg.isp_cfg is None or not spec.camera_prim_paths:
return
apply_rtx_exposure_overrides(stage, list(spec.camera_prim_paths))
def prepare_stage(self, stage: Any, num_envs: int) -> None:
if stage is None:
return
self._clone_plan = SimulationContext.instance().get_clone_plan()
if self._clone_plan is None or self._clone_plan.env_ids is None or self._clone_plan.positions is None:
raise RuntimeError("Clone plan with environment ids and positions is required when preparing OVRTX stage")
expected_ids = torch.arange(num_envs, device=self._clone_plan.env_ids.device)
if not torch.equal(self._clone_plan.env_ids, expected_ids):
raise RuntimeError("OVRTX requires ClonePlan environment ids ordered from zero.")
if self.cfg.temp_usd_dir is not None:
_write_file(Path(self.cfg.temp_usd_dir), "pre_ovrtx_renderer_stage.usda", stage.ExportToString())
logger.info("Preparing stage (%d envs)...", num_envs)
create_scene_partition_attributes(stage, num_envs)
self._capture_object_scales(stage, self._clone_plan)
self._exported_usd_string = export_stage_to_string(
stage,
num_envs,
source_paths=self._clone_plan.sources,
keep_env_roots=not self._use_ovstage,
)
def _capture_object_scales(self, stage: Any, plan: ClonePlan) -> None:
self._object_scales_by_path.clear()
from pxr import Gf, Usd, UsdGeom
envs_prim = stage.GetPrimAtPath("/World/envs")
if not envs_prim.IsValid():
return
xform_cache = UsdGeom.XformCache()
for prim in Usd.PrimRange(envs_prim):
if not prim.IsA(UsdGeom.Xformable):
continue
scale = Gf.Transform(xform_cache.GetLocalToWorldTransform(prim)).GetScale()
scale = (float(scale[0]), float(scale[1]), float(scale[2]))
if not all(math.isclose(axis, 1.0, rel_tol=1e-6, abs_tol=1e-6) for axis in scale):
self._object_scales_by_path[str(prim.GetPath())] = scale
# OVRTX creates non-source rows after this stage is exported, so those destination prims
# cannot be traversed above. Clone queries retain the plan's nearest-owner semantics.
for source_path, scale in tuple(self._object_scales_by_path.items()):
for env_id in clone_query.path_env_ids(plan, source_path):
clone_path = clone_query.path_to_clone(plan, source_path, env_id)
assert clone_path is not None
self._object_scales_by_path.setdefault(clone_path, scale)
def _create_object_scale_array(self, object_paths: list[str]) -> wp.array:
scales = [self._object_scales_by_path.get(path, (1.0, 1.0, 1.0)) for path in object_paths]
return wp.array(scales, dtype=wp.vec3f, device=self._device)
def _init_fields_legacy(self) -> None:
"""Initialize the legacy-path instance fields.
Counterpart to :meth:`_init_fields_ovstage`. Only fields the ovstage path never touches live
here: the ``bind_attribute``/``bind_array_attribute`` handles and the caller-owned object
transform buffer. State shared by both paths (``_object_newton_indices``, the particle
offset/count lists) stays in :meth:`__init__`.
"""
self._camera_xform_binding = None
self._object_xform_binding = None
self._object_transform_buffer: wp.array | None = None
self._deformable_points_binding = None
self._particle_points_binding = None
self._particle_workaround_applied = False
self._cable_points_binding = None
self._cable_point_slices: list[wp.array] = []
def _initialize_from_spec_legacy(self, spec: CameraRenderSpec):
width = spec.cfg.width
height = spec.cfg.height
num_envs = spec.num_instances
data_types = spec.cfg.data_types if spec.cfg.data_types else ["rgb"]
if spec.cfg.isp_cfg is not None and "rgb_hdr" not in data_types:
data_types = [*data_types, "rgb_hdr"]
env_0_prefix = "/World/envs/env_0/"
first_cam_path = spec.camera_prim_paths[0]
if not first_cam_path.startswith(env_0_prefix):
raise RuntimeError(f"Expected camera prim under '{env_0_prefix}', got '{first_cam_path}'")
self._camera_rel_path = spec.camera_path_relative_to_env_0
if self._exported_usd_string is None:
raise RuntimeError("Expected an exported USD string from stage")
render_product_string, render_product_path = build_render_product_as_string(
width=width,
height=height,
num_envs=num_envs,
data_types=data_types,
minimal_mode=_resolve_rtx_minimal_mode(data_types),
camera_rel_path=self._camera_rel_path,
background_color=getattr(spec.cfg, "background_color", None),
device_id=self._warp_device.ordinal,
enable_shadows=self.cfg.enable_shadows,
)
self._render_product_paths.append(render_product_path)
combined_usd_string = self._exported_usd_string + "\n\n" + render_product_string
self._exported_usd_string = None
if self.cfg.temp_usd_dir is not None:
_write_file(Path(self.cfg.temp_usd_dir), "ovrtx_renderer_stage.usda", combined_usd_string)
self._renderer.open_usd_from_string(combined_usd_string)
camera_paths = [f"/World/envs/env_{i}/{self._camera_rel_path}" for i in range(num_envs)]
if num_envs > 1:
self._clone_sources_in_ovrtx()
self._update_scene_partitions_after_clone(num_envs)
self._renderer.write_array_attribute(
prim_paths=[render_product_path], attribute_name="camera", tensors=[camera_paths]
)
self._initialized_scene = True
self._camera_xform_binding = self._renderer.bind_attribute(
prim_paths=camera_paths,
attribute_name="omni:xform",
semantic=Semantic.XFORM_MAT4x4,
prim_mode=PrimMode.EXISTING_ONLY,
)
self._renderer.write_attribute(
prim_paths=camera_paths,
attribute_name="omni:resetXformStack",
tensor=np.full(num_envs, True, dtype=np.bool_),
)
if self._camera_xform_binding is None:
raise RuntimeError("Camera binding is None — cannot render without a valid camera binding")
self._setup_xform_bindings()
self._setup_deformable_bindings(num_envs)
self._setup_particle_bindings()
self._setup_cable_bindings()
def _clone_sources_in_ovrtx(self):
clone_plan = self._clone_plan
if clone_plan is None or clone_plan.env_ids is None or clone_plan.positions is None:
raise RuntimeError("Clone plan with environment ids and positions is required when using OVRTX cloning")
env_ids = clone_plan.env_ids.detach().cpu()
clone_mask = clone_plan.clone_mask.detach().cpu()
num_envs = len(env_ids)
env_prim_paths = [f"/World/envs/env_{env_id}" for env_id in env_ids.tolist()]
for row_idx, (source, destination) in enumerate(zip(clone_plan.sources, clone_plan.destinations, strict=True)):
target_paths = [
destination.format(int(env_id))
for env_id in env_ids[clone_mask[row_idx]].tolist()
if destination.format(int(env_id)) != source
]
if target_paths:
try:
self._renderer.clone_usd(source, target_paths)
except Exception as exc:
raise RuntimeError(f"Failed to clone row {row_idx} from {source}: {exc}") from exc
env_root_xforms = np.tile(np.eye(4, dtype=np.float64), (num_envs, 1, 1))
env_root_xforms[:, 3, :3] = clone_plan.positions.cpu().numpy()
self._renderer.write_attribute(
prim_paths=env_prim_paths,
attribute_name="omni:xform",
tensor=env_root_xforms,
semantic=Semantic.XFORM_MAT4x4,
prim_mode=PrimMode.MUST_EXIST,
)
def _update_scene_partitions_after_clone(self, num_envs: int):
partition_tokens = [f"env_{i}" for i in range(num_envs)]
env_prim_paths = [f"/World/envs/env_{i}" for i in range(num_envs)]
camera_prim_paths = [f"/World/envs/env_{i}/{self._camera_rel_path}" for i in range(num_envs)]
self._renderer.write_attribute(
env_prim_paths, "primvars:omni:scenePartition", partition_tokens, semantic=Semantic.TOKEN_STRING
)
self._renderer.write_attribute(
camera_prim_paths, "omni:scenePartition", partition_tokens, semantic=Semantic.TOKEN_STRING
)
def _setup_xform_bindings_legacy(self):
try:
from isaaclab_newton.physics import NewtonManager
except ImportError:
return
if SimulationContext.instance() is None:
return
newton_model = NewtonManager.get_model()
if newton_model is None:
return
all_body_paths = getattr(newton_model, "body_label", None)
if all_body_paths is None:
return
object_paths = []
newton_indices = []
for idx, path in enumerate(all_body_paths):
if "/World/envs/" in path and self._camera_rel_path not in path and "GroundPlane" not in path:
object_paths.append(path)
newton_indices.append(idx)
if not object_paths:
return
self._object_xform_binding = self._renderer.bind_attribute(
prim_paths=object_paths,
attribute_name="omni:xform",
semantic=Semantic.XFORM_MAT4x4,
prim_mode=PrimMode.EXISTING_ONLY,
)
self._renderer.write_attribute(
prim_paths=object_paths,
attribute_name="omni:resetXformStack",
tensor=np.full(len(object_paths), True, dtype=np.bool_),
)
if self._object_xform_binding is None:
raise RuntimeError("Failed to create OVRTX object bindings")
self._object_newton_indices = wp.array(newton_indices, dtype=wp.int32, device=self._device)
self._object_scales = self._create_object_scale_array(object_paths)
self._object_transform_buffer = wp.zeros(len(newton_indices), dtype=wp.mat44d, device=self._device)
def _setup_deformable_bindings_legacy(self, num_envs: int):
try:
from isaaclab_newton.physics import NewtonManager
except ImportError:
return
deformable_registry = NewtonManager._deformable_registry
if not deformable_registry:
return
bad_entries = [entry for entry in deformable_registry if len(entry.particle_offsets) != num_envs]
if bad_entries:
details = "\n".join(
f"- '{entry.prim_path}' has {len(entry.particle_offsets)} particle offsets" for entry in bad_entries
)
raise RuntimeError(
f"OVRTX expects one particle offset per environment ({num_envs}), but the following "
f"deformable entries have a mismatched offset count:\n{details}"
)
self._deformable_particle_offsets = []
self._deformable_particle_counts = []
vis_mesh_prim_paths = []
for entry in deformable_registry:
for idx, particle_offset in enumerate(entry.particle_offsets):
self._deformable_particle_offsets.append(particle_offset)
self._deformable_particle_counts.append(entry.particles_per_body)
vis_mesh_prim_paths.append(
re.sub(r"(?<=[Ee]nv_)(?:\[\^/\][*+]|\.\*)", str(idx), entry.vis_mesh_prim_path)
)
prim_count = len(vis_mesh_prim_paths)
if not prim_count:
return
self._renderer.write_attribute(
prim_paths=vis_mesh_prim_paths,
attribute_name="omni:resetXformStack",
tensor=np.full(prim_count, True, dtype=np.bool_),
prim_mode=PrimMode.MUST_EXIST,
)
self._renderer.write_attribute(
prim_paths=vis_mesh_prim_paths,
attribute_name="omni:xform",
tensor=np.tile(np.eye(4, dtype=np.float64), (prim_count, 1, 1)),
semantic=Semantic.XFORM_MAT4x4,
prim_mode=PrimMode.MUST_EXIST,
)
self._deformable_points_binding = self._renderer.bind_array_attribute(
prim_paths=vis_mesh_prim_paths,
attribute_name="points",
dtype=np.float32,
shape=(3,),
prim_mode=PrimMode.MUST_EXIST,
flags=BindingFlag.OPTIMIZE,
)
if self._deformable_points_binding is None:
raise RuntimeError("Failed to create OVRTX deformable body bindings")
def _setup_cable_bindings_legacy(self) -> None:
discovered = self._discover_cable_segment_bindings()
if discovered is None:
return
cable_prim_paths, flat_shape_ids, offsets, counts = discovered
prim_count = len(cable_prim_paths)
self._renderer.write_attribute(
prim_paths=cable_prim_paths,
attribute_name="omni:resetXformStack",
tensor=np.full(prim_count, True, dtype=np.bool_),
prim_mode=PrimMode.MUST_EXIST,
)
self._renderer.write_attribute(
prim_paths=cable_prim_paths,
attribute_name="omni:xform",
tensor=np.tile(np.eye(4, dtype=np.float64), (prim_count, 1, 1)),
semantic=Semantic.XFORM_MAT4x4,
prim_mode=PrimMode.MUST_EXIST,
)
self._cable_points_binding = self._renderer.bind_array_attribute(
prim_paths=cable_prim_paths,
attribute_name="points",
dtype=np.float32,
shape=(3,),
prim_mode=PrimMode.MUST_EXIST,
flags=BindingFlag.OPTIMIZE,
)
if self._cable_points_binding is None:
raise RuntimeError("Failed to create OVRTX cable point bindings")
self._allocate_cable_device_buffers(flat_shape_ids, offsets, counts)
self._cable_point_slices = [
self._cable_points[offset + curve : offset + curve + segment_count + 1]
for curve, (offset, segment_count) in enumerate(zip(offsets, counts, strict=True))
]
def _setup_particle_bindings_legacy(self) -> None:
try:
from isaaclab_newton.physics import NewtonManager
except ImportError:
return
particle_visual_prims = NewtonManager._particle_visual_prims
if not particle_visual_prims:
return
self._particle_visual_offsets = []
self._particle_visual_counts = []
points_prim_paths = []
for prim_path, record in particle_visual_prims.items():
points_prim_paths.append(prim_path)
self._particle_visual_offsets.append(record.offset)
self._particle_visual_counts.append(record.count)
prim_count = len(points_prim_paths)
self._renderer.write_attribute(
prim_paths=points_prim_paths,
attribute_name="omni:resetXformStack",
tensor=np.full(prim_count, True, dtype=np.bool_),
prim_mode=PrimMode.MUST_EXIST,
)
self._renderer.write_attribute(
prim_paths=points_prim_paths,
attribute_name="omni:xform",
tensor=np.tile(np.eye(4, dtype=np.float64), (prim_count, 1, 1)),
semantic=Semantic.XFORM_MAT4x4,
prim_mode=PrimMode.MUST_EXIST,
)
self._particle_points_binding = self._renderer.bind_array_attribute(
prim_paths=points_prim_paths,
attribute_name="points",
dtype=np.float32,
shape=(3,),
prim_mode=PrimMode.MUST_EXIST,
flags=BindingFlag.OPTIMIZE,
)
def create_render_data(self, spec: CameraRenderSpec) -> OVRTXRenderData:
# Resolve the device once through Warp: a bare "cuda" pins to Warp's current CUDA device
# here, and the normalized string keeps every downstream consumer on that same device.
self._warp_device = wp.get_device(spec.device)
self._device = str(self._warp_device)
if not self._initialized_scene:
self._initialize_from_spec(spec)
return OVRTXRenderData(spec, self._device)
def set_outputs(self, render_data: OVRTXRenderData, output_data: dict[str, ProxyArray]) -> None:
render_data.warp_buffers = {
name: proxy.warp for name, proxy in output_data.items() if name != str(RenderBufferKind.RGB)
}
if render_data.ppisp_pipeline is not None and str(RenderBufferKind.RGB_HDR) not in render_data.warp_buffers:
ref_proxy = next(iter(output_data.values()))
render_data.warp_buffers[str(RenderBufferKind.RGB_HDR)] = wp.zeros(
(render_data.num_envs, render_data.height, render_data.width, 3),
dtype=wp.float32,
device=ref_proxy.device,
)
if render_data.ppisp_pipeline is not None and str(RenderBufferKind.RGBA) not in render_data.warp_buffers:
raise ValueError(
"OVRTX renderer ISP requires 'rgba' (or 'rgb', which aliases into rgba) as the"
" LDR output destination, but neither was provided. Add 'rgb' or 'rgba' to"
" Camera.cfg.data_types when isp_cfg is set."
)
def _update_transforms_legacy(self) -> None:
"""Sync transforms to OVRTX."""
if (
self._object_xform_binding is None
or self._object_newton_indices is None
or self._object_scales is None
or self._object_transform_buffer is None
):
return
from isaaclab_newton.physics import NewtonManager
newton_state = NewtonManager.get_state()
if newton_state is None:
raise RuntimeError("Newton state should not be None")
body_q = getattr(newton_state, "body_q", None)
if body_q is None:
return
wp.launch(
kernel=sync_newton_transforms_kernel,
dim=len(self._object_newton_indices),
inputs=[self._object_transform_buffer, self._object_newton_indices, body_q, self._object_scales],
device=self._device,
)
# Blocking ``write()`` so the buffer stays valid until OVRTX finishes reading it.
# ``DataAccess.ASYNC`` + the Warp CUDA stream let OVRTX read in place and wait
# on-GPU for the kernel; ``SYNC`` is rejected for GPU buffers.
self._object_xform_binding.write(
self._object_transform_buffer,
data_access=DataAccess.ASYNC,
cuda_stream=self._warp_device.stream.cuda_stream,
)
def _update_geometries_legacy(self) -> None:
if self._deformable_points_binding is not None:
self._write_particle_q_slices(
self._deformable_points_binding,
self._deformable_particle_offsets,
self._deformable_particle_counts,
)
if self._particle_points_binding is not None:
self._write_particle_q_slices(
self._particle_points_binding,
self._particle_visual_offsets,
self._particle_visual_counts,
)
if self._cable_points_binding is not None:
self._write_cable_points_legacy()
def _write_cable_points_legacy(self) -> None:
self._compute_cable_points_world()
self._cable_points_binding.write(
cast(Any, self._cable_point_slices),
data_access=DataAccess.ASYNC,
cuda_stream=self._warp_device.stream.cuda_stream,
)
def _write_particle_q_slices(self, binding: Any, particle_offsets: list[int], particle_counts: list[int]) -> None:
from isaaclab_newton.physics import NewtonManager
state = NewtonManager.get_state()
if state is None:
raise RuntimeError("Newton state should not be None")
particle_q = getattr(state, "particle_q", None)
if particle_q is None:
raise RuntimeError("Newton state has no particle_q but particle geometry bindings exist")
particle_slices = [
particle_q[offset : offset + count] for offset, count in zip(particle_offsets, particle_counts, strict=True)
]
binding.write(
cast(Any, particle_slices),
data_access=DataAccess.ASYNC,
cuda_stream=self._warp_device.stream.cuda_stream,
)
def _update_camera_legacy(
self, render_data: OVRTXRenderData, positions: ProxyArray, orientations: ProxyArray, intrinsics: ProxyArray
) -> None:
num_envs = positions.shape[0]
converted_wp = wp.empty(num_envs, dtype=wp.quatf, device=self._device)
convert_camera_frame_orientation_convention_wp(
src=orientations.warp,
dst=converted_wp,
origin="world",
target="opengl",
device=self._device,
)
camera_transforms = wp.zeros(num_envs, dtype=wp.mat44d, device=self._device)
wp.launch(
kernel=create_camera_transforms_kernel,
dim=num_envs,
inputs=[positions, converted_wp, camera_transforms],
device=self._device,
)
if self._camera_xform_binding is not None:
self._camera_xform_binding.write(
camera_transforms,
data_access=DataAccess.ASYNC,
cuda_stream=self._warp_device.stream.cuda_stream,
)
def read_output(self, render_data: OVRTXRenderData, camera_data: CameraData) -> None:
assert camera_data.info is not None, "CameraData.info should be created in CameraData.allocate"
for output_name in camera_data.info:
camera_data.info[output_name] = render_data.renderer_info.get(output_name)
def _generate_random_colors_from_ids(self, input_ids: wp.array, output_colors: wp.array | None) -> wp.array:
if output_colors is None or output_colors.shape != input_ids.shape:
output_colors = wp.zeros(shape=input_ids.shape, dtype=wp.uint32, device=self._device)
wp.launch(
kernel=generate_random_colors_from_ids_kernel,
dim=input_ids.shape,
inputs=[input_ids, output_colors],
device=self._device,
)
return output_colors
@contextlib.contextmanager
def _map_render_var_to_dlpack(self, render_var: Any) -> Iterator[wp.array]:
gpu_side_sync = _gpu_side_render_var_sync_enabled()
sync_stream = self._warp_device.stream.cuda_stream if gpu_side_sync else 0
with render_var.map(device=Device.CUDA, sync_stream=sync_stream) as mapping:
if not gpu_side_sync:
mapping.wait()
yield wp.from_dlpack(mapping)
def _process_id_segmentation_render_var(
self,
render_data: OVRTXRenderData,
frame,
output_buffers: dict,
render_var_name: str,
buffer_key: str,
colorize: bool,
) -> None:
if render_var_name not in frame.render_vars or buffer_key not in output_buffers:
return
with self._map_render_var_to_dlpack(frame.render_vars[render_var_name]) as tiled_data:
if tiled_data.dtype != wp.uint32:
return
if colorize:
color_buffer = self._generate_random_colors_from_ids(
tiled_data, self._output_id_color_buffers.get(buffer_key)
)
self._output_id_color_buffers[buffer_key] = color_buffer
colors_torch = wp.to_torch(color_buffer)
colors_uint8 = colors_torch.view(torch.uint8)
if colors_torch.dim() == 2:
h, w = colors_torch.shape
colors_uint8 = colors_uint8.reshape(h, w, 4)
tiled_data = wp.from_torch(colors_uint8, dtype=wp.uint8)
self._extract_rgba_tiles(render_data, tiled_data, output_buffers, buffer_key)
else:
if tiled_data.ndim == 2:
tiled_data = tiled_data.reshape((*tiled_data.shape, 1))
self._launch_extract_all_tiles(render_data, tiled_data, output_buffers[buffer_key])
def _process_semantic_id_map(self, render_data: OVRTXRenderData, frame) -> None:
if "SemanticIdMap" not in frame.render_vars:
return
with frame.render_vars["SemanticIdMap"].map(device=Device.CPU) as mapping:
labels_by_id = decode_semantic_id_map(np.from_dlpack(mapping))
render_data.renderer_info["semantic_segmentation"] = {
"idToLabels": build_semantic_id_to_labels(
labels_by_id, colorize=self.cfg.colorize_semantic_segmentation, device=self._device
)
}
def _process_instance_segmentation_maps(self, render_data: OVRTXRenderData, frame) -> None:
required_vars = ("StableIdSemanticIdMap", "StableIdMap", "SemanticIdMap")
missing = [var for var in required_vars if var not in frame.render_vars]
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:
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:
stable_id_to_path = decode_stable_id_map(np.from_dlpack(mapping))
with frame.render_vars["SemanticIdMap"].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(
stable_id_semantic_id_map,
stable_id_to_path,
semantic_id_to_labels,
colorize=self.cfg.colorize_instance_segmentation,
device=self._device,
)
render_data.renderer_info["instance_segmentation"] = {
"idToLabels": id_to_labels,
"idToSemantics": id_to_semantics,
}
def _launch_extract_all_tiles(
self, render_data: OVRTXRenderData, tiled_buffer: wp.array, output_buffer: wp.array
) -> None:
tiled_channels = tiled_buffer.shape[-1]
output_channels = output_buffer.shape[-1]
if output_channels > tiled_channels:
raise ValueError(
f"Output buffer has {output_channels} channels but the tiled buffer only has {tiled_channels};"
" extract_all_tiles_kernel would read out of bounds."
)
wp.launch(
kernel=extract_all_tiles_kernel,
dim=(render_data.num_envs, render_data.height, render_data.width),
inputs=[tiled_buffer, output_buffer, render_data.num_cols, render_data.width, render_data.height],
device=self._device,
)
def _extract_rgba_tiles(
self,
render_data: OVRTXRenderData,
tiled_data: wp.array,
output_buffers: dict,
buffer_key: str,
suffix: str = "",
) -> None:
output_buffer = output_buffers[buffer_key]
if output_buffer.shape[-1] not in (3, 4):
raise ValueError(f"Expected RGB (3 channels) or RGBA (4 channels), got {output_buffer.shape[-1]}")
self._launch_extract_all_tiles(render_data, tiled_data, output_buffer)
def _extract_depth_tiles(
self,
render_data: OVRTXRenderData,
tiled_depth_data: wp.array,
output_buffers: dict,
buffer_keys: Sequence[str],
) -> None:
for depth_type in buffer_keys:
if depth_type in output_buffers:
self._launch_extract_all_tiles(render_data, tiled_depth_data, output_buffers[depth_type])
def _extract_hdr_color_tiles(
self, render_data: OVRTXRenderData, tiled_data: wp.array, output_buffers: dict
) -> None:
if "rgb_hdr" not in output_buffers:
return
if tiled_data.dtype not in (wp.float16, wp.float32):
raise TypeError(f"Unsupported OVRTX HdrColor dtype: {tiled_data.dtype}.")
self._launch_extract_all_tiles(render_data, tiled_data, output_buffers["rgb_hdr"])
def _prepare_ppisp_hdr_source(
self, render_data: OVRTXRenderData, tiled_data: wp.array, output_buffers: dict
) -> wp.array:
if render_data.ppisp_pipeline is None:
return tiled_data
output_device = str(output_buffers[str(RenderBufferKind.RGB_HDR)].device)
if str(tiled_data.device) == output_device:
return tiled_data
# The render product pins ``deviceIds`` to this renderer's CUDA device, so the mapping
# normally lands on the output device already. This stays as a fallback for the case OVRTX
# reports as "deviceIds ... not in the active device set" and falls back to automatic
# assignment.
return wp.clone(tiled_data, device=output_device)
def _process_render_frame(self, render_data: OVRTXRenderData, frame, output_buffers: dict) -> None:
render_data.renderer_info.clear()
if "LdrColor" in frame.render_vars:
buffer_key = None
if render_data.ppisp_pipeline is None and "rgba" in output_buffers:
buffer_key = "rgba"
else:
for data_type in _RTX_MINIMAL_MODES:
if data_type in output_buffers:
buffer_key = data_type
break
if buffer_key is not None:
with self._map_render_var_to_dlpack(frame.render_vars["LdrColor"]) 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 or not any(key in output_buffers for key in buffer_keys):
continue
with self._map_render_var_to_dlpack(frame.render_vars[depth_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_data:
self._extract_rgba_tiles(render_data, tiled_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_data:
tiled_data = self._prepare_ppisp_hdr_source(render_data, tiled_data, output_buffers)
self._extract_hdr_color_tiles(render_data, tiled_data, output_buffers)
self._process_id_segmentation_render_var(
render_data,
frame,
output_buffers,
"SemanticSegmentation",
"semantic_segmentation",
self.cfg.colorize_semantic_segmentation,
)
if "semantic_segmentation" in output_buffers:
self._process_semantic_id_map(render_data, frame)
self._process_id_segmentation_render_var(
render_data,
frame,
output_buffers,
"NonStableInstanceSegmentation",
"instance_segmentation",
self.cfg.colorize_instance_segmentation,
)
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_data:
self._launch_extract_all_tiles(render_data, tiled_data, output_buffers["normals"])
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_data:
self._launch_extract_all_tiles(render_data, tiled_data, output_buffers["motion_vectors"])
def _render_legacy(self, render_data: OVRTXRenderData) -> None:
if not self._initialized_scene:
raise RuntimeError("Scene not initialized. Call initialize() first.")
if self._renderer is None or not self._render_product_paths:
return
material_writer = self._visual_material_writer_ref() if self._visual_material_writer_ref is not None else None
try:
if material_writer is not None:
material_writer.publish()
products = self._renderer.step(render_products=set(self._render_product_paths), delta_time=1.0 / 60.0)
finally:
if material_writer is not None:
drain_errors = contextlib.nullcontext() if sys.exc_info()[0] is None else contextlib.suppress(Exception)
with drain_errors:
material_writer.drain()
product_path = self._render_product_paths[0]
if product_path in products and products[product_path].frames:
self._process_render_frame(render_data, products[product_path].frames[0], render_data.warp_buffers)
if render_data.ppisp_pipeline is not None:
render_data.ppisp_pipeline.apply(
render_data.warp_buffers[str(RenderBufferKind.RGB_HDR)],
render_data.warp_buffers[str(RenderBufferKind.RGBA)],
)
def _close_legacy(self) -> None:
def _safe_unbind(binding, name: str) -> None:
if binding is None:
return
try:
binding.unbind()
except Exception as exc:
if "destroyed" not in str(exc).lower():
logger.warning("Error unbinding %s: %s", name, exc)
for attr, name in (
("_camera_xform_binding", "camera transforms"),
("_object_xform_binding", "object transforms"),
("_deformable_points_binding", "deformable points"),
("_particle_points_binding", "particle points"),
("_cable_points_binding", "cable points"),
):
_safe_unbind(getattr(self, attr), name)
setattr(self, attr, None)
self._object_transform_buffer = None
self._deformable_particle_offsets = []
self._deformable_particle_counts = []
self._particle_visual_offsets = []
self._particle_visual_counts = []
self._particle_workaround_applied = False
self._cable_point_slices = []
self._cable_segment_counts = []
self._cable_max_points = 0
self._cable_points = None
self._cable_shape_ids = None
self._cable_offsets = None
self._cable_counts = None
if self._renderer:
try:
self._renderer.reset_stage()
except Exception as exc:
logger.warning("Error resetting stage: %s", exc)
self._renderer = None
self._render_product_paths.clear()
self._output_id_color_buffers.clear()
self._initialized_scene = False
def _init_fields(self) -> None:
self._init_fields_ovstage() if self._use_ovstage else self._init_fields_legacy()
def _initialize_from_spec(self, spec: CameraRenderSpec) -> None:
self._initialize_from_spec_ovstage(spec) if self._use_ovstage else self._initialize_from_spec_legacy(spec)
def _setup_xform_bindings(self) -> None:
self._setup_xform_bindings_ovstage() if self._use_ovstage else self._setup_xform_bindings_legacy()
def _setup_deformable_bindings(self, num_envs: int) -> None:
self._setup_deformable_bindings_ovstage(
num_envs
) if self._use_ovstage else self._setup_deformable_bindings_legacy(num_envs)
def _setup_particle_bindings(self) -> None:
self._setup_particle_bindings_ovstage() if self._use_ovstage else self._setup_particle_bindings_legacy()
def _setup_cable_bindings(self) -> None:
self._setup_cable_bindings_ovstage() if self._use_ovstage else self._setup_cable_bindings_legacy()
@staticmethod
def _discover_cable_segment_bindings() -> tuple[list[str], list[int], list[int], list[int]] | None:
try:
from isaaclab_newton.physics import NewtonManager
except ImportError:
return None
cable_segment_shape_ids = NewtonManager.collect_cable_segment_shape_ids()
if not cable_segment_shape_ids:
return None
paths, flat_ids, offsets, counts = [], [], [], []
for prim_path, segment_shape_ids in cable_segment_shape_ids.items():
paths.append(prim_path)
offsets.append(len(flat_ids))
counts.append(len(segment_shape_ids))
flat_ids.extend(segment_shape_ids)
return paths, flat_ids, offsets, counts
def _allocate_cable_device_buffers(self, flat_shape_ids: list[int], offsets: list[int], counts: list[int]) -> None:
self._cable_shape_ids = wp.array(flat_shape_ids, dtype=wp.int32, device=self._device)
self._cable_offsets = wp.array(offsets, dtype=wp.int32, device=self._device)
self._cable_counts = wp.array(counts, dtype=wp.int32, device=self._device)
self._cable_segment_counts = counts
self._cable_max_points = max(counts) + 1 if counts else 0
self._cable_points = wp.zeros(sum(count + 1 for count in counts), dtype=wp.vec3f, device=self._device)
def _compute_cable_points_world(self) -> None:
from isaaclab_newton.physics import NewtonManager
model = NewtonManager.get_model()
state = NewtonManager.get_state()
if state is None:
raise RuntimeError("Newton state should not be None")
wp.launch(
compute_cable_points_world_kernel,
dim=(len(self._cable_segment_counts), self._cable_max_points),
inputs=[
self._cable_shape_ids,
self._cable_offsets,
self._cable_counts,
model.shape_body,
state.body_q,
model.shape_transform,
model.shape_scale,
self._cable_points,
],
device=self._device,
)
def update_transforms(self) -> None:
self._update_transforms_ovstage() if self._use_ovstage else self._update_transforms_legacy()
def update_geometries(self) -> None:
self._update_geometries_ovstage() if self._use_ovstage else self._update_geometries_legacy()
def update_camera(
self, render_data: OVRTXRenderData, positions: ProxyArray, orientations: ProxyArray, intrinsics: ProxyArray
) -> None:
if self._use_ovstage:
self._update_camera_ovstage(render_data, positions, orientations, intrinsics)
else:
self._update_camera_legacy(render_data, positions, orientations, intrinsics)
def render(self, render_data: OVRTXRenderData) -> None:
self._render_ovstage(render_data) if self._use_ovstage else self._render_legacy(render_data)
def cleanup(self, render_data: OVRTXRenderData | None) -> None:
if render_data is None:
return
render_data.warp_buffers.clear()
render_data.renderer_info.clear()
render_data.ppisp_pipeline = None
def close(self) -> None:
self._close_ovstage() if self._use_ovstage else self._close_legacy()
self._visual_material_writer_ref = None
def _init_fields_ovstage(self) -> None:
self._stage = None
self._stage_paths = None
self._ovstage_exit_stack: contextlib.ExitStack | None = None
self._current_ordinal = 0
self._camera_xform_query = self._camera_paths_list = None
self._object_xform_query = self._object_paths_list = None
self._deformable_points_query = self._deformable_paths_list = None
self._particle_points_query = self._particle_paths_list = None
self._cable_points_query = self._cable_paths_list = None
self._cable_point_tensors = []
def _initialize_from_spec_ovstage(self, spec: CameraRenderSpec) -> None:
width, height, num_envs = spec.cfg.width, spec.cfg.height, spec.num_instances
data_types = spec.cfg.data_types if spec.cfg.data_types else ["rgb"]
if spec.cfg.isp_cfg is not None and "rgb_hdr" not in data_types:
data_types = [*data_types, "rgb_hdr"]
first_cam_path = spec.camera_prim_paths[0]
if not first_cam_path.startswith("/World/envs/env_0/"):
raise RuntimeError(f"Expected camera prim under '/World/envs/env_0/', got '{first_cam_path}'")
self._camera_rel_path = spec.camera_path_relative_to_env_0
if self._exported_usd_string is None:
raise RuntimeError("Expected an exported USD string from stage")
render_product_string, render_product_path = build_render_product_as_string(
width=width,
height=height,
num_envs=num_envs,
data_types=data_types,
minimal_mode=_resolve_rtx_minimal_mode(data_types),
camera_rel_path=self._camera_rel_path,
device_id=self._warp_device.ordinal,
enable_shadows=self.cfg.enable_shadows,
)
self._render_product_paths.append(render_product_path)
combined_usd_string = self._exported_usd_string + "\n\n" + render_product_string
self._exported_usd_string = None
if self.cfg.temp_usd_dir is not None:
_write_file(Path(self.cfg.temp_usd_dir), "ovrtx_renderer_stage.usda", combined_usd_string)
self._ovstage_exit_stack = contextlib.ExitStack()
self._stage = self._ovstage_exit_stack.enter_context(create_ovstage("isaaclab.ovrtx"))
self._stage_paths = self._ovstage_exit_stack.enter_context(ovstage.PathDictionary(self._stage))
self._current_ordinal += 1
ovstage.population.open_usd_from_string(
self._stage,
combined_usd_string,
ordinal=self._current_ordinal,
domains=ovstage.PopulationDomain.RENDERING,
)
if num_envs > 1:
self._clone_sources_ovstage()
self._update_scene_partitions_after_clone_ovstage(num_envs)
self._initialized_scene = True
camera_paths = [f"/World/envs/env_{i}/{self._camera_rel_path}" for i in range(num_envs)]
render_product_paths = self._stage_paths.create_path_list_from_strings([render_product_path])
with self._stage.query_from_path_list(render_product_paths) as query:
target_ids = np.array([self._stage_paths.intern_path(path) for path in camera_paths], dtype=np.uint64)
self._stage.write_attribute(
query,
self._stage_paths.intern_token("camera"),
ordinal=self._current_ordinal,
tensors=target_ids,
is_array=True,
semantic=ovstage.AttributeSemantic.RELATIONSHIP_PATH_ID,
).wait()
self._stage_paths.destroy_path_list(render_product_paths)
self._camera_paths_list = self._stage_paths.create_path_list_from_strings(camera_paths)
self._camera_xform_query = self._stage.query_from_path_list(self._camera_paths_list)
if self._camera_xform_query is None:
raise RuntimeError("Camera query is None — cannot render without a valid camera query")
self._stage.write_attribute(
self._camera_xform_query,
"omni:resetXformStack",
ordinal=self._current_ordinal,
tensors=np.full(num_envs, True, dtype=np.bool_),
is_array=False,
).wait()
self._setup_xform_bindings_ovstage()
self._setup_deformable_bindings_ovstage(num_envs)
self._setup_particle_bindings_ovstage()
self._setup_cable_bindings_ovstage()
self._stage.advance_write_floor(ordinal=self._current_ordinal).wait()
self._renderer.attach_ovstage(self._stage)
self._current_ordinal += 1
def _clone_sources_ovstage(self):
clone_plan = self._clone_plan
if clone_plan is None or clone_plan.env_ids is None or clone_plan.positions is None:
raise RuntimeError("Clone plan with environment ids and positions is required when using OVRTX cloning")
env_ids = clone_plan.env_ids.detach().cpu()
clone_mask = clone_plan.clone_mask.detach().cpu()
paths = [f"/World/envs/env_{env_id}" for env_id in env_ids.tolist()]
for row_idx, (source, destination) in enumerate(zip(clone_plan.sources, clone_plan.destinations, strict=True)):
targets = [
destination.format(int(env_id))
for env_id in env_ids[clone_mask[row_idx]].tolist()
if destination.format(int(env_id)) != source
]
if targets:
self._stage.clone(source, targets, ordinal=self._current_ordinal)
xforms = np.tile(np.eye(4, dtype=np.float64), (len(env_ids), 1, 1))
xforms[:, 3, :3] = clone_plan.positions.cpu().numpy()
path_list = self._stage_paths.create_path_list_from_strings(paths)
query = self._stage.query_from_path_list(path_list)
self._stage.write_attribute(
query,
"omni:xform",
ordinal=self._current_ordinal,
tensors=xform_tensor_from_numpy(xforms),
is_array=False,
semantic=ovstage.AttributeSemantic.MATRIX,
).wait()
self._stage.release_query(query).wait()
self._stage_paths.destroy_path_list(path_list)
def _update_scene_partitions_after_clone_ovstage(self, num_envs: int):
tokens = np.array([self._stage_paths.intern_token(f"env_{i}") for i in range(num_envs)], dtype=np.uint64)
for paths, attribute in (
([f"/World/envs/env_{i}" for i in range(num_envs)], "primvars:omni:scenePartition"),
([f"/World/envs/env_{i}/{self._camera_rel_path}" for i in range(num_envs)], "omni:scenePartition"),
):
path_list = self._stage_paths.create_path_list_from_strings(paths)
query = self._stage.query_from_path_list(path_list)
self._stage.write_attribute(
query,
attribute,
ordinal=self._current_ordinal,
tensors=tokens,
is_array=False,
semantic=ovstage.AttributeSemantic.TOKEN_ID,
).wait()
self._stage.release_query(query).wait()
self._stage_paths.destroy_path_list(path_list)
def _setup_xform_bindings_ovstage(self) -> None:
try:
from isaaclab_newton.physics import NewtonManager
except ImportError:
return
model = NewtonManager.get_model()
if SimulationContext.instance() is None or model is None or getattr(model, "body_label", None) is None:
return
object_paths, indices = [], []
for idx, path in enumerate(model.body_label):
if "/World/envs/" in path and self._camera_rel_path not in path and "GroundPlane" not in path:
object_paths.append(path)
indices.append(idx)
if not object_paths:
return
self._object_paths_list = self._stage_paths.create_path_list_from_strings(object_paths)
self._object_xform_query = self._stage.query_from_path_list(self._object_paths_list)
self._stage.write_attribute(
self._object_xform_query,
"omni:resetXformStack",
ordinal=self._current_ordinal,
tensors=np.full(len(object_paths), True, dtype=np.bool_),
is_array=False,
).wait()
self._object_newton_indices = wp.array(indices, dtype=wp.int32, device=self._device)
self._object_scales = self._create_object_scale_array(object_paths)
def _setup_deformable_bindings_ovstage(self, num_envs: int) -> None:
try:
from isaaclab_newton.physics import NewtonManager
except ImportError:
return
registry = NewtonManager._deformable_registry
if not registry:
return
bad = [entry for entry in registry if len(entry.particle_offsets) != num_envs]
if bad:
raise RuntimeError(f"OVRTX expects one particle offset per environment ({num_envs})")
paths = []
self._deformable_particle_offsets = []
self._deformable_particle_counts = []
for entry in registry:
for idx, offset in enumerate(entry.particle_offsets):
paths.append(re.sub(r"(?<=[Ee]nv_)(?:\[\^/\][*+]|\.\*)", str(idx), entry.vis_mesh_prim_path))
self._deformable_particle_offsets.append(offset)
self._deformable_particle_counts.append(entry.particles_per_body)
if not paths:
return
self._deformable_paths_list = self._stage_paths.create_path_list_from_strings(paths)
self._deformable_points_query = self._stage.query_from_path_list(self._deformable_paths_list)
self._write_identity_xforms(self._deformable_points_query, len(paths))
def _setup_particle_bindings_ovstage(self) -> None:
try:
from isaaclab_newton.physics import NewtonManager
except ImportError:
return
records = NewtonManager._particle_visual_prims
if not records:
return
paths = list(records)
self._particle_visual_offsets = [record.offset for record in records.values()]
self._particle_visual_counts = [record.count for record in records.values()]
self._particle_paths_list = self._stage_paths.create_path_list_from_strings(paths)
self._particle_points_query = self._stage.query_from_path_list(self._particle_paths_list)
self._write_identity_xforms(self._particle_points_query, len(paths))
def _setup_cable_bindings_ovstage(self) -> None:
discovered = self._discover_cable_segment_bindings()
if discovered is None:
return
paths, ids, offsets, counts = discovered
self._cable_paths_list = self._stage_paths.create_path_list_from_strings(paths)
self._cable_points_query = self._stage.query_from_path_list(self._cable_paths_list)
self._write_identity_xforms(self._cable_points_query, len(paths))
self._allocate_cable_device_buffers(ids, offsets, counts)
self._cable_point_slices = [
self._cable_points[offset + curve : offset + curve + count + 1]
for curve, (offset, count) in enumerate(zip(offsets, counts, strict=True))
]
self._cable_point_tensors = [points_tensor_from_warp(points) for points in self._cable_point_slices]
def _write_identity_xforms(self, query, count: int) -> None:
self._stage.write_attribute(
query,
"omni:resetXformStack",
ordinal=self._current_ordinal,
tensors=np.full(count, True, dtype=np.bool_),
is_array=False,
).wait()
self._stage.write_attribute(
query,
"omni:xform",
ordinal=self._current_ordinal,
tensors=xform_tensor_from_numpy(np.tile(np.eye(4, dtype=np.float64), (count, 1, 1))),
is_array=False,
semantic=ovstage.AttributeSemantic.MATRIX,
).wait()
def _update_transforms_ovstage(self) -> None:
if self._object_xform_query is None or self._object_newton_indices is None or self._object_scales is None:
return
from isaaclab_newton.physics import NewtonManager
state = NewtonManager.get_state()
if state is None:
raise RuntimeError("Newton state should not be None")
if getattr(state, "body_q", None) is None:
return
transforms = wp.empty(len(self._object_newton_indices), dtype=wp.mat44d, device=self._device)
wp.launch(
sync_newton_transforms_kernel,
len(self._object_newton_indices),
inputs=[transforms, self._object_newton_indices, state.body_q, self._object_scales],
device=self._device,
)
self._stage.write_attribute(
self._object_xform_query,
"omni:xform",
ordinal=self._current_ordinal,
tensors=xform_tensor_from_warp(transforms),
is_array=False,
semantic=ovstage.AttributeSemantic.MATRIX,
cuda_stream=self._warp_device.stream.cuda_stream,
).wait()
def _update_geometries_ovstage(self) -> None:
if self._deformable_points_query is not None or self._particle_points_query is not None:
from isaaclab_newton.physics import NewtonManager
state = NewtonManager.get_state()
if state is None or getattr(state, "particle_q", None) is None:
raise RuntimeError("Newton state has no particle_q but particle geometry queries exist")
if self._deformable_points_query is not None:
self._write_particle_q_slices_ovstage(
self._deformable_points_query,
state.particle_q,
self._deformable_particle_offsets,
self._deformable_particle_counts,
)
if self._particle_points_query is not None:
self._write_particle_q_slices_ovstage(
self._particle_points_query,
state.particle_q,
self._particle_visual_offsets,
self._particle_visual_counts,
)
if self._cable_points_query is not None:
self._write_cable_points_ovstage()
def _write_particle_q_slices_ovstage(self, query, particle_q, offsets, counts) -> None:
tensors = [
points_tensor_from_warp(particle_q[offset : offset + count])
for offset, count in zip(offsets, counts, strict=True)
]
self._stage.write_attribute(
query,
"points",
ordinal=self._current_ordinal,
tensors=tensors,
is_array=True,
semantic=ovstage.AttributeSemantic.POINT,
cuda_stream=self._warp_device.stream.cuda_stream,
).wait()
def _write_cable_points_ovstage(self) -> None:
self._compute_cable_points_world()
self._stage.write_attribute(
self._cable_points_query,
"points",
ordinal=self._current_ordinal,
tensors=self._cable_point_tensors,
is_array=True,
semantic=ovstage.AttributeSemantic.POINT,
cuda_stream=self._warp_device.stream.cuda_stream,
).wait()
def _update_camera_ovstage(
self, render_data: OVRTXRenderData, positions: ProxyArray, orientations: ProxyArray, intrinsics: ProxyArray
) -> None:
num_envs = positions.shape[0]
converted = wp.empty(num_envs, dtype=wp.quatf, device=self._device)
convert_camera_frame_orientation_convention_wp(
src=orientations.warp, dst=converted, origin="world", target="opengl", device=self._device
)
transforms = wp.zeros(num_envs, dtype=wp.mat44d, device=self._device)
wp.launch(
create_camera_transforms_kernel, num_envs, inputs=[positions, converted, transforms], device=self._device
)
if self._camera_xform_query is not None:
self._stage.write_attribute(
self._camera_xform_query,
"omni:xform",
ordinal=self._current_ordinal,
tensors=xform_tensor_from_warp(transforms),
is_array=False,
semantic=ovstage.AttributeSemantic.MATRIX,
cuda_stream=self._warp_device.stream.cuda_stream,
).wait()
def _render_ovstage(self, render_data: OVRTXRenderData) -> None:
if not self._initialized_scene:
raise RuntimeError("Scene not initialized. Call initialize() first.")
if self._renderer is None or not self._render_product_paths:
return
writer = self._visual_material_writer_ref() if self._visual_material_writer_ref is not None else None
try:
if writer is not None:
writer.publish()
self._stage.advance_write_floor(ordinal=self._current_ordinal).wait()
finally:
if writer is not None:
with contextlib.nullcontext() if sys.exc_info()[0] is None else contextlib.suppress(Exception):
writer.drain()
products = self._renderer.step(
render_products=set(self._render_product_paths),
delta_time=1.0 / 60.0,
ordinal=self._current_ordinal,
)
self._current_ordinal += 1
path = self._render_product_paths[0]
if path in products and products[path].frames:
self._process_render_frame(render_data, products[path].frames[0], render_data.warp_buffers)
if render_data.ppisp_pipeline is not None:
render_data.ppisp_pipeline.apply(
render_data.warp_buffers[str(RenderBufferKind.RGB_HDR)],
render_data.warp_buffers[str(RenderBufferKind.RGBA)],
)
def _close_ovstage(self) -> None:
def release(query, paths):
if query is not None and self._stage is not None:
with contextlib.suppress(Exception):
self._stage.release_query(query).wait()
if paths is not None and self._stage_paths is not None:
with contextlib.suppress(Exception):
self._stage_paths.destroy_path_list(paths)
for query, paths in (
(self._camera_xform_query, self._camera_paths_list),
(self._object_xform_query, self._object_paths_list),
(self._deformable_points_query, self._deformable_paths_list),
(self._particle_points_query, self._particle_paths_list),
(self._cable_points_query, self._cable_paths_list),
):
release(query, paths)
self._camera_xform_query = self._camera_paths_list = None
self._object_xform_query = self._object_paths_list = None
self._deformable_points_query = self._deformable_paths_list = None
self._particle_points_query = self._particle_paths_list = None
self._cable_points_query = self._cable_paths_list = None
self._object_newton_indices = self._object_scales = None
self._object_scales_by_path = {}
self._deformable_particle_offsets = self._deformable_particle_counts = []
self._particle_visual_offsets = self._particle_visual_counts = []
self._cable_segment_counts = []
self._cable_max_points = 0
self._cable_point_tensors = self._cable_point_slices = []
self._cable_points = self._cable_shape_ids = self._cable_offsets = self._cable_counts = None
if self._renderer is not None and self._stage is not None:
self._renderer.detach_ovstage()
self._renderer = None
if self._ovstage_exit_stack is not None:
self._ovstage_exit_stack.close()
self._ovstage_exit_stack = self._stage = self._stage_paths = None
self._render_product_paths.clear()
self._output_id_color_buffers.clear()
self._initialized_scene = False
self._current_ordinal = 0