Skip to content

Commit 60ff515

Browse files
committed
feat: add fabric_read/fabric_write context managers to FabricFrameView
Add RAII-style context managers for safe raw Fabric access: - fabric_write(): calls PrepareForReuse on entry, update_world_xforms + sync on exit. Provides world_matrices fabricarray and view_to_fabric mapping for custom warp kernel launches. - fabric_read(): calls PrepareForReuse on entry (ensures valid pointers after topology changes), no-op on exit. Also exposes read-only properties: - world_matrices: the raw fabricarray of omni:fabric:worldMatrix - view_to_fabric_mapping: the view-index to fabric-index mapping This addresses Piotr's Issue isaac-sim#6 (reader/writer pattern) by providing a structured way to bracket Fabric operations that ensures PrepareForReuse and hierarchy updates are never forgotten. Tests added: - test_fabric_write_context_manager: validates write + readback - test_fabric_read_context_manager: validates read without side effects Depends on: fix/fabric-prepare-for-reuse (PR isaac-sim#5380)
1 parent 49c51f7 commit 60ff515

2 files changed

Lines changed: 199 additions & 1 deletion

File tree

source/isaaclab_physx/isaaclab_physx/sim/views/fabric_frame_view.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,53 @@ def _rebuild_fabric_arrays(self) -> None:
352352

353353
self._fabric_world_matrices = wp.fabricarray(self._fabric_selection, "omni:fabric:worldMatrix")
354354

355+
# ------------------------------------------------------------------
356+
# Context managers for raw Fabric access
357+
# ------------------------------------------------------------------
358+
359+
def fabric_write(self):
360+
"""Context manager for raw Fabric write operations.
361+
362+
Calls ``PrepareForReuse()`` on entry (notifying the renderer that
363+
data is about to change) and ``update_world_xforms()`` +
364+
``PrepareForReuse()`` on exit (propagating changes through the
365+
hierarchy).
366+
367+
Example::
368+
369+
with view.fabric_write() as fab:
370+
# fab.world_matrices is the fabricarray
371+
wp.launch(my_kernel, dim=N, inputs=[fab.world_matrices, ...])
372+
"""
373+
return _FabricWriteContext(self)
374+
375+
def fabric_read(self):
376+
"""Context manager for raw Fabric read operations.
377+
378+
Calls ``PrepareForReuse()`` on entry to ensure the view’s
379+
fabricarray pointers are still valid after potential topology
380+
changes.
381+
382+
Example::
383+
384+
with view.fabric_read() as fab:
385+
wp.launch(my_read_kernel, dim=N, inputs=[fab.world_matrices, ...])
386+
"""
387+
return _FabricReadContext(self)
388+
389+
@property
390+
def world_matrices(self) -> wp.fabricarray | None:
391+
"""The raw Fabric world-matrix array (read-only property).
392+
393+
Returns None if Fabric is not initialized.
394+
"""
395+
return getattr(self, "_fabric_world_matrices", None)
396+
397+
@property
398+
def view_to_fabric_mapping(self) -> wp.array | None:
399+
"""View-index → Fabric-index mapping array."""
400+
return getattr(self, "_view_to_fabric", None)
401+
355402
# ------------------------------------------------------------------
356403
# Internal — Fabric initialization
357404
# ------------------------------------------------------------------
@@ -456,3 +503,90 @@ def _resolve_indices_wp(self, indices: wp.array | None) -> wp.array:
456503
if indices.dtype != wp.uint32:
457504
return wp.array(indices.numpy().astype("uint32"), dtype=wp.uint32, device=self._device)
458505
return indices
506+
507+
508+
# ======================================================================
509+
# Context manager helpers (module-level, not inside FabricFrameView)
510+
# ======================================================================
511+
512+
513+
class _FabricWriteContext:
514+
"""RAII context manager for Fabric write operations.
515+
516+
On entry: ensures Fabric is initialized, calls PrepareForReuse.
517+
On exit (no exception): synchronizes, propagates hierarchy, marks sync done.
518+
"""
519+
520+
__slots__ = ("_view",)
521+
522+
def __init__(self, view: FabricFrameView):
523+
self._view = view
524+
525+
def __enter__(self):
526+
if not self._view._fabric_initialized:
527+
self._view._initialize_fabric()
528+
if not self._view._fabric_usd_sync_done:
529+
self._view._sync_fabric_from_usd_once()
530+
self._view._prepare_for_reuse()
531+
return self
532+
533+
def __exit__(self, exc_type, exc_val, exc_tb):
534+
if exc_type is None:
535+
wp.synchronize()
536+
self._view._fabric_hierarchy.update_world_xforms()
537+
self._view._fabric_usd_sync_done = True
538+
return False
539+
540+
@property
541+
def world_matrices(self) -> wp.fabricarray:
542+
"""The fabricarray of omni:fabric:worldMatrix."""
543+
return self._view._fabric_world_matrices
544+
545+
@property
546+
def view_to_fabric(self) -> wp.array:
547+
"""View-index to Fabric-index mapping."""
548+
return self._view._view_to_fabric
549+
550+
@property
551+
def count(self) -> int:
552+
"""Number of prims in the view."""
553+
return self._view.count
554+
555+
556+
class _FabricReadContext:
557+
"""RAII context manager for Fabric read operations.
558+
559+
On entry: ensures Fabric is initialized, calls PrepareForReuse.
560+
On exit: no-op.
561+
"""
562+
563+
__slots__ = ("_view",)
564+
565+
def __init__(self, view: FabricFrameView):
566+
self._view = view
567+
568+
def __enter__(self):
569+
if not self._view._fabric_initialized:
570+
self._view._initialize_fabric()
571+
if not self._view._fabric_usd_sync_done:
572+
self._view._sync_fabric_from_usd_once()
573+
self._view._prepare_for_reuse()
574+
return self
575+
576+
def __exit__(self, exc_type, exc_val, exc_tb):
577+
return False
578+
579+
@property
580+
def world_matrices(self) -> wp.fabricarray:
581+
"""The fabricarray of omni:fabric:worldMatrix."""
582+
return self._view._fabric_world_matrices
583+
584+
@property
585+
def view_to_fabric(self) -> wp.array:
586+
"""View-index to Fabric-index mapping."""
587+
return self._view._view_to_fabric
588+
589+
@property
590+
def count(self) -> int:
591+
"""Number of prims in the view."""
592+
return self._view.count

source/isaaclab_physx/test/sim/test_views_xform_prim_fabric.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@
2323
import torch # noqa: E402
2424
import warp as wp # noqa: E402
2525
from frame_view_contract_utils import * # noqa: F401, F403, E402
26-
from frame_view_contract_utils import CHILD_OFFSET, ViewBundle, test_set_world_updates_local # noqa: E402
26+
from frame_view_contract_utils import CHILD_OFFSET, ViewBundle # noqa: E402
2727
from isaaclab_physx.sim.views import FabricFrameView as FrameView # noqa: E402
2828

2929
from pxr import Gf, UsdGeom # noqa: E402
3030

3131
import isaaclab.sim as sim_utils # noqa: E402
32+
from isaaclab.utils.warp import fabric as fabric_utils # noqa: E402
3233

3334
PARENT_POS = (0.0, 0.0, 1.0)
3435

@@ -201,6 +202,69 @@ def test_prepare_for_reuse_detects_topology_change(device, view_factory):
201202
assert not result, "PrepareForReuse should return False when no topology change"
202203

203204

205+
@pytest.mark.parametrize("device", ["cuda:0"])
206+
def test_fabric_write_context_manager(device, view_factory):
207+
"""Verify fabric_write() context manager correctly brackets writes.
208+
209+
After using the context manager to modify world matrices, the
210+
resulting poses should be readable via get_world_poses.
211+
"""
212+
bundle = view_factory(2, device)
213+
view = bundle.view
214+
view.get_world_poses() # trigger Fabric init
215+
216+
# Write via context manager
217+
with view.fabric_write() as fab:
218+
assert fab.world_matrices is not None, "world_matrices should be available"
219+
assert fab.view_to_fabric is not None, "view_to_fabric should be available"
220+
assert fab.count == 2, f"Expected count=2, got {fab.count}"
221+
222+
# Move all prims to (42, 42, 42)
223+
new_pos = wp.zeros((2, 3), dtype=wp.float32, device=device)
224+
wp.launch(kernel=_fill_position, dim=2, inputs=[new_pos, 42.0, 42.0, 42.0], device=device)
225+
226+
wp.launch(
227+
kernel=fabric_utils.compose_fabric_transformation_matrix_from_warp_arrays,
228+
dim=2,
229+
inputs=[
230+
fab.world_matrices,
231+
new_pos,
232+
wp.zeros((0, 4), dtype=wp.float32, device=device),
233+
wp.zeros((0, 3), dtype=wp.float32, device=device),
234+
False,
235+
False,
236+
False,
237+
view._default_view_indices,
238+
fab.view_to_fabric,
239+
],
240+
device=device,
241+
)
242+
243+
# Verify via high-level API
244+
pos, _ = view.get_world_poses()
245+
pos_t = wp.to_torch(pos)
246+
assert torch.allclose(pos_t, torch.tensor([[42.0, 42.0, 42.0]] * 2, device=device), atol=0.5), (
247+
f"Expected ~(42,42,42) but got {pos_t}"
248+
)
249+
250+
251+
@pytest.mark.parametrize("device", ["cuda:0"])
252+
def test_fabric_read_context_manager(device, view_factory):
253+
"""Verify fabric_read() context manager provides access without side effects."""
254+
bundle = view_factory(1, device)
255+
view = bundle.view
256+
pos_before, _ = view.get_world_poses() # trigger Fabric init
257+
pos_before_t = wp.to_torch(pos_before).clone()
258+
259+
with view.fabric_read() as fab:
260+
assert fab.world_matrices is not None
261+
assert fab.count == 1
262+
263+
# Poses should be unchanged after a read context
264+
pos_after, _ = view.get_world_poses()
265+
assert torch.allclose(wp.to_torch(pos_after), pos_before_t, atol=0.01)
266+
267+
204268
@pytest.mark.parametrize("device", ["cuda:0"])
205269
def test_get_scales_fabric_path(device, view_factory):
206270
"""Exercise the Fabric-native get_scales path."""

0 commit comments

Comments
 (0)