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
16 changes: 7 additions & 9 deletions .github/workflows/license-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,13 @@ jobs:

# If the package is in the exceptions list
if [ -n "$EXCEPTION" ]; then
# If the license is provided in the exceptions list, check the license
EXCEPTION_LICENSE=$(echo "$EXCEPTION" | jq -r '.license')

# echo "Comparing licenses for $pkg:"
# echo " EXCEPTION_LICENSE='${EXCEPTION_LICENSE}' (len=${#EXCEPTION_LICENSE})"
# echo " LICENSE='${LICENSE}' (len=${#LICENSE})"

# If the exceptions list has a license and doesn't match the current license
if [ "$EXCEPTION_LICENSE" != "null" ] && [ "$EXCEPTION_LICENSE" != "$LICENSE" ]; then
# If licenses are provided in the exception, require the primary value or an alias.
LICENSE_MATCH=$(echo "$EXCEPTION" | jq -r --arg license "$LICENSE" '
(.license == null)
or (.license == $license)
or any(.license_aliases[]?; . == $license)
')
if [ "$LICENSE_MATCH" != "true" ]; then
echo "ERROR: $pkg has license: $LICENSE"
FAILED_PACKAGES=$((FAILED_PACKAGES + 1)) # Increment the counter
fi
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/license-exceptions.json
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,9 @@
"package": "ovrtx",
"package_type": "Python",
"license": "NVIDIA Proprietary Software",
"license_aliases": [
"LicenseRef-NvidiaProprietary"
],
"usage": "runtime_dependency",
"interaction": "same_process_dynamic",
"linkage": "dynamic",
Expand Down
2 changes: 1 addition & 1 deletion docs/_extensions/isaaclab_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ def run(self) -> list[nodes.Node]:
content = f"""\
.. code-block:: bash

pip install --extra-index-url https://pypi.nvidia.com "ovrtx{spec}"
pip install "ovrtx{spec}"
"""
return _parse_rst(self, content)

Expand Down
3 changes: 2 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ def _read_pinned_versions() -> dict:
isaacsim_version = _pinned_versions["isaacsim"]
torch_version = _pinned_versions["torch"]
torchvision_version = _pinned_versions["torchvision"]
ovrtx_spec = _pinned_versions["ovrtx"]
ovrtx_version = _pinned_versions["ovrtx"]
ovrtx_spec = f"=={ovrtx_version}" if ovrtx_version[0].isdigit() else ovrtx_version
ovphysx_version = _pinned_versions["ovphysx"]

# Short version strings used in external documentation URLs and badges.
Expand Down
6 changes: 3 additions & 3 deletions docs/source/overview/core-concepts/renderers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ Installing the OVRTX renderer

The OVRTX renderer is provided by the ``isaaclab_ov`` extension. The extension's
source package ships with the core install, but the renderer's ``ovrtx`` runtime
wheel (the `ovrtx <https://github.com/NVIDIA-Omniverse/ovrtx>`_ package, hosted on
``pypi.nvidia.com``) is **not** installed by default. You must request it
wheel (the `ovrtx <https://github.com/NVIDIA-Omniverse/ovrtx>`_ package, published
on public PyPI) is **not** installed by default. You must request it
explicitly — OVRTX does **not** require Isaac Sim.

Install via the Isaac Lab CLI using the ``ov[ovrtx]`` token:
Expand All @@ -133,7 +133,7 @@ Install via the Isaac Lab CLI using the ``ov[ovrtx]`` token:
packages are already part of the core install). Use ``ov[ovrtx]`` (or ``ov[all]``)
to pull in the ``ovrtx`` dependency.

Or install the ``ovrtx`` runtime wheel directly with pip (note the extra index URL):
Or install the public ``ovrtx`` package directly from PyPI:
Comment thread
nvsekkin marked this conversation as resolved.

.. isaaclab-ovrtx-install::

Expand Down
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,9 @@ rerun = [

isaacsim = ["isaacsim[all,extscache]==6.0.1.0"]

ov = ["ovphysx==0.5.9", "ovrtx>0.4.0,<0.4.1", "ovstage==0.1.0.346039"]
ov = ["ovphysx==0.5.9", "ovrtx==0.4.1.364340", "ovstage==0.1.0.346039"]
ovphysx = ["ovphysx==0.5.9", "ovstage==0.1.0.346039"]
ovrtx = ["ovrtx>0.4.0,<0.4.1", "ovstage==0.1.0.346039"]
ovrtx = ["ovrtx==0.4.1.364340", "ovstage==0.1.0.346039"]

mimic = [
"isaaclab-mimic",
Expand Down Expand Up @@ -212,7 +212,7 @@ torch = "2.11.0"
torchvision = "0.26.0"
torchaudio = "2.11.0"
ovphysx = "0.5.9"
ovrtx = ">0.4.0,<0.4.1"
ovrtx = "0.4.1.364340"
ovstage = "0.1.0.346039"
newton = "release-1.5"
warp = "1.16.0"
Expand Down Expand Up @@ -417,6 +417,7 @@ package = false
[tool.uv.sources]
omniverseclient = { index = "pypi-public" }
ovphysx = { index = "pypi-public" }
ovrtx = { index = "pypi-public" }
ovstage = { index = "pypi-public" }
isaaclab = { path = "source/isaaclab", editable = true }
"isaaclab-assets" = { path = "source/isaaclab_assets", editable = true }
Expand Down
1 change: 1 addition & 0 deletions source/isaaclab/changelog.d/esekkin-ovrtx-041.skip
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Test-only: verified that the public OVRTX package resolves through the public PyPI index.
6 changes: 2 additions & 4 deletions source/isaaclab/test/cli/test_uv_run_pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,7 @@ def spec(package: str) -> str:
line.strip() for line in build_workflow.splitlines() if "extra-pip-packages:" in line and "ovrtx" in line
]
assert ovrtx_install_lines
assert all(
f"ovrtx{versions['ovrtx']}" in line or "steps.ov_pins.outputs.ovrtx" in line for line in ovrtx_install_lines
)
assert all(spec("ovrtx") in line or "steps.ov_pins.outputs.ovrtx" in line for line in ovrtx_install_lines)

# uv torch-stack overrides mirror the table.
for package in ("torch", "torchvision", "torchaudio"):
Expand All @@ -195,7 +193,7 @@ def test_public_ov_packages_use_public_pypi_index():
"url": "https://pypi.org/simple",
"explicit": True,
}
for package in ("omniverseclient", "ovphysx", "ovstage"):
for package in ("omniverseclient", "ovphysx", "ovrtx", "ovstage"):
assert sources[package] == {"index": "pypi-public"}


Expand Down
6 changes: 6 additions & 0 deletions source/isaaclab_ov/changelog.d/esekkin-ovrtx-041.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Changed
^^^^^^^

* Updated the optional OVRTX runtime dependency to the public ``ovrtx==0.4.1.364340`` package and
enabled synchronous texture streaming for deterministic material readiness. Reinstall the OVRTX
extra with ``uv sync --extra ovrtx`` to use the supported runtime.
63 changes: 13 additions & 50 deletions source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,15 @@
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 --extra-index-url https://pypi.nvidia.com "
"'ovrtx>0.4.0,<0.4.1')."
"(or, manually: python -m pip install 'ovrtx==0.4.1.364340')."
) from exc

from isaaclab.cloner.clone_plan import ClonePlan
Expand Down Expand Up @@ -376,14 +376,9 @@ def __init__(self, cfg: OVRTXRendererCfg):
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,
)
# Isaac Lab still drives ovrtx's legacy stage API until the ovstage path is the default, so
# its deprecation warnings are noise no user of this renderer can act on. Set after
# construction because ``RendererConfig`` is a plain dataclass and wheels predating the
# option would reject it as an unexpected keyword argument.
# TODO: Remove this once the ovstage path is the default and the legacy stage API is removed.
if hasattr(OVRTX_CONFIG, "suppress_deprecation_warnings"):
OVRTX_CONFIG.suppress_deprecation_warnings = True

self._renderer = Renderer(OVRTX_CONFIG)
if not self._renderer:
Expand Down Expand Up @@ -450,15 +445,14 @@ 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 four ``bind_attribute``/``bind_array_attribute`` handles and the
``UsdGeom.Points`` seeding flag. State shared by both paths (``_object_newton_indices``, the
particle offset/count lists) stays in :meth:`__init__`.
here: the four ``bind_attribute``/``bind_array_attribute`` handles. 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._deformable_points_binding = None
self._particle_points_binding = None
self._particle_workaround_applied = False

def _initialize_from_spec_legacy(self, spec: CameraRenderSpec):
"""Initialize the OVRTX renderer with internal environment cloning.
Expand Down Expand Up @@ -799,8 +793,6 @@ def _setup_particle_bindings_legacy(self) -> None:
flags=BindingFlag.OPTIMIZE,
)

self._particle_workaround_applied = False

def create_render_data(self, spec: CameraRenderSpec) -> OVRTXRenderData:
"""Create OVRTX-specific RenderData with GPU buffers.

Expand Down Expand Up @@ -897,16 +889,12 @@ def _update_geometries_legacy(self) -> None:
)

if self._particle_points_binding is not None:
if not self._particle_workaround_applied:
self._apply_particle_workaround(particle_q)
self._particle_workaround_applied = True
else:
self._write_particle_q_slices(
self._particle_points_binding,
particle_q,
self._particle_visual_offsets,
self._particle_visual_counts,
)
self._write_particle_q_slices(
self._particle_points_binding,
particle_q,
self._particle_visual_offsets,
self._particle_visual_counts,
)

def _write_particle_q_slices(
self,
Expand Down Expand Up @@ -942,24 +930,6 @@ def _write_particle_q_slices(
cuda_stream=cuda_stream,
)

def _apply_particle_workaround(self, particle_q: wp.array) -> None:
"""Host-SYNC seed ``UsdGeom.Points`` so later GPU ASYNC writes can work correctly.

OVRTX does not initialize UsdGeom.Points prims from a GPU ASYNC ``points`` write as expected.
A host SYNC write + a renderer step call are needed to finish initialization; later frames
can then use zero-copy GPU ASYNC write with the same binding.

TODO: The workaround will be removed when OVRTX fixes the issue (OMPE-102610).
"""
particle_q_host = particle_q.numpy()
host_slices = [
particle_q_host[particle_offset : particle_offset + particle_count]
for particle_offset, particle_count in zip(
self._particle_visual_offsets, self._particle_visual_counts, strict=True
)
]
self._particle_points_binding.write(cast(Any, host_slices), data_access=DataAccess.SYNC)

def _update_camera_legacy(
self,
render_data: OVRTXRenderData,
Expand Down Expand Up @@ -1425,7 +1395,6 @@ def _safe_unbind(binding, name: str) -> None:
self._deformable_particle_counts = []
self._particle_visual_offsets = []
self._particle_visual_counts = []
self._particle_workaround_applied = False

if self._renderer:
try:
Expand Down Expand Up @@ -1942,12 +1911,6 @@ def _setup_particle_bindings_ovstage(self) -> None:
if self._particle_points_query is None:
raise RuntimeError("Failed to create OVRTX particle point bindings")

# Note: no ``UsdGeom.Points`` seeding workaround is needed here. The legacy path's
# ``_apply_particle_workaround`` (OMPE-102610) exists because OVRTX fails to initialize
# Points prims from a zero-copy GPU ASYNC ``points`` write through ``bind_array_attribute``.
# The ovstage path instead writes host numpy DLTensors through ``Stage.write_attribute``,
# which populates the column synchronously, so the prims are valid from the first frame.

def _update_transforms_ovstage(self) -> None:
if self._object_xform_query is None or self._object_newton_indices is None:
return
Expand Down
40 changes: 10 additions & 30 deletions source/isaaclab_ov/test/test_ovrtx_deformable_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ def _make_renderer_without_backend(device: str = "cpu") -> tuple[OVRTXRenderer,
renderer._particle_points_binding = None
renderer._particle_visual_offsets = []
renderer._particle_visual_counts = []
renderer._particle_workaround_applied = False
renderer._use_ovstage = False
return renderer, renderer._renderer

Expand Down Expand Up @@ -345,7 +344,6 @@ def test_setup_particle_points_bindings_binds_mpm_visual_prims(monkeypatch: pyte
assert backend.calls[0]["attribute_name"] == "points"
assert backend.calls[0]["flags"] is BindingFlag.OPTIMIZE
assert renderer._particle_points_binding is backend.bindings["points"]
assert renderer._particle_workaround_applied is False
assert renderer._particle_visual_offsets == [10, 15]
assert renderer._particle_visual_counts == [5, 5]
assert len(backend.writes) == 2
Expand Down Expand Up @@ -378,13 +376,12 @@ def test_setup_particle_points_bindings_binds_multiple_mpm_assets(monkeypatch: p
assert renderer._particle_visual_counts == [5, 5, 3, 3]


def test_update_particle_points_primes_with_host_sync_then_gpu_async(monkeypatch: pytest.MonkeyPatch):
"""First MPM ``points`` update host-SYNC primes via binding; later frames use GPU ASYNC."""
def test_update_particle_points_writes_world_particle_positions(monkeypatch: pytest.MonkeyPatch):
"""The first MPM ``points`` update writes world-space positions through GPU ASYNC."""
renderer, backend = _make_renderer_without_backend()
renderer._particle_points_binding = _FakePointsBinding("points")
renderer._particle_visual_offsets = [2]
renderer._particle_visual_counts = [2]
renderer._particle_workaround_applied = False
particle_q = wp.array(
[
wp.vec3f(0.0, 0.0, 0.0),
Expand All @@ -404,40 +401,29 @@ class _FakeStream:

renderer.update_geometries()

assert renderer._particle_workaround_applied is True
assert len(backend.writes) == 0
written = renderer._particle_points_binding.written
assert written is not None
assert len(written) == 1
assert isinstance(written[0], np.ndarray)
assert written[0].tolist() == [
assert written[0].ptr == particle_q[2:4].ptr
assert written[0].numpy().tolist() == [
[2.0, 3.0, 4.0],
[5.0, 6.0, 7.0],
]
assert renderer._particle_points_binding.write_kwargs["data_access"] is DataAccess.SYNC
assert "cuda_stream" not in renderer._particle_points_binding.write_kwargs

renderer.update_geometries()

written = renderer._particle_points_binding.written
assert written is not None
assert len(written) == 1
assert written[0].ptr == particle_q[2:4].ptr
assert renderer._particle_points_binding.write_kwargs is not None
assert renderer._particle_points_binding.write_kwargs["data_access"] is DataAccess.ASYNC
assert renderer._particle_points_binding.write_kwargs["cuda_stream"] == 42
assert len(backend.writes) == 0


def test_update_geometries_writes_deformable_and_mpm_bindings(monkeypatch: pytest.MonkeyPatch):
"""Deformable mesh stays GPU ASYNC; MPM primes once with host SYNC then switches to ASYNC."""
"""Deformable and MPM points use GPU ASYNC writes from the first update."""
renderer, backend = _make_renderer_without_backend()
renderer._deformable_points_binding = _FakePointsBinding("deformable_points")
renderer._deformable_particle_offsets = [0]
renderer._deformable_particle_counts = [2]
renderer._particle_points_binding = _FakePointsBinding("points")
renderer._particle_visual_offsets = [2]
renderer._particle_visual_counts = [2]
renderer._particle_workaround_applied = False
particle_q = wp.array(
[
wp.vec3f(0.0, 0.0, 0.0),
Expand All @@ -461,21 +447,15 @@ class _FakeStream:
assert deformable_written is not None
assert len(deformable_written) == 1
assert deformable_written[0].ptr == particle_q[0:2].ptr
assert renderer._deformable_points_binding.write_kwargs is not None
assert renderer._deformable_points_binding.write_kwargs["data_access"] is DataAccess.ASYNC
assert renderer._deformable_points_binding.write_kwargs["cuda_stream"] == 42

assert renderer._particle_workaround_applied is True
assert len(backend.writes) == 0
mpm_written = renderer._particle_points_binding.written
assert mpm_written is not None
assert isinstance(mpm_written[0], np.ndarray)
assert mpm_written[0].tolist() == [[2.0, 3.0, 4.0], [5.0, 6.0, 7.0]]
assert renderer._particle_points_binding.write_kwargs["data_access"] is DataAccess.SYNC

renderer.update_geometries()

mpm_written = renderer._particle_points_binding.written
assert mpm_written is not None
assert len(mpm_written) == 1
assert mpm_written[0].ptr == particle_q[2:4].ptr
assert renderer._particle_points_binding.write_kwargs is not None
assert renderer._particle_points_binding.write_kwargs["data_access"] is DataAccess.ASYNC
assert len(backend.writes) == 0
assert renderer._particle_points_binding.write_kwargs["cuda_stream"] == 42
Loading
Loading