Skip to content

Commit efd9d1e

Browse files
authored
perf: add PrepareForReuse to FabricFrameView, remove sync_usd_on_fabric_write (#5380)
## Summary Replace the `sync_usd_on_fabric_write` workaround in `FabricFrameView` with proper `PrepareForReuse()` calls on the Fabric `PrimSelection`. This tells the renderer (FSD/Storm) that Fabric data has changed, so the next rendered frame reflects updated transforms — eliminating the need to copy Fabric writes back to USD. ## Motivation The existing `sync_usd_on_fabric_write` flag worked by mirroring every Fabric write back to USD, which defeated the performance benefits of Fabric. With `PrepareForReuse()`, the rendering pipeline is properly notified of Fabric data changes without any USD writeback. Additionally, the old code incorrectly fell back to USD for CPU devices — Warp handles CPU Fabric buffers correctly, so the fallback was unnecessary. This addresses two of the issues raised in @pbarejko Piotr's review of PR #4923: - **Issue #1** (USD write-back): Fabric writes no longer sync back to USD - **Issue #4** (PrepareForReuse): Renderer notification via `PrepareForReuse()` instead of USD writeback ## Changes ### Core (FabricFrameView) - Call `_prepare_for_reuse()` in write paths (`set_world_poses`, `set_scales`) to notify the renderer - Remove `sync_usd_on_fabric_write` parameter (accepted via `**kwargs` for backward compat) - Remove incorrect CPU/device fallback warnings — Warp handles CPU Fabric buffers correctly - Add `_rebuild_fabric_arrays()` for topology change recovery when `PrepareForReuse()` returns True, with assertion guarding the prim-count invariant ### Camera - Remove `sync_usd_on_fabric_write=True` from FrameView construction in `camera.py` ## Benchmark Results 1024 prims, 50 iterations, NVIDIA L40 GPU: | Operation | USD (ms) | Fabric (ms) | Speedup | |---|---|---|---| | Get World Poses | 14.71 | 0.07 | **203x** | | Set World Poses | 40.75 | 0.16 | **259x** | | Interleaved Set→Get | 55.90 | 0.24 | **232x** | | Get Local Poses | 11.08 | 11.12 | 1.0x | | Set Local Poses | 16.14 | 16.28 | 1.0x | Local poses fall back to USD (expected — Fabric only accelerates world poses via `omni:fabric:worldMatrix`). ## Tests Added | Test | What it validates | |------|------------------| | `test_camera_pose_update_reflected_in_render` | Camera pose changes propagate to rendered depth (close vs far) for CPU/GPU, tiled/non-tiled | | `test_fabric_set_world_does_not_write_back_to_usd` | Fabric writes stay in Fabric, USD prim unchanged | | `test_set_world_updates_local` (xfail) | Documents Issue #5: `set_world_poses` doesn't update local pose in Fabric mode | ## Test Results | Test Suite | Passed | Skipped | Xfailed | Total | |---|---|---|---|---| | Fabric contract tests (`test_views_xform_prim_fabric.py`) | 17 | 16 | 1 | 34 | | USD contract tests (`test_views_xform_prim.py`) | 45 | 0 | 0 | 45 | | Camera render test (`test_tiled_camera.py`) | 8 | 0 | 0 | 8 | ## Type of change - Performance improvement (removes redundant USD writeback on Fabric operations) ## 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` - [ ] 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 *No doc changes needed (parameter wasn't referenced in any docs)*
1 parent bf646bb commit efd9d1e

10 files changed

Lines changed: 323 additions & 54 deletions

File tree

CONTRIBUTORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ Guidelines for modifications:
144144
* Patrick Yin
145145
* Paul Reeves
146146
* Peter Du
147+
* Peter Verswyvelen
147148
* Philipp Reist
148149
* Piotr Barejko
149150
* Pulkit Goyal
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Changed
2+
^^^^^^^
3+
4+
* Updated :class:`~isaaclab.sensors.camera.Camera` to construct its internal
5+
:class:`~isaaclab.sim.views.FrameView` without the now-removed
6+
``sync_usd_on_fabric_write`` kwarg. USD attributes on camera prims are
7+
no longer kept in sync with Fabric writes; read poses through the view's
8+
getters instead.

source/isaaclab/isaaclab/sensors/camera/camera.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@
1818
import isaaclab.sim as sim_utils
1919
import isaaclab.utils.sensors as sensor_utils
2020
from isaaclab.app.settings_manager import get_settings_manager
21-
from isaaclab.renderers import BaseRenderer
22-
from isaaclab.renderers.camera_render_spec import CameraRenderSpec
21+
from isaaclab.renderers import BaseRenderer, CameraRenderSpec
2322
from isaaclab.sim.views import FrameView
2423
from isaaclab.utils import to_camel_case
2524
from isaaclab.utils.math import (
@@ -380,9 +379,7 @@ def _initialize_impl(self):
380379
# references to prims located in the stage.
381380
sim_ctx.render_context.ensure_prepare_stage(self.stage, self._num_envs)
382381

383-
# Create a view for the sensor with Fabric enabled for fast pose queries.
384-
# TODO: remove sync_usd_on_fabric_write=True once the GPU Fabric sync bug is fixed.
385-
self._view = FrameView(self.cfg.prim_path, device=self._device, stage=self.stage, sync_usd_on_fabric_write=True)
382+
self._view = FrameView(self.cfg.prim_path, device=self._device, stage=self.stage)
386383
# Check that sizes are correct
387384
if self._view.count != self._num_envs:
388385
raise RuntimeError(

source/isaaclab/isaaclab/sim/views/usd_frame_view.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,7 @@ def __init__(
7272
stage: USD stage to search for prims. Defaults to None, in which case the current
7373
active stage from the simulation context is used.
7474
**kwargs: Additional keyword arguments (ignored). Allows forward-compatible
75-
construction when callers pass backend-specific options like
76-
``sync_usd_on_fabric_write``.
75+
construction when callers pass backend-specific options.
7776
7877
Raises:
7978
ValueError: If any matched prim is not Xformable or doesn't have standardized

source/isaaclab/test/sensors/test_camera.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,72 @@ def cleanup(self, render_data):
11611161
Renderer._registry.pop(backend, None)
11621162

11631163

1164+
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
1165+
@pytest.mark.isaacsim_ci
1166+
def test_camera_pose_update_reflected_in_render(setup_camera_device, device):
1167+
"""Camera pose changes via FrameView should be visible in rendered depth.
1168+
1169+
Moves the camera close then far, renders depth, and verifies that the mean
1170+
valid depth from the far position is significantly larger (>1.5×) than the
1171+
close position. This validates that Fabric-side pose writes (via
1172+
PrepareForReuse) and USD writes are correctly propagated to the RTX
1173+
renderer.
1174+
"""
1175+
sim, _unused_cam_cfg, dt = setup_camera_device
1176+
1177+
cam_cfg = CameraCfg(
1178+
prim_path="/World/PoseTestCam",
1179+
height=128,
1180+
width=256,
1181+
update_period=0,
1182+
update_latest_camera_pose=True,
1183+
data_types=["distance_to_camera"],
1184+
spawn=sim_utils.PinholeCameraCfg(
1185+
focal_length=24.0,
1186+
focus_distance=400.0,
1187+
horizontal_aperture=20.955,
1188+
clipping_range=(0.1, 1.0e5),
1189+
),
1190+
)
1191+
camera = Camera(cam_cfg)
1192+
try:
1193+
sim.reset()
1194+
1195+
target = torch.tensor([[0.0, 0.0, 0.0]], dtype=torch.float32, device=camera.device)
1196+
max_range = cam_cfg.spawn.clipping_range[1]
1197+
1198+
# -- close position --
1199+
eyes_close = torch.tensor([[2.0, 2.0, 2.0]], dtype=torch.float32, device=camera.device)
1200+
camera.set_world_poses_from_view(eyes_close, target)
1201+
sim.step()
1202+
camera.update(dt)
1203+
depth_close = camera.data.output["distance_to_camera"].clone()
1204+
1205+
# -- far position --
1206+
eyes_far = torch.tensor([[8.0, 8.0, 8.0]], dtype=torch.float32, device=camera.device)
1207+
camera.set_world_poses_from_view(eyes_far, target)
1208+
sim.step()
1209+
camera.update(dt)
1210+
depth_far = camera.data.output["distance_to_camera"].clone()
1211+
1212+
# -- validate --
1213+
valid_close = depth_close[depth_close < max_range]
1214+
valid_far = depth_far[depth_far < max_range]
1215+
1216+
assert valid_close.numel() > 0, "No valid close-range depth pixels"
1217+
assert valid_far.numel() > 0, "No valid far-range depth pixels"
1218+
1219+
mean_close = valid_close.mean().item()
1220+
mean_far = valid_far.mean().item()
1221+
1222+
assert mean_far > mean_close * 1.5, (
1223+
f"Far depth ({mean_far:.2f}) should be > 1.5× close depth ({mean_close:.2f}). "
1224+
"Camera pose change may not be reaching the renderer."
1225+
)
1226+
finally:
1227+
del camera
1228+
1229+
11641230
def _populate_scene():
11651231
"""Add prims to the scene."""
11661232
# Ground-plane

source/isaaclab/test/sensors/test_multi_mesh_ray_caster_camera.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -752,11 +752,11 @@ def test_output_equal_to_usd_camera_when_intrinsics_set(setup_simulation):
752752

753753
# set camera position
754754
camera_warp.set_world_poses_from_view(
755-
eyes=torch.tensor([[0.0, 0.0, 5.0]], device=camera_warp.device),
755+
eyes=torch.tensor([[0.1, 0.0, 5.0]], device=camera_warp.device),
756756
targets=torch.tensor([[0.0, 0.0, 0.0]], device=camera_warp.device),
757757
)
758758
camera_usd.set_world_poses_from_view(
759-
eyes=torch.tensor([[0.0, 0.0, 5.0]], device=camera_usd.device),
759+
eyes=torch.tensor([[0.1, 0.0, 5.0]], device=camera_usd.device),
760760
targets=torch.tensor([[0.0, 0.0, 0.0]], device=camera_usd.device),
761761
)
762762

source/isaaclab/test/sensors/test_ray_caster_camera.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -898,11 +898,11 @@ def test_output_equal_to_usd_camera_when_intrinsics_set(setup_sim, focal_length_
898898

899899
# set camera position
900900
camera_warp.set_world_poses_from_view(
901-
eyes=torch.tensor([[0.001, 0.0, 5.0]], device=camera_warp.device),
901+
eyes=torch.tensor([[0.1, 0.0, 5.0]], device=camera_warp.device),
902902
targets=torch.tensor([[0.0, 0.0, 0.0]], device=camera_warp.device),
903903
)
904904
camera_usd.set_world_poses_from_view(
905-
eyes=torch.tensor([[0.001, 0.0, 5.0]], device=camera_usd.device),
905+
eyes=torch.tensor([[0.1, 0.0, 5.0]], device=camera_usd.device),
906906
targets=torch.tensor([[0.0, 0.0, 0.0]], device=camera_usd.device),
907907
)
908908

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Changed
2+
^^^^^^^
3+
4+
* **Breaking:** Removed the ``sync_usd_on_fabric_write`` keyword argument from
5+
:class:`~isaaclab_physx.sim.views.FabricFrameView`. Fabric writes
6+
(``set_world_poses``, ``set_scales``) now notify the renderer via
7+
``PrepareForReuse()`` on the underlying ``PrimSelection`` instead of writing
8+
back to USD, which is ~200x faster and avoids the stale USD shadow state the
9+
old path produced. Callers passing ``sync_usd_on_fabric_write=True`` should
10+
remove the argument; if they relied on USD reflecting Fabric writes, they
11+
should now read Fabric poses directly via the view's getters or refresh USD
12+
explicitly.

0 commit comments

Comments
 (0)