Skip to content

Commit e47d8a1

Browse files
authored
Complete OVRTX cloning (#5781)
# Description Complete OVRTX cloning ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there
1 parent ef2d74e commit e47d8a1

75 files changed

Lines changed: 1002 additions & 98 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build.yaml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,12 @@ jobs:
681681
isaacsim-base-image: ${{ needs.config.outputs.isaacsim_image_name }}
682682
isaacsim-version: ${{ needs.config.outputs.isaacsim_image_tag }}
683683
filter-pattern: "isaaclab_tasks"
684-
include-files: "test_rendering_cartpole.py,test_rendering_dexsuite_kuka.py,test_rendering_registered_tasks.py,test_rendering_shadow_hand.py"
684+
include-files: >-
685+
test_rendering_cartpole.py,
686+
test_rendering_dexsuite_kuka_homo.py,
687+
test_rendering_dexsuite_kuka_hetero.py,
688+
test_rendering_registered_tasks.py,
689+
test_rendering_shadow_hand.py
685690
container-name: isaac-lab-rendering-correctness-test
686691

687692
test-rendering-correctness-kitless:
@@ -703,7 +708,11 @@ jobs:
703708
isaacsim-version: ${{ needs.config.outputs.isaacsim_image_tag }}
704709
filter-pattern: "isaaclab_tasks"
705710
extra-pip-packages: "ovrtx ovphysx==0.4.13"
706-
include-files: "test_rendering_cartpole_kitless.py,test_rendering_dexsuite_kuka_kitless.py,test_rendering_shadow_hand_kitless.py"
711+
include-files: >-
712+
test_rendering_cartpole_kitless.py,
713+
test_rendering_dexsuite_kuka_homo_kitless.py,
714+
test_rendering_dexsuite_kuka_hetero_kitless.py,
715+
test_rendering_shadow_hand_kitless.py
707716
container-name: isaac-lab-rendering-correctness-kitless-test
708717
#endregion
709718

scripts/benchmarks/nsys_trace.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,5 +162,25 @@
162162
{"function": "NewtonWarpRenderer.render", "color": "0x4CAF50"},
163163
{"function": "NewtonWarpRenderer.read_output", "color": "0xFF9800"}
164164
]
165+
},
166+
{
167+
"domain": "OVRTXRenderer",
168+
"color": "0x76B800",
169+
"module": "isaaclab_ov.renderers.ovrtx_renderer",
170+
"functions": [
171+
"OVRTXRenderer.prepare_stage",
172+
"OVRTXRenderer.create_render_data",
173+
"OVRTXRenderer._clone_sources_in_ovrtx",
174+
"OVRTXRenderer._update_scene_partitions_after_clone",
175+
"OVRTXRenderer._setup_object_bindings",
176+
"OVRTXRenderer.update_transforms",
177+
"OVRTXRenderer.render",
178+
"OVRTXRenderer.read_output",
179+
{"module": "isaaclab_ov.renderers.ovrtx_usd", "function": "create_scene_partition_attributes"},
180+
{"module": "isaaclab_ov.renderers.ovrtx_usd", "function": "export_stage_to_string"},
181+
{"module": "ovrtx", "function": "Renderer.open_usd_from_string", "color": "0x87CEEB"},
182+
{"module": "ovrtx", "function": "Renderer.bind_attribute", "color": "0x87CEEB"},
183+
{"module": "ovrtx", "function": "Renderer.write_attribute", "color": "0x87CEEB"}
184+
]
165185
}
166186
]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Changed
2+
^^^^^^^
3+
4+
* Extended the :attr:`~isaaclab_ov.renderers.OVRTXRendererCfg.use_ovrtx_cloning` path to support
5+
heterogeneous scenes as well as homogeneous ones. :meth:`~isaaclab_ov.renderers.OVRTXRenderer.prepare_stage`
6+
now exports only :class:`~isaaclab.cloner.ClonePlan` source prototypes plus global stage metadata, and
7+
replication uses OVRTX cloning API (``clone_usd``) for all rows in the published clone plan instead of
8+
cloning only ``/World/envs/env_0``.

source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py

Lines changed: 115 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import logging
2323
import math
2424
import os
25+
from itertools import compress
2526
from pathlib import Path
2627
from typing import TYPE_CHECKING, Any, NoReturn
2728

@@ -51,6 +52,7 @@
5152
"(or, manually: pip install --extra-index-url https://pypi.nvidia.com -e 'source/isaaclab_ov[ovrtx]')."
5253
) from exc
5354

55+
from isaaclab.cloner.clone_plan import ClonePlan
5456
from isaaclab.renderers import BaseRenderer, RenderBufferKind, RenderBufferSpec
5557
from isaaclab.sim import SimulationContext
5658
from isaaclab.utils.warp.warp_math import convert_camera_frame_orientation_convention_wp
@@ -130,6 +132,67 @@ def _resolve_rtx_minimal_mode(data_types: list[str]) -> int | None:
130132
return _RTX_MINIMAL_MODES[filtered_data_types[0]]
131133

132134

135+
def _write_file(output_dir: Path, file_name: str, content: str) -> None:
136+
"""Write ``content`` to ``output_dir / file_name``.
137+
138+
Creates ``output_dir`` and any missing parents when needed.
139+
140+
Args:
141+
output_dir: Directory that receives the file.
142+
file_name: Base name of the file to write.
143+
content: Text content written with UTF-8 encoding.
144+
"""
145+
output_dir.mkdir(parents=True, exist_ok=True)
146+
output_path = output_dir / file_name
147+
148+
with open(output_path, "w", encoding="utf-8") as file:
149+
file.write(content)
150+
logger.info("Wrote USD file: %s", output_path)
151+
152+
153+
def _create_homogeneous_clone_plan(num_envs: int) -> ClonePlan:
154+
"""Create a homogeneous fallback plan that replicates ``env_0`` to every environment."""
155+
return ClonePlan(
156+
sources=("/World/envs/env_0",),
157+
destinations=("/World/envs/env_{}",),
158+
clone_mask=torch.ones((1, num_envs), dtype=torch.bool, device="cpu"),
159+
)
160+
161+
162+
def _resolve_clone_plan(num_envs: int) -> ClonePlan:
163+
"""Resolve clone plan for local use.
164+
165+
If no clone plan is published by the scene or it has no active rows, returns a homogeneous clone plan.
166+
If the clone plan has some inactive rows, returns a copy of the published clone plan with only the active rows.
167+
If the clone plan has all active rows, returns the published clone plan (shallow copy).
168+
"""
169+
published_clone_plan = SimulationContext.instance().get_clone_plan()
170+
171+
if published_clone_plan is None:
172+
logger.warning("No clone plan is published by the scene; returning homogeneous clone plan")
173+
return _create_homogeneous_clone_plan(num_envs)
174+
175+
active_rows = published_clone_plan.clone_mask.any(dim=1)
176+
177+
# If no rows are active, return a homogeneous clone plan.
178+
if not active_rows.any():
179+
logger.warning("Clone plan has no active rows, returning homogeneous clone plan")
180+
return _create_homogeneous_clone_plan(num_envs)
181+
182+
# If some rows are inactive, return a copy of the published clone plan with only the active rows.
183+
if not active_rows.all():
184+
logger.warning("Clone plan has some inactive rows; returning a copy with only active rows")
185+
active = active_rows.tolist()
186+
return ClonePlan(
187+
sources=tuple(compress(published_clone_plan.sources, active)),
188+
destinations=tuple(compress(published_clone_plan.destinations, active)),
189+
clone_mask=published_clone_plan.clone_mask[active_rows],
190+
)
191+
192+
# If all rows are active, return the published clone plan (shallow copy).
193+
return published_clone_plan
194+
195+
133196
class OVRTXRenderData:
134197
"""OVRTX-specific RenderData. Holds warp output buffers sized from :class:`CameraRenderSpec`."""
135198

@@ -197,14 +260,7 @@ def __init__(self, cfg: OVRTXRendererCfg):
197260
self._exported_usd_string: str | None = None
198261
self._camera_rel_path: str | None = None
199262
self._output_semantic_color_buffer: wp.array | None = None
200-
201-
self._use_ovrtx_cloning = self.cfg.use_ovrtx_cloning
202-
203-
if self._use_ovrtx_cloning:
204-
clone_plan = SimulationContext.instance().get_clone_plan()
205-
if clone_plan and not clone_plan.clone_mask.all().item():
206-
logger.warning("OVRTX cloning disabled because the simulation uses a heterogeneous env setup")
207-
self._use_ovrtx_cloning = False
263+
self._clone_plan: ClonePlan | None = None
208264

209265
logger.info("Creating OVRTX renderer...")
210266
OVRTX_CONFIG = RendererConfig(
@@ -246,16 +302,27 @@ def prepare_cameras(self, stage: Any, spec: CameraRenderSpec) -> None:
246302
def prepare_stage(self, stage: Any, num_envs: int) -> None:
247303
"""Prepare the USD stage for OVRTX before :meth:`create_render_data`.
248304
249-
Adds cloning attributes and exports the stage to a string held on the renderer until
305+
Adds scene partition attributes and exports the stage to a string held on the renderer until
250306
:meth:`create_render_data` is called.
251307
"""
252308
if stage is None:
253309
return
254310

255-
logger.info("Preparing stage for export (%d envs, cloning=%s)...", num_envs, self._use_ovrtx_cloning)
256-
create_scene_partition_attributes(stage, num_envs, self._use_ovrtx_cloning)
311+
# If temp_usd_dir is set, write the pre-ovrtx stage to a temporary file.
312+
if self.cfg.temp_usd_dir is not None:
313+
_write_file(Path(self.cfg.temp_usd_dir), "pre_ovrtx_renderer_stage.usda", stage.ExportToString())
257314

258-
self._exported_usd_string = export_stage_to_string(stage, num_envs, self._use_ovrtx_cloning)
315+
logger.info("Preparing stage (%d envs)...", num_envs)
316+
create_scene_partition_attributes(stage, num_envs)
317+
318+
# Resolve the clone plan for local use.
319+
self._clone_plan = _resolve_clone_plan(num_envs)
320+
321+
self._exported_usd_string = export_stage_to_string(
322+
stage,
323+
num_envs,
324+
source_paths=self._clone_plan.sources,
325+
)
259326

260327
def _initialize_from_spec(self, spec: CameraRenderSpec):
261328
"""Initialize the OVRTX renderer with internal environment cloning.
@@ -294,12 +361,7 @@ def _initialize_from_spec(self, spec: CameraRenderSpec):
294361

295362
# If temp_usd_dir is set, write the combined USD stage to a temporary file.
296363
if self.cfg.temp_usd_dir is not None:
297-
temp_usd_dir = Path(self.cfg.temp_usd_dir)
298-
temp_usd_dir.mkdir(parents=True, exist_ok=True)
299-
temp_usd_path = temp_usd_dir / "ovrtx_renderer_stage.usda"
300-
with open(temp_usd_path, "w", encoding="utf-8") as f:
301-
f.write(combined_usd_string)
302-
logger.info("Wrote combined USD stage to %s", temp_usd_path)
364+
_write_file(Path(self.cfg.temp_usd_dir), "ovrtx_renderer_stage.usda", combined_usd_string)
303365

304366
logger.info("Loading USD into OvRTX...")
305367
try:
@@ -309,9 +371,8 @@ def _initialize_from_spec(self, spec: CameraRenderSpec):
309371
logger.exception("Error loading USD: %s", e)
310372
raise
311373

312-
if self._use_ovrtx_cloning and num_envs > 1:
313-
logger.info("Using OVRTX internal cloning")
314-
self._clone_environments_in_ovrtx(num_envs)
374+
if num_envs > 1:
375+
self._clone_sources_in_ovrtx()
315376
self._update_scene_partitions_after_clone(num_envs)
316377

317378
self._initialized_scene = True
@@ -341,17 +402,39 @@ def _initialize_from_spec(self, spec: CameraRenderSpec):
341402

342403
self._setup_object_bindings()
343404

344-
def _clone_environments_in_ovrtx(self, num_envs: int):
345-
"""Clone base environment (env_0) to all other environments using OvRTX."""
346-
logger.info("Cloning base environment to %d targets...", num_envs - 1)
347-
source_path = "/World/envs/env_0"
348-
target_paths = [f"/World/envs/env_{i}" for i in range(1, num_envs)]
349-
try:
350-
self._renderer.clone_usd(source_path, target_paths)
351-
logger.info("Cloned %d environments successfully", len(target_paths))
352-
except Exception as e:
353-
logger.error("Failed to clone environments: %s", e)
354-
raise RuntimeError(f"OvRTX environment cloning failed: {e}")
405+
def _clone_sources_in_ovrtx(self):
406+
"""Clone sources in OVRTX using the scene :class:`~isaaclab.cloner.ClonePlan`."""
407+
clone_plan = self._clone_plan
408+
if clone_plan is None:
409+
raise RuntimeError("Clone plan is required when using OVRTX cloning")
410+
411+
logger.info("Cloning sources in OVRTX...")
412+
413+
num_envs = clone_plan.clone_mask.shape[1]
414+
env_ids = torch.arange(num_envs, dtype=torch.int32, device=clone_plan.clone_mask.device)
415+
416+
num_cloned_sources = 0
417+
418+
for row_idx, (source, destination) in enumerate(zip(clone_plan.sources, clone_plan.destinations, strict=True)):
419+
target_env_ids = env_ids[clone_plan.clone_mask[row_idx]].tolist()
420+
421+
target_paths = []
422+
for env_id in target_env_ids:
423+
resolved_destination = destination.format(env_id)
424+
if resolved_destination != source:
425+
target_paths.append(resolved_destination)
426+
427+
if target_paths:
428+
logger.debug("Cloning row %d: %s -> %d target(s)", row_idx, source, len(target_paths))
429+
try:
430+
self._renderer.clone_usd(source, target_paths)
431+
num_cloned_sources += 1
432+
except Exception as e:
433+
error_msg = f"Failed to clone row {row_idx} from {source}: {e}"
434+
logger.error(error_msg)
435+
raise RuntimeError(error_msg)
436+
437+
logger.info("Cloned %d sources successfully in OVRTX", num_cloned_sources)
355438

356439
def _update_scene_partitions_after_clone(self, num_envs: int):
357440
"""Update scene partition attributes on cloned environments and cameras in OvRTX."""

source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer_cfg.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,11 @@ class OVRTXRendererCfg(RendererCfg):
2727
"""Type identifier for OVRTX renderer."""
2828

2929
temp_usd_dir: str | None = None
30-
"""Directory for temporary combined USD files (scene + injected cameras).
31-
Used by the OVRTX renderer when building the render scope; must be writable.
32-
"""
33-
34-
use_ovrtx_cloning: bool = True
35-
"""When True, export only env_0 and use OVRTX ``clone_usd``. When False, export full multi-environment stage.
36-
37-
OVRTX cloning is only supported in OVRTX 0.3.0 or newer.
30+
"""Directory for temporary USD debug dumps written during OVRTX stage preparation.
3831
39-
If the simulation uses a heterogeneous env setup, the renderer disables this path and exports the full
40-
multi-environment stage instead (same effect as setting this to ``False`` for that run).
32+
When set, the renderer writes ``pre_ovrtx_renderer_stage.usda`` (raw stage before
33+
partition attributes and export trimming) and ``ovrtx_renderer_stage.usda`` (exported
34+
stage plus injected render products) under this directory. Must be writable.
4135
"""
4236

4337
log_level: str = "verbose"

0 commit comments

Comments
 (0)