2323
2424logger = logging .getLogger (__name__ )
2525
26- # TODO: extend this to ``cuda:N`` once we wire up multi-GPU support for the view.
27- # Recent Kit / USDRT releases do support multi-GPU ``SelectPrims``, but the
28- # rest of the FabricFrameView wiring (selections, indexed arrays, etc.) still
29- # assumes a single device — to be tackled in a follow-up.
30- _fabric_supported_devices = ("cpu" , "cuda" , "cuda:0" )
31-
3226
3327def _to_float32_2d (a : wp .array | torch .Tensor ) -> wp .array | torch .Tensor :
3428 """Ensure array is compatible with Fabric kernels (2-D float32).
@@ -92,15 +86,6 @@ def __init__(
9286 settings = SettingsManager .instance ()
9387 self ._use_fabric = bool (settings .get ("/physics/fabricEnabled" , False ))
9488
95- if self ._use_fabric and self ._device not in _fabric_supported_devices :
96- logger .warning (
97- f"Fabric mode is not supported on device '{ self ._device } '. "
98- "USDRT SelectPrims and Warp fabric arrays are currently "
99- f"only supported on { ', ' .join (_fabric_supported_devices )} . "
100- "Falling back to standard USD operations. This may impact performance."
101- )
102- self ._use_fabric = False
103-
10489 self ._fabric_initialized = False
10590 self ._fabric_usd_sync_done = False
10691 self ._fabric_selection = None
@@ -149,43 +134,7 @@ def set_world_poses(self, positions=None, orientations=None, indices=None):
149134 if not self ._use_fabric :
150135 self ._usd_view .set_world_poses (positions , orientations , indices )
151136 return
152-
153- if not self ._fabric_initialized :
154- self ._initialize_fabric ()
155-
156- self ._prepare_for_reuse ()
157-
158- indices_wp = self ._resolve_indices_wp (indices )
159- count = indices_wp .shape [0 ]
160-
161- dummy = wp .zeros ((0 , 3 ), dtype = wp .float32 , device = self ._device )
162- positions_wp = _to_float32_2d (positions ) if positions is not None else dummy
163- orientations_wp = (
164- _to_float32_2d (orientations )
165- if orientations is not None
166- else wp .zeros ((0 , 4 ), dtype = wp .float32 , device = self ._device )
167- )
168-
169- wp .launch (
170- kernel = fabric_utils .compose_fabric_transformation_matrix_from_warp_arrays ,
171- dim = count ,
172- inputs = [
173- self ._fabric_world_matrices ,
174- positions_wp ,
175- orientations_wp ,
176- dummy ,
177- False ,
178- False ,
179- False ,
180- indices_wp ,
181- self ._view_to_fabric ,
182- ],
183- device = self ._fabric_device ,
184- )
185- wp .synchronize ()
186-
187- self ._fabric_hierarchy .update_world_xforms ()
188- self ._fabric_usd_sync_done = True
137+ self ._compose_fabric_transform (positions = positions , orientations = orientations , indices = indices )
189138
190139 def get_world_poses (self , indices : wp .array | None = None ) -> tuple [ProxyArray , ProxyArray ]:
191140 if not self ._use_fabric :
@@ -244,7 +193,15 @@ def set_scales(self, scales, indices=None):
244193 if not self ._use_fabric :
245194 self ._usd_view .set_scales (scales , indices )
246195 return
196+ self ._compose_fabric_transform (scales = scales , indices = indices )
197+
198+ def _compose_fabric_transform (self , positions = None , orientations = None , scales = None , indices = None ):
199+ """Write the given subset of (position, orientation, scale) into Fabric in one kernel launch.
247200
201+ Components left as ``None`` are skipped via empty input arrays — the kernel reads them
202+ from the existing Fabric matrix. Always invokes :meth:`_prepare_for_reuse` exactly once
203+ per write, even when multiple components are updated together.
204+ """
248205 if not self ._fabric_initialized :
249206 self ._initialize_fabric ()
250207
@@ -253,17 +210,19 @@ def set_scales(self, scales, indices=None):
253210 indices_wp = self ._resolve_indices_wp (indices )
254211 count = indices_wp .shape [0 ]
255212
256- dummy3 = wp .zeros ((0 , 3 ), dtype = wp .float32 , device = self ._device )
257- dummy4 = wp .zeros ((0 , 4 ), dtype = wp .float32 , device = self ._device )
258- scales_wp = _to_float32_2d (scales )
213+ empty3 = wp .zeros ((0 , 3 ), dtype = wp .float32 , device = self ._device )
214+ empty4 = wp .zeros ((0 , 4 ), dtype = wp .float32 , device = self ._device )
215+ positions_wp = _to_float32_2d (positions ) if positions is not None else empty3
216+ orientations_wp = _to_float32_2d (orientations ) if orientations is not None else empty4
217+ scales_wp = _to_float32_2d (scales ) if scales is not None else empty3
259218
260219 wp .launch (
261220 kernel = fabric_utils .compose_fabric_transformation_matrix_from_warp_arrays ,
262221 dim = count ,
263222 inputs = [
264223 self ._fabric_world_matrices ,
265- dummy3 ,
266- dummy4 ,
224+ positions_wp ,
225+ orientations_wp ,
267226 scales_wp ,
268227 False ,
269228 False ,
@@ -347,10 +306,11 @@ def _rebuild_fabric_arrays(self) -> None:
347306 pattern (via ``_usd_view.count``) and does not change when Fabric rearranges its
348307 internal memory layout. The assertion below guards this invariant.
349308 """
350- assert self .count == self ._default_view_indices .shape [0 ], (
351- f"Prim count changed ({ self .count } vs { self ._default_view_indices .shape [0 ]} ). "
352- "Fabric topology change added/removed tracked prims — full re-initialization required."
353- )
309+ if self .count != self ._default_view_indices .shape [0 ]:
310+ raise RuntimeError (
311+ f"Prim count changed ({ self .count } vs { self ._default_view_indices .shape [0 ]} ). "
312+ "Fabric topology change added/removed tracked prims — full re-initialization required."
313+ )
354314 self ._view_to_fabric = wp .zeros ((self .count ,), dtype = wp .uint32 , device = self ._fabric_device )
355315 self ._fabric_to_view = wp .fabricarray (self ._fabric_selection , self ._view_index_attr )
356316
@@ -404,9 +364,6 @@ def _initialize_fabric(self) -> None:
404364 )
405365 wp .synchronize ()
406366
407- # The constructor should have taken care of this, but double check here to avoid regressions
408- assert self ._device in _fabric_supported_devices
409-
410367 self ._fabric_selection = fabric_stage .SelectPrims (
411368 require_attrs = [
412369 (usdrt .Sdf .ValueTypeNames .UInt , self ._view_index_attr , usdrt .Usd .Access .Read ),
@@ -442,19 +399,20 @@ def _initialize_fabric(self) -> None:
442399 def _sync_fabric_from_usd_once (self ) -> None :
443400 """Sync Fabric world matrices from USD once, on the first read.
444401
445- ``set_world_poses`` and ``set_scales`` each set ``_fabric_usd_sync_done``
446- themselves, so no explicit flag assignment is needed here.
402+ Combines position/orientation/scale into a single Fabric write so
403+ :meth:`_prepare_for_reuse` (and its underlying ``PrepareForReuse``) is invoked
404+ exactly once across the full sync.
447405 """
448406 if not self ._fabric_initialized :
449407 self ._initialize_fabric ()
450408
451409 positions_usd_ta , orientations_usd_ta = self ._usd_view .get_world_poses ()
452- positions_usd = positions_usd_ta .warp
453- orientations_usd = orientations_usd_ta .warp
454410 scales_usd = self ._usd_view .get_scales ()
455-
456- self .set_world_poses (positions_usd , orientations_usd )
457- self .set_scales (scales_usd )
411+ self ._compose_fabric_transform (
412+ positions = positions_usd_ta .warp ,
413+ orientations = orientations_usd_ta .warp ,
414+ scales = scales_usd ,
415+ )
458416
459417 def _resolve_indices_wp (self , indices : wp .array | None ) -> wp .array :
460418 """Resolve view indices as a Warp uint32 array."""
0 commit comments