diff --git a/source/isaaclab/isaaclab/sim/views/frame_view.py b/source/isaaclab/isaaclab/sim/views/frame_view.py index 95c5f0bb6850..610ed2d84cf1 100644 --- a/source/isaaclab/isaaclab/sim/views/frame_view.py +++ b/source/isaaclab/isaaclab/sim/views/frame_view.py @@ -6,15 +6,37 @@ """Backend-dispatching FrameView. ``FrameView(path, device=...)`` automatically selects the right backend: -- PhysX: :class:`~isaaclab_physx.sim.views.FabricFrameView` +- PhysX + Fabric enabled + supported device: :class:`~isaaclab_physx.sim.views.FabricFrameView` +- PhysX without Fabric (or unsupported device): :class:`~isaaclab.sim.views.UsdFrameView` +- OVPhysX: :class:`~isaaclab_ovphysx.sim.views.OvPhysxFrameView` - Newton: :class:`~isaaclab_newton.sim.views.NewtonSiteFrameView` """ from __future__ import annotations +import logging + from isaaclab.utils.backend_utils import FactoryBase from .base_frame_view import BaseFrameView +from .usd_frame_view import UsdFrameView + +logger = logging.getLogger(__name__) + +def _is_fabric_supported_device(device: str) -> bool: + """Return True if *device* can use the Fabric-accelerated path. + + Any ``cuda:`` index is supported — multi-GPU setups use cuda:1, cuda:2, etc. + """ + if device in ("cpu", "cuda"): + return True + if device.startswith("cuda:"): + try: + int(device.split(":", 1)[1]) + return True + except (ValueError, IndexError): + pass + return False class FrameView(FactoryBase, BaseFrameView): @@ -23,8 +45,10 @@ class FrameView(FactoryBase, BaseFrameView): Callers use ``FrameView(prim_path, device=device)`` and get the correct implementation automatically: - - **PhysX / no backend**: :class:`~isaaclab_physx.sim.views.FabricFrameView` - (Fabric GPU acceleration with USD fallback). + - **PhysX + Fabric**: :class:`~isaaclab_physx.sim.views.FabricFrameView` + (GPU-accelerated transforms via Warp + USDRT). + - **PhysX without Fabric**: :class:`~isaaclab.sim.views.UsdFrameView` + (standard USD operations). - **OVPhysX**: :class:`~isaaclab_ovphysx.sim.views.OvPhysxFrameView` (Warp-native, reads body poses via an OVPhysX ``RIGID_BODY_POSE`` tensor binding). @@ -36,22 +60,50 @@ class FrameView(FactoryBase, BaseFrameView): "physx": "FabricFrameView", "ovphysx": "OvPhysxFrameView", "newton": "NewtonSiteFrameView", + # "usd" is registered eagerly below — no dynamic import needed. } @classmethod def _get_backend(cls, *args, **kwargs) -> str: + from isaaclab.app.settings_manager import SettingsManager # noqa: PLC0415 from isaaclab.sim.simulation_context import SimulationContext # noqa: PLC0415 ctx = SimulationContext.instance() if ctx is None: - return "physx" + return "usd" + manager_name = ctx.physics_manager.__name__.lower() if "newton" in manager_name: return "newton" if "ovphysx" in manager_name: return "ovphysx" - return "physx" + + # PhysX path — check if Fabric is enabled and the device is supported. + settings = SettingsManager.instance() + fabric_enabled = bool(settings.get("/physics/fabricEnabled", False)) + + device = kwargs.get("device", "cpu") + if len(args) >= 2: + device = args[1] + + if fabric_enabled and _is_fabric_supported_device(device): + return "physx" + + if fabric_enabled and not _is_fabric_supported_device(device): + logger.warning( + f"Fabric mode is not supported on device '{device}'. " + "USDRT SelectPrims and Warp fabric arrays are currently " + "only supported on cpu and cuda: devices. " + "Falling back to UsdFrameView." + ) + + return "usd" def __new__(cls, *args, **kwargs) -> BaseFrameView: """Create a new FrameView for the active physics backend.""" return super().__new__(cls, *args, **kwargs) + + +# Eagerly register UsdFrameView — it lives in isaaclab, not a backend package, +# so FactoryBase's dynamic import (isaaclab_{backend}.sim.views) can't find it. +FrameView.register("usd", UsdFrameView) diff --git a/source/isaaclab_physx/isaaclab_physx/sim/views/fabric_frame_view.py b/source/isaaclab_physx/isaaclab_physx/sim/views/fabric_frame_view.py index ee329c7976e6..4443fe0d9af0 100644 --- a/source/isaaclab_physx/isaaclab_physx/sim/views/fabric_frame_view.py +++ b/source/isaaclab_physx/isaaclab_physx/sim/views/fabric_frame_view.py @@ -15,7 +15,6 @@ from pxr import Usd import isaaclab.sim as sim_utils -from isaaclab.app.settings_manager import SettingsManager from isaaclab.sim.views.base_frame_view import BaseFrameView from isaaclab.sim.views.usd_frame_view import UsdFrameView from isaaclab.utils.warp import ProxyArray @@ -43,22 +42,20 @@ def _to_float32_2d(a: wp.array | torch.Tensor) -> wp.array | torch.Tensor: class FabricFrameView(BaseFrameView): """FrameView with Fabric GPU acceleration for the PhysX backend. - Uses composition: holds a :class:`UsdFrameView` internally for USD - fallback and non-accelerated operations (local poses, visibility, scales - when Fabric is disabled). + This class is only instantiated when Fabric is enabled and the device is + supported. The :class:`~isaaclab.sim.views.FrameView` factory dispatches + to :class:`~isaaclab.sim.views.UsdFrameView` otherwise. - When Fabric is enabled, world-pose and scale operations use Warp kernels - operating on ``omni:fabric:worldMatrix``. Fabric acceleration runs on - the same CUDA device the view was constructed with — ``cuda:0``, - ``cuda:1``, or any other available CUDA index — so this view is safe - to use from distributed-training workers pinned to non-primary GPUs. - All other operations delegate to the internal USD view. +Uses composition: holds a :class:`UsdFrameView` internally for operations + that don't have a Fabric-accelerated path (local poses, visibility). - After every Fabric write (``set_world_poses``, ``set_scales``), - :meth:`PrepareForReuse` is called on the ``PrimSelection`` to notify - the FSD renderer that Fabric data has changed and to detect topology - changes that require rebuilding internal mappings. Read operations - do not call PrepareForReuse to avoid unnecessary renderer invalidation. + World-pose and scale operations use Warp kernels operating on + ``omni:fabric:worldMatrix``. After every Fabric write + (``set_world_poses``, ``set_scales``), :meth:`PrepareForReuse` is called + on the ``PrimSelection`` to notify the FSD renderer that Fabric data has + changed and to detect topology changes that require rebuilding internal + mappings. Read operations do not call PrepareForReuse to avoid + unnecessary renderer invalidation. Pose getters return :class:`~isaaclab.utils.warp.ProxyArray`. Setters accept ``wp.array``. """ @@ -88,12 +85,6 @@ def __init__( self._usd_view = UsdFrameView(prim_path, device=device, validate_xform_ops=validate_xform_ops, stage=stage) self._device = device - settings = SettingsManager.instance() - self._use_fabric = bool(settings.get("/physics/fabricEnabled", False)) - # TODO(pv): Misleading abstraction — FabricFrameView can fall back to USD internally; - # the concrete class should be determined by the factory instead. (PR #5673 pv/fabric-view-no-fallback) - # TODO(pv): Fuse set_world_poses/set_scales into single kernel launch (PR #5674 pv/fabric-fused-compose) - self._fabric_initialized = False self._fabric_usd_sync_done = False self._fabric_selection = None @@ -139,10 +130,6 @@ def set_visibility(self, visibility, indices=None): # ------------------------------------------------------------------ def set_world_poses(self, positions=None, orientations=None, indices=None): - if not self._use_fabric: - self._usd_view.set_world_poses(positions, orientations, indices) - return - if not self._fabric_initialized: self._initialize_fabric() @@ -181,9 +168,6 @@ def set_world_poses(self, positions=None, orientations=None, indices=None): self._fabric_usd_sync_done = True def get_world_poses(self, indices: wp.array | None = None) -> tuple[ProxyArray, ProxyArray]: - if not self._use_fabric: - return self._usd_view.get_world_poses(indices) - if not self._fabric_initialized: self._initialize_fabric() if not self._fabric_usd_sync_done: @@ -234,10 +218,6 @@ def get_local_poses(self, indices: wp.array | None = None) -> tuple[ProxyArray, # ------------------------------------------------------------------ def set_scales(self, scales, indices=None): - if not self._use_fabric: - self._usd_view.set_scales(scales, indices) - return - if not self._fabric_initialized: self._initialize_fabric() @@ -272,9 +252,6 @@ def set_scales(self, scales, indices=None): self._fabric_usd_sync_done = True def get_scales(self, indices=None): - if not self._use_fabric: - return self._usd_view.get_scales(indices) - if not self._fabric_initialized: self._initialize_fabric() if not self._fabric_usd_sync_done: