Skip to content

Commit 8ab1bdb

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 caa1949 commit 8ab1bdb

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
@@ -341,6 +341,53 @@ def _rebuild_fabric_arrays(self) -> None:
341341

342342
self._fabric_world_matrices = wp.fabricarray(self._fabric_selection, "omni:fabric:worldMatrix")
343343

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