Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ rerun = [
isaacsim = ["isaacsim[all,extscache]==6.0.0.1"]
# Omniverse renderer / physics backends (``ov[ovphysx|ovrtx|all]`` selectors).
ov = ["ovphysx==0.5.2+head.f62c22207c"]
rtx = ["ovrtx>=0.3.0,<0.4.0"]
rtx = ["ovrtx>=0.4.0,<0.5.0"]
mimic = [
"isaaclab-mimic",
"ipywidgets>=8.1.5",
Expand Down Expand Up @@ -166,7 +166,7 @@ torch = "2.11.0"
torchvision = "0.26.0"
torchaudio = "2.11.0"
ovphysx = "0.5.2+head.f62c22207c"
ovrtx = ">=0.3.0,<0.4.0"
ovrtx = ">=0.4.0,<0.5.0"
# Newton git commit + matching warp-lang prerelease (upgraded together).
newton = "c7ae7c7648cd0717df39e5c94b95d5a02c997320"
warp = "1.15.0.dev20260626"
Expand Down
11 changes: 11 additions & 0 deletions source/isaaclab_ov/changelog.d/esekkin-ovrtx-bump-04.minor.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Changed
^^^^^^^

* Updated the optional OVRTX runtime dependency to ``ovrtx>=0.4.0,<0.5.0``. Reinstall the OVRTX
extra with ``./isaaclab.sh -i 'ov[ovrtx]'`` to use the supported 0.4 runtime.

Fixed
^^^^^

* Worked around OVRTX 0.4 tiled RenderProducts collapsing to one camera after runtime cloning by
preserving camera prims in trimmed stage exports.
31 changes: 18 additions & 13 deletions source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_usd.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,18 +267,18 @@ def create_scene_partition_attributes(
logger.debug("Set scene partition '%s' on '%s'", scene_partition, attr_path.GetPrimPath())


def _collect_prims_to_deactivate(parent_prim: Usd.Prim, source_paths: frozenset[Sdf.Path]) -> list[Sdf.Path]:
def _collect_prims_to_deactivate(parent_prim: Usd.Prim, preserve_paths: frozenset[Sdf.Path]) -> list[Sdf.Path]:
"""Collect child prims under ``parent_prim`` for deactivation.

For each child:

* If the child is a source, keep the full subtree and stop descending.
* If the child is an ancestor of some source, recurse to deactivate non-source siblings deeper in the tree.
* If the child is preserved, keep the full subtree and stop descending.
* If the child is an ancestor of a preserved path, recurse to deactivate non-preserved siblings.
* Otherwise, deactivate the child prim (including descendants).

Args:
parent_prim: Parent prim whose children are considered.
source_paths: The paths to the cloning sources.
preserve_paths: Source and camera paths to preserve in the trimmed stage.

Returns:
Paths of prims to deactivate on the root layer.
Expand All @@ -288,13 +288,13 @@ def _collect_prims_to_deactivate(parent_prim: Usd.Prim, source_paths: frozenset[
for child in parent_prim.GetChildren():
child_path = child.GetPath()

# If the child is a source, keep it and stop walking down the tree.
if child_path in source_paths:
# If the child is preserved, keep it and stop walking down the tree.
if child_path in preserve_paths:
continue

# If the child is an ancestor of some source, recurse to deactivate non-source siblings deeper in the tree.
if any(source.HasPrefix(child_path) for source in source_paths):
prim_paths.extend(_collect_prims_to_deactivate(child, source_paths))
# Preserve the ancestor chain while deactivating unrelated descendants.
if any(path.HasPrefix(child_path) for path in preserve_paths):
prim_paths.extend(_collect_prims_to_deactivate(child, preserve_paths))
continue

# Otherwise, deactivate the child prim (including descendants).
Expand Down Expand Up @@ -347,14 +347,19 @@ def export_stage_to_string(stage: Usd.Stage, num_envs: int, source_paths: tuple[
raise RuntimeError(f"Failed to get prim at path: {envs_path}")

source_path_set = frozenset(map(Sdf.Path, source_paths))
camera_path_set = frozenset(prim.GetPath() for prim in Usd.PrimRange(envs_prim) if prim.IsA(UsdGeom.Camera))
# OVRTX 0.4 resolves a RenderProduct's Fabric camera relationship during initial population and does not add
# cameras created later by clone_usd. Keep all cameras and their ancestor chains active in the trimmed stage so
# tiled RenderProducts discover every camera while preserving the optimized runtime-cloning path.
preserve_path_set = source_path_set | camera_path_set
prim_paths: list[Sdf.Path] = []

for child in envs_prim.GetChildren():
# All env roots will be kept in the stage. If an env root is a source, keep the full subtree and don't walk down
# the subtree, otherwise walk down the subtree to collect descendant prims that are not sources to deactivate.
# All env roots remain in the stage. Keep full source subtrees and camera paths,
# while trimming other descendants.
child_path = child.GetPath()
if child_path not in source_path_set:
prim_paths.extend(_collect_prims_to_deactivate(child, source_path_set))
if child_path not in preserve_path_set:
prim_paths.extend(_collect_prims_to_deactivate(child, preserve_path_set))

root_layer = stage.GetRootLayer()

Expand Down
10 changes: 6 additions & 4 deletions source/isaaclab_ov/test/test_ovrtx_clone_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,16 @@ def _make_multi_env_stage(num_envs: int) -> Usd.Stage:
return stage


def _assert_export_contains_env_roots_and_children(exported: str, env_indices: range | list[int]) -> None:
def _assert_export_contains_env_roots_and_children(
exported: str, env_indices: range | list[int], camera_count: int | None = None
) -> None:
"""Listed environment roots appear in the stage export."""
for env_idx in env_indices:
assert f'def Xform "env_{env_idx}"' in exported
assert f'def Xform "Object_env{env_idx}_only"' in exported

assert exported.count('def Xform "Robot"') == len(env_indices)
assert exported.count('def Camera "Camera"') == len(env_indices)
assert exported.count('def Camera "Camera"') == (len(env_indices) if camera_count is None else camera_count)


def _assert_export_contains_env_roots_but_omits_children(exported: str, env_indices: range | list[int]) -> None:
Expand Down Expand Up @@ -437,6 +439,6 @@ def test_prepare_stage_stores_clone_plan_and_exports(monkeypatch: pytest.MonkeyP
assert renderer._clone_plan is not None
assert renderer._clone_plan.sources == published.sources

# Only the env_0 prototype subtree is exported.
_assert_export_contains_env_roots_and_children(renderer._exported_usd_string, [0])
# Only the env_0 prototype geometry is exported, while all cameras remain discoverable.
_assert_export_contains_env_roots_and_children(renderer._exported_usd_string, [0], camera_count=num_envs)
_assert_export_contains_env_roots_but_omits_children(renderer._exported_usd_string, [1, 2, 3])
36 changes: 30 additions & 6 deletions source/isaaclab_ov/test/test_ovrtx_usd.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,16 @@ def _make_multi_env_stage(num_envs: int) -> Usd.Stage:
return stage


def _assert_export_contains_env_roots_and_children(exported: str, env_indices: range | list[int]) -> None:
def _assert_export_contains_env_roots_and_children(
exported: str, env_indices: range | list[int], camera_count: int | None = None
) -> None:
"""Listed environment roots appear in the stage export."""
for env_idx in env_indices:
assert f'def Xform "env_{env_idx}"' in exported
assert f'def Xform "Object_env{env_idx}_only"' in exported

assert exported.count('def Xform "Robot"') == len(env_indices)
assert exported.count('def Camera "Camera"') == len(env_indices)
assert exported.count('def Camera "Camera"') == (len(env_indices) if camera_count is None else camera_count)


def _assert_export_omits_env_children(exported: str, env_indices: range | list[int]) -> None:
Expand Down Expand Up @@ -226,8 +228,8 @@ def test_export_stage_full_when_single_env():
_assert_export_contains_env_roots_and_children(exported, range(num_envs))


def test_export_stage_homogeneous_keeps_only_env0_prototype():
"""Homogeneous cloning exports only the env_0 prototype subtree."""
def test_export_stage_homogeneous_keeps_env0_prototype_and_all_cameras():
"""Homogeneous cloning exports the env_0 prototype subtree and every camera."""
num_envs = 4
stage = _make_multi_env_stage(num_envs)

Expand All @@ -237,10 +239,32 @@ def test_export_stage_homogeneous_keeps_only_env0_prototype():
source_paths=("/World/envs/env_0",),
)

_assert_export_contains_env_roots_and_children(exported, [0])
_assert_export_contains_env_roots_and_children(exported, [0], camera_count=num_envs)
_assert_export_omits_env_children(exported, range(1, num_envs))


def test_export_stage_preserves_nested_camera_ancestor_chains():
"""Trimming keeps nested cameras and their ancestors while omitting unrelated siblings."""
num_envs = 4
stage = _make_multi_env_stage(num_envs)
for env_idx in range(num_envs):
robot_path = f"/World/envs/env_{env_idx}/Robot"
UsdGeom.Xform.Define(stage, f"{robot_path}/Sensors")
UsdGeom.Camera.Define(stage, f"{robot_path}/Sensors/HeadCamera")
UsdGeom.Xform.Define(stage, f"{robot_path}/UnrelatedVisual")

exported = export_stage_to_string(
stage,
num_envs,
source_paths=("/World/envs/env_0",),
)

assert exported.count('def Xform "Robot"') == num_envs
assert exported.count('def Xform "Sensors"') == num_envs
assert exported.count('def Camera "HeadCamera"') == num_envs
assert exported.count('def Xform "UnrelatedVisual"') == 1


def test_export_stage_heterogeneous_keeps_multiple_sources():
"""Heterogeneous source paths export only prototype env subtrees."""
num_envs = 4
Expand All @@ -261,7 +285,7 @@ def test_export_stage_heterogeneous_keeps_multiple_sources():
# Other env roots remain, but their prototype children are omitted.
_assert_export_omits_env_children(exported, [1, 2])
assert 'def Xform "Robot"' not in exported
assert 'def Camera "Camera"' not in exported
assert exported.count('def Camera "Camera"') == num_envs


def test_export_stage_restores_active_state():
Expand Down
Loading