diff --git a/source/isaaclab_ov/changelog.d/ovrtx-caller-owned-transform-write.rst b/source/isaaclab_ov/changelog.d/ovrtx-caller-owned-transform-write.rst new file mode 100644 index 00000000000..f4d10ee70d0 --- /dev/null +++ b/source/isaaclab_ov/changelog.d/ovrtx-caller-owned-transform-write.rst @@ -0,0 +1,4 @@ +Fixed +^^^^^ + +* Fixed OVRTX object and camera transform updates to write a caller-owned GPU buffer instead of mapping and unmapping OVRTX memory every frame. diff --git a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py index 6bd20be8675..a87a091ae5d 100644 --- a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py +++ b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py @@ -85,7 +85,6 @@ decode_stable_id_map, decode_stable_id_semantic_id_map, ) -from .ovrtx_mapping import map_attribute_for_warp_writes from .ovrtx_renderer_cfg import OVRTXRendererCfg from .ovrtx_renderer_kernels import ( compute_cable_points_world_kernel, @@ -422,8 +421,16 @@ def _create_object_scale_array(self, object_paths: list[str]) -> wp.array: 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 @@ -563,6 +570,7 @@ def _setup_xform_bindings_legacy(self): 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: @@ -719,7 +727,13 @@ def set_outputs(self, render_data: OVRTXRenderData, output_data: dict[str, Proxy ) def _update_transforms_legacy(self) -> None: - if self._object_xform_binding is None or self._object_newton_indices is None or self._object_scales is 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 @@ -729,15 +743,21 @@ def _update_transforms_legacy(self) -> None: body_q = getattr(newton_state, "body_q", None) if body_q is None: return - with map_attribute_for_warp_writes( - self._object_xform_binding, self._warp_device, wp.mat44d - ) as ovrtx_transforms: - wp.launch( - kernel=sync_newton_transforms_kernel, - dim=len(self._object_newton_indices), - inputs=[ovrtx_transforms, self._object_newton_indices, body_q, self._object_scales], - device=self._device, - ) + + 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: @@ -801,10 +821,11 @@ def _update_camera_legacy( device=self._device, ) if self._camera_xform_binding is not None: - with map_attribute_for_warp_writes( - self._camera_xform_binding, self._warp_device, wp.mat44d - ) as transforms_view: - wp.copy(transforms_view, camera_transforms) + 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" @@ -1063,6 +1084,7 @@ def _safe_unbind(binding, name: str) -> None: ): _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 = [] diff --git a/source/isaaclab_ov/test/test_ovrtx_deformable_bindings.py b/source/isaaclab_ov/test/test_ovrtx_deformable_bindings.py index bf1f1602ba9..0166e49ba8a 100644 --- a/source/isaaclab_ov/test/test_ovrtx_deformable_bindings.py +++ b/source/isaaclab_ov/test/test_ovrtx_deformable_bindings.py @@ -590,3 +590,56 @@ def _write(query, attribute, **kwargs): assert tensors[0].data == particle_q[1:4].ptr assert tensors[0].shape_tuple == (3,) assert tensors[0].dtype.lanes == 3 + + +def test_update_transforms_writes_caller_owned_buffer(monkeypatch: pytest.MonkeyPatch): + """Object xforms fill a persistent GPU buffer and blocking ASYNC write, not map/unmap.""" + renderer, _ = _make_renderer_without_backend() + buffer = object() + renderer._object_xform_binding = _FakePointsBinding("omni:xform") + renderer._object_newton_indices = [0, 1] + renderer._object_scales = object() + renderer._object_transform_buffer = buffer + + monkeypatch.setattr(NewtonManager, "get_state", classmethod(lambda cls: SimpleNamespace(body_q=object()))) + launch_kwargs: dict = {} + + def _capture_launch(*args, **kwargs): + launch_kwargs.update(kwargs) + + monkeypatch.setattr(ovrtx_renderer_module.wp, "launch", _capture_launch) + renderer._warp_device = SimpleNamespace(stream=SimpleNamespace(cuda_stream=99)) + + renderer.update_transforms() + + assert launch_kwargs["inputs"][0] is buffer + assert launch_kwargs["dim"] == 2 + assert renderer._object_xform_binding.written is buffer + assert renderer._object_xform_binding.write_kwargs["data_access"] is DataAccess.ASYNC + assert renderer._object_xform_binding.write_kwargs["cuda_stream"] == 99 + + +def test_update_camera_writes_without_mapping(monkeypatch: pytest.MonkeyPatch): + """Camera xforms are handed to ``write()`` instead of copied into a mapped OVRTX buffer.""" + renderer, _ = _make_renderer_without_backend() + renderer._camera_xform_binding = _FakePointsBinding("omni:xform") + camera_transforms = [] + + monkeypatch.setattr(ovrtx_renderer_module, "convert_camera_frame_orientation_convention_wp", lambda **kwargs: None) + monkeypatch.setattr(ovrtx_renderer_module.wp, "empty", lambda *args, **kwargs: object()) + + def _fake_zeros(*args, **kwargs): + arr = object() + camera_transforms.append(arr) + return arr + + monkeypatch.setattr(ovrtx_renderer_module.wp, "zeros", _fake_zeros) + monkeypatch.setattr(ovrtx_renderer_module.wp, "launch", lambda *args, **kwargs: None) + renderer._warp_device = SimpleNamespace(stream=SimpleNamespace(cuda_stream=7)) + + positions = SimpleNamespace(shape=(2,), warp=object()) + renderer.update_camera(object(), positions, SimpleNamespace(warp=object()), object()) + + assert renderer._camera_xform_binding.written is camera_transforms[0] + assert renderer._camera_xform_binding.write_kwargs["data_access"] is DataAccess.ASYNC + assert renderer._camera_xform_binding.write_kwargs["cuda_stream"] == 7 diff --git a/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py b/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py index 32512e6f634..f2b298eeac6 100644 --- a/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py +++ b/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py @@ -682,6 +682,7 @@ def test_ovrtx_close_releases_legacy_renderer_state(): ] assert renderer._camera_xform_binding is None assert renderer._object_xform_binding is None + assert renderer._object_transform_buffer is None assert renderer._deformable_points_binding is None assert renderer._particle_points_binding is None assert renderer._cable_points_binding is None