Skip to content

Commit 4afb31b

Browse files
committed
Rework FabricFrameView to use indexedfabricarray + Fabric localMatrix
Adopts the prototype design from bareya/pbarejko/camera-update for FabricFrameView's transform handling, and replaces the previous software composition of local poses (which forced a Python USD parent loop and made get_local_poses 3x slower than USD). Architecture changes: * Three persistent ``PrimSelection`` instances (trans_ro, world_rw, local_rw), differing only in per-attribute access mode. Replaces the single selection that required a custom ``isaaclab:view_index`` attribute on every prim. * Path-based view -> fabric index computation: the integer mapping is derived once from ``selection.GetPaths()`` and stored as a Warp array. No prim attributes are added to the stage. * All transform reads and writes go through ``wp.indexedfabricarray``, so kernels just dereference ``ifa[view_index]`` without a separate mapping argument. The two new indexed kernels live next to the existing ones in ``isaaclab.utils.warp.fabric``. * Stage-level ``IFabricHierarchy`` cache and dirty-stage set: multiple FabricFrameView instances on the same stage share one hierarchy. Local-pose path: * ``set_local_poses`` writes ``omni:fabric:localMatrix`` directly through Fabric and marks the stage dirty. The next ``get_world_poses`` fires a Warp kernel that recomputes ``child_world = parent_world * child_local`` (we cannot rely on ``IFabricHierarchy.update_world_xforms`` -- in practice it re-reads USD's authored xformOps and overwrites the Fabric matrices we just authored). * Symmetrically, ``set_world_poses`` runs a kernel that recomputes ``child_local = inv(parent_world) * child_world`` so subsequent ``get_local_poses`` calls return consistent values. * Initial Fabric population reads through the internal ``UsdFrameView`` (children) and ``UsdGeom.XformCache`` (parents) and writes via the same compose kernel set used at runtime. ``SetWorldXformFromUsd`` / ``SetLocalXformFromUsd`` are no-ops on author-then-query stages, so we do this manually. Performance impact (1024 prims, 50 iterations, A6000): Operation USD (ms) Fabric (ms) Speedup Get World Poses 13.56 0.067 201x Set World Poses 29.97 0.123 244x Interleaved Set->Get 43.78 0.159 276x Get Local Poses 6.15 0.064 96x (was 0.31x SLOWER) Set Local Poses 8.61 0.053 163x (was 0.42x SLOWER) The two new indexed kernels added to ``isaaclab.utils.warp.fabric``: * ``decompose_indexed_fabric_transforms`` -- like the existing ``decompose_fabric_transformation_matrix_to_warp_arrays`` but takes a ``wp.indexedfabricarray`` so the view->fabric mapping is baked in. * ``compose_indexed_fabric_transforms`` -- mirror. * ``update_indexed_local_matrix_from_world`` -- new: recomputes ``child_local = inv(parent_world) * child_world`` per child. * ``update_indexed_world_matrix_from_local`` -- new: mirror. Tests updated: * ``test_fabric_rebuild_after_topology_change`` no longer monkey-patches the removed ``_prepare_for_reuse`` / ``_rebuild_fabric_arrays`` helpers; it now exercises ``_compute_fabric_indices`` and ``_build_indexed_array`` directly to simulate a rebuild. * ``test_prepare_for_reuse_detects_topology_change`` verifies all three persistent selections respond to ``PrepareForReuse`` (instead of the removed single ``_fabric_selection``). All 41 tests in ``test_views_xform_prim_fabric.py`` pass.
1 parent a1183e9 commit 4afb31b

4 files changed

Lines changed: 677 additions & 379 deletions

File tree

source/isaaclab/isaaclab/utils/warp/fabric.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818
if TYPE_CHECKING:
1919
FabricArrayUInt32 = Any
2020
FabricArrayMat44d = Any
21+
IndexedFabricArrayMat44d = Any
2122
ArrayUInt32 = Any
2223
ArrayUInt32_1d = Any
2324
ArrayFloat32_2d = Any
2425
else:
2526
FabricArrayUInt32 = wp.fabricarray(dtype=wp.uint32)
2627
FabricArrayMat44d = wp.fabricarray(dtype=wp.mat44d)
28+
IndexedFabricArrayMat44d = wp.indexedfabricarray(dtype=wp.mat44d)
2729
ArrayUInt32 = wp.array(ndim=1, dtype=wp.uint32)
2830
ArrayUInt32_1d = wp.array(dtype=wp.uint32)
2931
ArrayFloat32_2d = wp.array(ndim=2, dtype=wp.float32)
@@ -163,6 +165,178 @@ def compose_fabric_transformation_matrix_from_warp_arrays(
163165
)
164166

165167

168+
@wp.kernel(enable_backward=False)
169+
def decompose_indexed_fabric_transforms(
170+
fabric_matrices: IndexedFabricArrayMat44d,
171+
array_positions: ArrayFloat32_2d,
172+
array_orientations: ArrayFloat32_2d,
173+
array_scales: ArrayFloat32_2d,
174+
indices: ArrayUInt32,
175+
):
176+
"""Decompose indexed Fabric transformation matrices into position, orientation, and scale.
177+
178+
Like :func:`decompose_fabric_transformation_matrix_to_warp_arrays` but operates on a
179+
:class:`wp.indexedfabricarray` that already encodes the view-to-fabric mapping, removing
180+
the need for a separate ``mapping`` array.
181+
182+
Args:
183+
fabric_matrices: Indexed fabric array containing 4x4 transformation matrices.
184+
array_positions: Output array for positions [m], shape (N, 3).
185+
array_orientations: Output array for quaternions in xyzw format, shape (N, 4).
186+
array_scales: Output array for scales, shape (N, 3).
187+
indices: View indices to process (subset selection).
188+
"""
189+
output_index = wp.tid()
190+
view_index = indices[output_index]
191+
192+
position, rotation, scale = _decompose_transformation_matrix(wp.mat44f(fabric_matrices[view_index]))
193+
194+
if array_positions.shape[0] > 0:
195+
array_positions[output_index, 0] = position[0]
196+
array_positions[output_index, 1] = position[1]
197+
array_positions[output_index, 2] = position[2]
198+
if array_orientations.shape[0] > 0:
199+
array_orientations[output_index, 0] = rotation[0]
200+
array_orientations[output_index, 1] = rotation[1]
201+
array_orientations[output_index, 2] = rotation[2]
202+
array_orientations[output_index, 3] = rotation[3]
203+
if array_scales.shape[0] > 0:
204+
array_scales[output_index, 0] = scale[0]
205+
array_scales[output_index, 1] = scale[1]
206+
array_scales[output_index, 2] = scale[2]
207+
208+
209+
@wp.kernel(enable_backward=False)
210+
def compose_indexed_fabric_transforms(
211+
fabric_matrices: IndexedFabricArrayMat44d,
212+
array_positions: ArrayFloat32_2d,
213+
array_orientations: ArrayFloat32_2d,
214+
array_scales: ArrayFloat32_2d,
215+
broadcast_positions: bool,
216+
broadcast_orientations: bool,
217+
broadcast_scales: bool,
218+
indices: ArrayUInt32,
219+
):
220+
"""Compose indexed Fabric transformation matrices from position, orientation, and scale.
221+
222+
Like :func:`compose_fabric_transformation_matrix_from_warp_arrays` but operates on a
223+
:class:`wp.indexedfabricarray` that already encodes the view-to-fabric mapping, removing
224+
the need for a separate ``mapping`` array.
225+
226+
Args:
227+
fabric_matrices: Indexed fabric array containing 4x4 transformation matrices to update.
228+
array_positions: Input array for positions [m], shape (N, 3).
229+
array_orientations: Input array for quaternions in xyzw format, shape (N, 4).
230+
array_scales: Input array for scales, shape (N, 3).
231+
broadcast_positions: If True, use first position for all prims.
232+
broadcast_orientations: If True, use first orientation for all prims.
233+
broadcast_scales: If True, use first scale for all prims.
234+
indices: View indices to process (subset selection).
235+
"""
236+
i = wp.tid()
237+
view_index = indices[i]
238+
position, rotation, scale = _decompose_transformation_matrix(wp.mat44f(fabric_matrices[view_index]))
239+
240+
if array_positions.shape[0] > 0:
241+
if broadcast_positions:
242+
index = 0
243+
else:
244+
index = i
245+
position[0] = array_positions[index, 0]
246+
position[1] = array_positions[index, 1]
247+
position[2] = array_positions[index, 2]
248+
if array_orientations.shape[0] > 0:
249+
if broadcast_orientations:
250+
index = 0
251+
else:
252+
index = i
253+
rotation[0] = array_orientations[index, 0]
254+
rotation[1] = array_orientations[index, 1]
255+
rotation[2] = array_orientations[index, 2]
256+
rotation[3] = array_orientations[index, 3]
257+
if array_scales.shape[0] > 0:
258+
if broadcast_scales:
259+
index = 0
260+
else:
261+
index = i
262+
scale[0] = array_scales[index, 0]
263+
scale[1] = array_scales[index, 1]
264+
scale[2] = array_scales[index, 2]
265+
266+
fabric_matrices[view_index] = wp.mat44d( # type: ignore[arg-type]
267+
wp.transpose(wp.transform_compose(position, rotation, scale)) # type: ignore[arg-type]
268+
)
269+
270+
271+
@wp.kernel(enable_backward=False)
272+
def update_indexed_local_matrix_from_world(
273+
child_world_matrices: IndexedFabricArrayMat44d,
274+
parent_world_matrices: IndexedFabricArrayMat44d,
275+
child_local_matrices: IndexedFabricArrayMat44d,
276+
indices: ArrayUInt32,
277+
):
278+
"""Recompute child localMatrix from (parent worldMatrix, child worldMatrix).
279+
280+
Computes ``child_local = inv(parent_world) * child_world`` per prim and writes the
281+
result back to the child's :data:`omni:fabric:localMatrix` so that subsequent
282+
``get_local_poses`` calls see consistent values after a world-pose write.
283+
284+
All three indexed arrays are expected to be indexed by the same per-view indices
285+
(i.e. ``view_to_child_fabric``, ``view_to_parent_fabric``, ``view_to_child_fabric``)
286+
so the kernel only needs the view-side indices.
287+
288+
Storage convention: Fabric matrices are stored as the transpose of the standard
289+
column-major math convention (translation in the bottom row). We transpose into
290+
math convention, do the inverse-multiply, and transpose back when writing.
291+
292+
Args:
293+
child_world_matrices: Indexed fabric array of child world matrices (read).
294+
parent_world_matrices: Indexed fabric array of parent world matrices (read).
295+
child_local_matrices: Indexed fabric array of child local matrices (written).
296+
indices: View indices to process.
297+
"""
298+
i = wp.tid()
299+
view_index = indices[i]
300+
child_world_storage = wp.mat44f(child_world_matrices[view_index])
301+
parent_world_storage = wp.mat44f(parent_world_matrices[view_index])
302+
# Transpose into standard math convention (translation in the right column).
303+
child_world = wp.transpose(child_world_storage)
304+
parent_world = wp.transpose(parent_world_storage)
305+
child_local = wp.inverse(parent_world) * child_world
306+
# Transpose back to Fabric storage convention.
307+
child_local_matrices[view_index] = wp.mat44d(wp.transpose(child_local)) # type: ignore[arg-type]
308+
309+
310+
@wp.kernel(enable_backward=False)
311+
def update_indexed_world_matrix_from_local(
312+
child_local_matrices: IndexedFabricArrayMat44d,
313+
parent_world_matrices: IndexedFabricArrayMat44d,
314+
child_world_matrices: IndexedFabricArrayMat44d,
315+
indices: ArrayUInt32,
316+
):
317+
"""Recompute child worldMatrix from (parent worldMatrix, child localMatrix).
318+
319+
Computes ``child_world = parent_world * child_local`` per prim and writes the
320+
result back to the child's :data:`omni:fabric:worldMatrix`. Used after a
321+
``set_local_poses`` write so that subsequent ``get_world_poses`` calls see
322+
consistent values. Mirror of :func:`update_indexed_local_matrix_from_world`.
323+
324+
Args:
325+
child_local_matrices: Indexed fabric array of child local matrices (read).
326+
parent_world_matrices: Indexed fabric array of parent world matrices (read).
327+
child_world_matrices: Indexed fabric array of child world matrices (written).
328+
indices: View indices to process.
329+
"""
330+
i = wp.tid()
331+
view_index = indices[i]
332+
child_local_storage = wp.mat44f(child_local_matrices[view_index])
333+
parent_world_storage = wp.mat44f(parent_world_matrices[view_index])
334+
child_local = wp.transpose(child_local_storage)
335+
parent_world = wp.transpose(parent_world_storage)
336+
child_world = parent_world * child_local
337+
child_world_matrices[view_index] = wp.mat44d(wp.transpose(child_world)) # type: ignore[arg-type]
338+
339+
166340
@wp.func
167341
def _decompose_transformation_matrix(m: Any): # -> tuple[wp.vec3f, wp.quatf, wp.vec3f]
168342
"""Decompose a 4x4 transformation matrix into position, orientation, and scale.

source/isaaclab_physx/changelog.d/fabric-frame-view-local-poses.rst

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,24 @@ Fixed
22
^^^^^
33

44
* Fixed :meth:`~isaaclab_physx.sim.views.FabricFrameView.get_local_poses`
5-
returning stale USD values after Fabric world-pose writes. Local poses are
6-
now computed from Fabric world matrices using the parent prim's USD world
7-
transform.
5+
returning stale USD values after Fabric world-pose writes. Local poses
6+
are now read directly from Fabric's ``omni:fabric:localMatrix`` via
7+
:class:`wp.indexedfabricarray`, and are kept consistent with worldMatrix
8+
through Warp kernels that propagate either direction on writes.
89

910
Changed
1011
^^^^^^^
1112

13+
* Reworked :class:`~isaaclab_physx.sim.views.FabricFrameView` to follow
14+
the prototype design from ``bareya/pbarejko/camera-update``: three
15+
persistent ``PrimSelection`` instances (one per access mode), path-based
16+
view → fabric index mapping (no custom prim attributes), and Warp kernels
17+
that operate on :class:`wp.indexedfabricarray` so the kernels just index
18+
``ifa[view_index]`` instead of taking a separate mapping array.
1219
* :meth:`~isaaclab_physx.sim.views.FabricFrameView.set_local_poses` now
13-
composes ``child_world = parent_world * local`` and writes the result
14-
through Fabric, instead of routing the write to USD when Fabric is active
15-
and synchronized.
20+
writes ``omni:fabric:localMatrix`` directly through Fabric. The next
21+
``get_world_poses`` runs a Warp kernel that recomputes
22+
``child_world = parent_world * child_local``. Symmetrically,
23+
``set_world_poses`` runs a kernel that recomputes
24+
``child_local = inv(parent_world) * child_world`` so subsequent
25+
``get_local_poses`` calls return consistent values.

0 commit comments

Comments
 (0)