Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3795b51
Add renderer leases and the OVXR host adapter
hougantc-nvda Oct 30, 2026
614c319
Limit changes to the kitless feature scope
hougantc-nvda Oct 31, 2026
804868f
Defer partition-visibility export change to its consumer
hougantc-nvda Oct 31, 2026
7b3e5b3
Keep poisoned render leases retirable
hougantc-nvda Oct 31, 2026
2342d8f
Split OVXR config and type the anchor contract
hougantc-nvda Oct 31, 2026
2551f65
Mock Isaac Capture for the OVXR session API docs
hougantc-nvda Oct 31, 2026
bef60fa
Run OVXR config tests without Isaac Capture
hougantc-nvda Oct 31, 2026
48d1297
Document that OVRTX material sync requires CUDA
hougantc-nvda Oct 31, 2026
a02fbf4
Validate the OVXR environment count and device before the claim
hougantc-nvda Oct 31, 2026
8776b78
Require the claimed owner transfer before binding the OVXR Stage
hougantc-nvda Oct 31, 2026
ab024b5
Drop the XrAnchorManager claim from the anchor protocol docstring
hougantc-nvda Oct 31, 2026
00ee188
Sort imports in the OVXR adapter test
hougantc-nvda Oct 31, 2026
cc90610
Let a missing Isaac Capture raise its original import error
hougantc-nvda Nov 1, 2026
7314947
Document and close the OVStage scene publisher contract
hougantc-nvda Nov 1, 2026
9577aef
Keep CPU OVRTX support out of the renderer lease
hougantc-nvda Nov 1, 2026
2982179
Keep the async write buffers in the OVRTX renderer module
hougantc-nvda Nov 1, 2026
2888357
Keep the upstream material drain comment and names
hougantc-nvda Nov 1, 2026
452a41a
Say when a leased material writer compiles its addresses
hougantc-nvda Nov 1, 2026
c3434a3
Drop hidden visual prims from the OVXR adapter
hougantc-nvda Nov 1, 2026
dc4b12f
Drop a later layer's attribute from the OVXR adapter test fake
hougantc-nvda Nov 1, 2026
b5a8dc8
Rename OvLibrariesXrConfig to OvLibrariesXrCfg
hougantc-nvda Nov 1, 2026
e845280
Drop the repeated word from two render-lease test names
hougantc-nvda Nov 1, 2026
04f2716
Read the environment's simulation configuration directly
hougantc-nvda Nov 1, 2026
b2d4e47
Import NumPy, Warp and SceneDataFormat at module level
hougantc-nvda Nov 1, 2026
58bd756
Resolve a bare CUDA device through one OVXR session helper
hougantc-nvda Nov 1, 2026
dfbdf32
Document the render lease's effect on OVRTX clone routing
hougantc-nvda Nov 1, 2026
3cc5aa1
Put the material writer's pending-update property after its constructor
hougantc-nvda Nov 1, 2026
5af4912
Cover the scene publisher's component and authority rules
hougantc-nvda Nov 1, 2026
3c06330
Cover OVXR session preparation checks in the adapter tests
hougantc-nvda Nov 1, 2026
6e9caf6
Describe the OVXR adapter's ownership and dependencies in the README
hougantc-nvda Nov 1, 2026
b85423d
Add an opt-in native harness for the OVRTX owner transfer
hougantc-nvda Nov 1, 2026
1329d99
Merge upstream main into kitless borrowed renderer
hougantc-nvda Nov 2, 2026
d48f5d0
Export the leased scene publisher and type the lease controller
hougantc-nvda Nov 2, 2026
0789f6b
Validate the OVXR anchor reference path and use configclass for the a…
hougantc-nvda Nov 2, 2026
b4e5381
Test OVXR transfer cancellation through the real bootstrap constructor
hougantc-nvda Nov 2, 2026
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
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ def _read_pinned_versions() -> dict:
"ipywidgets",
"mpl_toolkits",
"isaacteleop",
"isaaccapture",
"scipy",
"hydra",
"hydra.core",
Expand Down
4 changes: 4 additions & 0 deletions docs/source/api/lab_ov/isaaclab_ov.cloner.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ both OVPhysX and Newton. OVPhysX also retains native physics cloning.
``0`` selects native OVRTX cloning. Set the flag before creating the simulation. Reset retains the
selected route. This flag alone never enables physics/rendering stage sharing.

An active :class:`~isaaclab_ov.renderers.OvLibrariesRenderLease` makes OVStage rendering the
unset default for every OVRTX renderer in that simulation, and rejects
``ISAAC_LAB_OVRTX_USE_OVSTAGE=0`` and ``ISAAC_LAB_SHARE_OVSTAGE=1``.

To share one OVStage with OVPhysX and OVRTX, set ``ISAAC_LAB_SHARE_OVSTAGE=1``. Sharing requires
OVPhysX and selects OVStage rendering regardless of ``ISAAC_LAB_OVRTX_USE_OVSTAGE``.

Expand Down
20 changes: 20 additions & 0 deletions docs/source/api/lab_ov/isaaclab_ov.renderers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,36 @@ The following classes are part of the public :mod:`isaaclab_ov.renderers` API.
OVRTXRenderer
OVRTXRendererCfg
OVRTXBackendCfg
OvLibrariesRenderLease
OvLibrariesScenePublisher

.. autoclass:: OVRTXRenderer
:show-inheritance:
:members: acquire_ov_libraries_render_lease

.. autoclass:: OVRTXRendererCfg
:show-inheritance:

.. autoclass:: OVRTXBackendCfg
:show-inheritance:

Renderer leases
---------------

An external consumer, such as the OVXR teleoperation adapter, can borrow one exact OVRTX
renderer and its private OVStage with :meth:`OVRTXRenderer.acquire_ov_libraries_render_lease`
before clone preparation. While the lease is active, ordinary camera rendering, scene writes and
renderer/stage destruction are rejected; eager scene updates are left to the lease's scene
publisher, which :meth:`OvLibrariesRenderLease.capture_scene_publisher` returns. A failed leased
publication poisons publication only; the borrower can still retire the pair, after which the
renderer must be recreated.

.. autoclass:: OvLibrariesRenderLease
:members:

.. autoclass:: OvLibrariesScenePublisher
:members:

Simulation-owned stages
-----------------------

Expand Down
11 changes: 11 additions & 0 deletions docs/source/api/lab_teleop/isaaclab_teleop.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ isaaclab_teleop
XrCfg
XrAnchorRotationMode
XrAnchorSynchronizer
OvLibrariesXrCfg
OvLibrariesXrSession

.. rubric:: Functions

Expand Down Expand Up @@ -61,6 +63,15 @@ XR Camera Feedback
.. autoclass:: XrCameraFeedSession
:members:

Kitless OVXR Session
--------------------

.. autoclass:: OvLibrariesXrCfg
:members:

.. autoclass:: OvLibrariesXrSession
:members:

Device
------

Expand Down
6 changes: 6 additions & 0 deletions source/isaaclab_ov/changelog.d/renderer-lease.added.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
* Added :class:`~isaaclab_ov.renderers.OvLibrariesRenderLease` and
:meth:`~isaaclab_ov.renderers.OVRTXRenderer.acquire_ov_libraries_render_lease` for application-owned
OVRTX/OVStage borrowing by external consumers, with private-stage selection, capability-checked
render-thread ownership transfer, deferred Stage materialization, scene publication through
:class:`~isaaclab_ov.renderers.OvLibrariesScenePublisher`, and ordered cleanup.
* Published dirty visual materials with leased scene updates, including material-only changes.
Empty file.
8 changes: 7 additions & 1 deletion source/isaaclab_ov/isaaclab_ov/cloner/replicate.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ def prepare(sim: SimulationContext, routing: dict[type[cloner.ReplicateContext],
renderers = sim.get_backends(OVRTXRendererCfg)
if not renderers:
return
leased = [renderer for renderer in renderers if renderer._render_lease.active is not None]
initialized = next((renderer for renderer in renderers if renderer.backend is not None), None)
if initialized is not None:
use_ovstage = initialized._use_ovstage
Expand All @@ -185,13 +186,16 @@ def prepare(sim: SimulationContext, routing: dict[type[cloner.ReplicateContext],
# Default to native OVRTX cloning to preserve camera-task throughput on both physics backends.
# Keep sharing opt-in until OVPhysX cold binding and shared-stage update performance are fixed.
with_ovphysx = sim.backend_manager.clone_context_type is OvPhysxReplicateContext
value = os.environ.get("ISAAC_LAB_OVRTX_USE_OVSTAGE", "0").strip()
# An explicit lease selects its private OVStage without changing process globals.
value = os.environ.get("ISAAC_LAB_OVRTX_USE_OVSTAGE", "1" if leased else "0").strip()
if value not in {"0", "1"}:
raise ValueError(f"Invalid ISAAC_LAB_OVRTX_USE_OVSTAGE: {value!r}. Expected 0 or 1.")
share_ovstage = os.environ.get("ISAAC_LAB_SHARE_OVSTAGE") == "1"
use_ovstage = share_ovstage or value == "1"
if share_ovstage and not with_ovphysx:
raise ValueError("Shared OVStage cloning requires OVPhysX.")
if leased and (not use_ovstage or share_ovstage):
raise RuntimeError("The render lease requires a private rendering OVStage")
if share_ovstage and sim.backend_manager.backend is not None and sim.backend_manager.backend.owns_scene:
raise RuntimeError("Configure OVRTX cameras before the first OVPhysX reset to share its OVStage.")
if use_ovstage:
Expand Down Expand Up @@ -230,6 +234,8 @@ def prepare(sim: SimulationContext, routing: dict[type[cloner.ReplicateContext],
renderer.backend = sim.get_or_create_backend(backend_cfg)
renderer.scene = scene if use_ovstage else renderer.backend
renderer._use_ovstage = use_ovstage
if renderer in leased:
renderer._render_lease.reserve_pair()
if renderer.cfg.async_rendering and use_ovstage:
logger.warning("Asynchronous OVRTX rendering is unsupported with ovstage; rendering synchronously.")

Expand Down
3 changes: 3 additions & 0 deletions source/isaaclab_ov/isaaclab_ov/renderers/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ __all__ = [
"OVRTXRenderer",
"OVRTXRendererCfg",
"OVRTXBackendCfg",
"OvLibrariesRenderLease",
"OvLibrariesScenePublisher",
"Renderer",
]

from .ovrtx_renderer import OVRTXRenderer
from .ovrtx_renderer import OVRTXRenderer as Renderer
from .ovrtx_renderer_cfg import OVRTXBackendCfg, OVRTXRendererCfg
from .render_lease import OvLibrariesRenderLease, OvLibrariesScenePublisher
116 changes: 94 additions & 22 deletions source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
its own pending ``step_async`` render operation and publishes it at the next ``read_output``.
The ovrtx binding write operations retain their input buffers until ovrtx has consumed them.

- **render_lease.py**: External borrowing, ownership transfer and scene publication ordering.

- **ovrtx_renderer_kernels.py**: Warp GPU kernels for OVRTX rendering pipeline.

- **ovrtx_usd.py**: USD helpers for OVRTX: render var config, camera injection, etc.
Expand Down Expand Up @@ -107,6 +109,8 @@
xform_tensor_from_warp,
)

from .render_lease import OvLibrariesRenderLease, RenderLeaseController

if TYPE_CHECKING:
from isaaclab_ppisp import PpispPipeline
from ovrtx import AttributeBinding, FrameOutput, Operation, PendingFetch, RenderVarOutput
Expand Down Expand Up @@ -213,6 +217,7 @@ class OVRTXBackend:
def __init__(self, cfg: OVRTXBackendCfg):
self.cfg = cfg
self.attached = False
self._render_lease_owner: OVRTXRenderer | None = None
self.render_products: set[str] = set()
# Resolve the wheel's native dependency here, including callers without a viewer.
dependency = Path(ovstage.__file__).parent / "bin/plugins/libosdCPU.so.3.6.0"
Expand Down Expand Up @@ -242,13 +247,25 @@ def __init__(self, cfg: OVRTXBackendCfg):
self.renderer = Renderer(native_cfg)

def detach(self) -> None:
"""Release the borrowed stage before it is rebuilt or destroyed."""
"""Release the borrowed stage before it is rebuilt or destroyed.

Raises:
RuntimeError: If an :class:`~isaaclab_ov.renderers.OvLibrariesRenderLease` is active.
"""
if self._render_lease_owner is not None:
raise RuntimeError("Cannot detach a backend with an active render lease")
if self.attached:
self.renderer.detach_ovstage()
self.attached = False

def close(self) -> None:
"""Destroy the engine; the simulation's backend registry releases any borrowed stage afterward."""
"""Destroy the engine; the simulation's backend registry releases any borrowed stage afterward.

Raises:
RuntimeError: If an :class:`~isaaclab_ov.renderers.OvLibrariesRenderLease` is active.
"""
if self._render_lease_owner is not None:
raise RuntimeError("Cannot destroy a backend with an active render lease")
if self.renderer is not None:
# The SDK destroys bindings and detaches only if attached, including partial initialization.
self.renderer.destroy()
Expand Down Expand Up @@ -334,6 +351,8 @@ def supported_output_types(self) -> dict[RenderBufferKind, RenderBufferSpec]:

def __init__(self, cfg: OVRTXRendererCfg, *, sim: SimulationContext):
super().__init__(cfg, sim=sim)
self._closed = False
self._render_lease = RenderLeaseController(self)
self._warp_device = wp.get_device(self._sim.device)
self._device = str(self._warp_device)
self._camera_render_data: list[OVRTXCameraRenderData] = []
Expand Down Expand Up @@ -366,17 +385,48 @@ def __init__(self, cfg: OVRTXRendererCfg, *, sim: SimulationContext):

@property
def visual_material_writer(self):
"""Return a writer factory once this renderer has a scene consumer."""
return self._create_visual_material_writer if self._initialized_scene else None
"""Return a writer factory once this renderer has a scene consumer.

A leased pair registers its writer before the borrower binds the Stage, because
environment reset finalizes material consumers first; addresses compile on the
first publication after population.
"""
return (
self._create_visual_material_writer
if self._initialized_scene or self._render_lease.active is not None
else None
)

def _create_visual_material_writer(self, batches: tuple[VisualMaterialBatch, ...]) -> OVRTXVisualMaterialWriter:
"""Create the detached-scene material writer after scene population."""
"""Retain material buffers; deferred scenes compile addresses after population."""
writer = OVRTXVisualMaterialWriter(self, batches)
self._visual_material_writer_ref = weakref.ref(writer)
return writer

@contextlib.contextmanager
def _publish_visual_materials(self) -> Iterator[None]:
"""Publish dirty materials and complete their reads before buffers may change."""
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()
yield
finally:
if material_writer is not None:
# When another exception is already propagating, log the drain failure instead of
# replacing it. Losing it silently would hide a failed material write.
primary_active = sys.exc_info()[0] is not None
try:
material_writer.drain()
except Exception as e:
if not primary_active:
raise
logger.warning("Error draining material writes after a failed scene update: %s", e, exc_info=True)

def prepare_stage(self, stage: Usd.Stage, num_envs: int) -> None:
"""Load, clone, and bind the shared scene before any camera products are registered."""
if not self._render_lease.prepare_stage(num_envs):
return
logger.info("Preparing stage (%d envs)...", num_envs)
env_paths = self.scene.clone_env_paths
if self.cfg.temp_usd_dir is not None or not self._use_ovstage or not self.scene.ordinal:
Expand Down Expand Up @@ -447,6 +497,11 @@ def prepare_stage(self, stage: Usd.Stage, num_envs: int) -> None:
self.scene.resources.callback(self.close)
self._initialized_scene = True
self._needs_transform_initialization = not self._use_ovstage and self.backend.cfg.read_gpu_transforms
self._render_lease.bind_stage()

def acquire_ov_libraries_render_lease(self) -> OvLibrariesRenderLease:
"""Reserve the native renderer/scene pair before stage preparation."""
return self._render_lease.acquire()

def _setup_xform_bindings(self, stage: Usd.Stage) -> None:
"""Bind SDP body paths and preserve their authored scales when writing world poses."""
Expand Down Expand Up @@ -556,6 +611,7 @@ def _setup_geometry_bindings(self) -> None:

def create_render_data(self, spec: CameraRenderSpec) -> OVRTXCameraRenderData:
"""Register a camera's product and bindings in the prepared scene."""
self._render_lease.reject_ordinary_operation("create ordinary camera render data")
if str(wp.get_device(spec.device)) != self._device:
raise ValueError("Cameras sharing an OVRTX renderer must use the simulation device.")
camera_paths = list(spec.camera_prim_paths)
Expand Down Expand Up @@ -982,6 +1038,17 @@ def _process_render_products(
)

def update_transforms(self) -> None:
"""Publish ordinary scene data when the native pair is not leased.

While a lease is active its scene publisher owns these writes, so eager scene
updates (for example ``lazy_sensor_update=False``) return without writing.
"""
if self._render_lease.owns_scene_updates():
return
self._render_lease.reject_ordinary_operation("update ordinary transforms")
self._update_transforms()

def _update_transforms(self) -> None:
"""Write changed SDP transforms to OVRTX."""
binding = self._object_xform_binding
if binding is None:
Expand Down Expand Up @@ -1014,6 +1081,16 @@ def update_transforms(self) -> None:
self._transforms_timestamp = timestamp

def update_geometries(self) -> None:
"""Publish ordinary scene data when the native pair is not leased.

While a lease is active its scene publisher owns these writes.
"""
if self._render_lease.owns_scene_updates():
return
self._render_lease.reject_ordinary_operation("update ordinary geometries")
self._update_geometries()

def _update_geometries(self) -> None:
"""Write changed SDP geometry to OVRTX."""
for callback in self.geometry_callbacks.values():
callback(self)
Expand Down Expand Up @@ -1069,6 +1146,7 @@ def update_camera(
intrinsics: ProxyArray,
) -> None:
"""Update camera poses using the camera's reusable conversion buffers."""
self._render_lease.reject_ordinary_operation("update ordinary cameras")
binding = render_data.camera_xform_binding
if binding is None:
return
Expand Down Expand Up @@ -1096,6 +1174,7 @@ def update_camera_intrinsics(self, render_data: OVRTXCameraRenderData, intrinsic
"""Publish calibration columns from GPU memory into the renderer-owned scene."""
# A runtime calibration change is a scene write for this camera's bindings, so its
# in-flight renders must finish first. Other cameras keep their pipelines.
self._render_lease.reject_ordinary_operation("update ordinary camera intrinsics")
errors = self.drain_pending_renders((render_data,))
if errors:
raise ExceptionGroup("OVRTX renders failed before calibration update", errors)
Expand Down Expand Up @@ -1126,6 +1205,7 @@ def render(self, render_data: OVRTXCameraRenderData) -> None:

def log_mesh(self, name: str, *args, **kwargs) -> None:
"""Publish a runtime mesh through the same scene used by every camera."""
self._render_lease.reject_ordinary_operation("log runtime meshes")
if name not in self.meshes:
self.meshes[name] = OVRTXMesh(self, f"/IsaacLabMeshes/{name.lstrip('/')}")
self.meshes[name].update(*args, **kwargs)
Expand All @@ -1147,15 +1227,13 @@ def render_batch(self, render_data: Sequence[OVRTXCameraRenderData]) -> None:
"""
if not render_data:
return
self._render_lease.reject_ordinary_operation("render an ordinary camera batch")
if not self._initialized_scene:
raise RuntimeError("Scene not initialized. Call initialize() first.")
if self.backend.renderer is None or not self._camera_render_data:
return
products = self.backend.render_products.copy()
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()
with self._publish_visual_materials():
if self._needs_transform_initialization:
# OVRTX 0.6 initializes GPU world transforms on its first step using
# authored poses. Flush that initialization before capturing runtime
Expand All @@ -1177,18 +1255,6 @@ def render_batch(self, render_data: Sequence[OVRTXCameraRenderData]) -> None:
else:
result = self.backend.renderer.step(render_products=products, delta_time=_RENDER_DELTA_TIME)
self._process_render_products(render_data, result)
finally:
if material_writer is not None:
# When another exception is already propagating, log the drain failure instead of
# replacing it. Losing it silently would hide a failed material write.
primary_active = sys.exc_info()[0] is not None
try:
material_writer.drain()
except Exception as e:
if not primary_active:
raise
logger.warning("Error draining material writes after a failed render: %s", e, exc_info=True)

if self._use_ovstage:
# Stage writes and material reads finish before rendering. Consume only after advancing
# the ordinal so a failed readback cannot leave later writes at the sealed floor.
Expand Down Expand Up @@ -1220,7 +1286,12 @@ def _remove_usd_reference(self, reference: int) -> None:
ovstage.population.apply_usd_changes(self.scene.stage, ordinal=self.scene.ordinal)

def close(self) -> None:
"""Complete borrowed-buffer reads and release bindings; the registry owns the native engine."""
"""Complete borrowed-buffer reads and release bindings; the registry owns the native engine.

Raises:
RuntimeError: If an :class:`~isaaclab_ov.renderers.OvLibrariesRenderLease` is active.
"""
self._render_lease.reject_ordinary_operation("close the renderer", retiring=True)
try:
with contextlib.ExitStack() as resources:
resources.callback(self._bindings.close)
Expand All @@ -1245,6 +1316,7 @@ def close(self) -> None:
self.meshes.clear()
self.markers.clear()
self.geometry_callbacks.clear()
self._closed = True


class OVRTXMesh:
Expand Down
Loading
Loading