Skip to content

Commit 3fd4f48

Browse files
Patch caller sensor _num_envs from OvPhysxFrameView
Under OVPhysX's clone_usd=False scenes (the default for InteractiveScene), USD only holds env_0 -- env_1..N are physics-layer clones via physx.clone(). SensorBase.__init__ derives _num_envs from len(find_matching_prims(...)) so any FrameView-using sensor (RayCaster, MultiMeshRayCaster, Camera) sees _num_envs=1 even when the scene has many envs. The sensor's _reset_mask_torch is then sized 1, and the first reset(env_ids=[0..N-1]) triggers a CUDA assert from out-of-bounds indexing. Walk the call stack at construction time to capture the sensor that owns this view, then at the end of _initialize_impl re-allocate its env-sized buffers (_ALL_ENV_MASK, _reset_mask + torch view, _is_outdated, _timestamp, _timestamp_last_update) to match the OVPhysX RIGID_BODY_POSE binding's row count. Duck-typed -- works for any SensorBase subclass without an isaaclab.sensors import dependency. Mirrors the local fix the OVPhysX ContactSensor already applies to itself at contact_sensor.py:240-248. This is a hack confined to the OVPhysX backend. A cleaner long-term fix would source _num_envs from InteractiveScene.num_envs (or move the under-cloning behaviour to be opt-in for rendering rather than backend- keyed), but both are larger changes that touch core IsaacLab. Verified: Anymal-D rough velocity env with presets=ovphysx + 64 envs now initializes past env.reset() without the CUDA assert.
1 parent dbe9e51 commit 3fd4f48

1 file changed

Lines changed: 74 additions & 0 deletions

File tree

source/isaaclab_ovphysx/isaaclab_ovphysx/sim/views/ovphysx_frame_view.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import logging
1111
import re
12+
import sys
1213
from typing import Any
1314

1415
import warp as wp
@@ -23,6 +24,19 @@
2324

2425
from isaaclab_ovphysx.physics import OvPhysxManager
2526

27+
# Attributes that ``SensorBase.__init__`` sizes from ``len(_parent_prims)`` and
28+
# that must be re-allocated when an :class:`OvPhysxFrameView` discovers a larger
29+
# binding count under ``clone_usd=False``. Used by :meth:`_patch_caller_sensor`.
30+
_SENSOR_BASE_ENV_SIZED_ATTRS = (
31+
"_num_envs",
32+
"_ALL_ENV_MASK",
33+
"_reset_mask",
34+
"_reset_mask_torch",
35+
"_is_outdated",
36+
"_timestamp",
37+
"_timestamp_last_update",
38+
)
39+
2640
logger = logging.getLogger(__name__)
2741

2842
WORLD_BODY_INDEX = -1
@@ -318,6 +332,12 @@ def __init__(self, prim_path: str, device: str = "cpu", stage: Usd.Stage | None
318332
# Lazy USD view for scales / visibility.
319333
self._usd_view: UsdFrameView | None = None
320334

335+
# HACK: Capture the caller sensor (if any) so we can patch its env-sized
336+
# buffers once the binding count is known. Required because
337+
# ``SensorBase.__init__`` sizes ``_num_envs`` from ``find_matching_prims``
338+
# which returns only env_0 under OVPhysX's ``clone_usd=False`` scenes.
339+
self._caller_sensor = self._find_caller_sensor()
340+
321341
# Try synchronous init; defer to PHYSICS_READY if the PhysX instance is not yet alive.
322342
physx = self._try_get_physx()
323343
if physx is not None:
@@ -334,6 +354,55 @@ def _try_get_physx() -> Any | None:
334354
"""Return the active OVPhysX ``PhysX`` instance, or ``None`` if not yet created."""
335355
return OvPhysxManager.get_physx_instance()
336356

357+
@staticmethod
358+
def _find_caller_sensor() -> Any | None:
359+
"""Walk the call stack to find a ``SensorBase``-like instance that owns this view.
360+
361+
Duck-typed: any object on the stack that exposes the full set of
362+
env-sized attributes that ``SensorBase.__init__`` allocates is treated
363+
as a sensor. Returns ``None`` when the view is constructed outside a
364+
sensor (e.g. directly by user code or via scene ``extras``).
365+
"""
366+
frame = sys._getframe(1)
367+
while frame is not None:
368+
candidate = frame.f_locals.get("self")
369+
if candidate is not None and all(hasattr(candidate, attr) for attr in _SENSOR_BASE_ENV_SIZED_ATTRS):
370+
return candidate
371+
frame = frame.f_back
372+
return None
373+
374+
def _patch_caller_sensor_env_count(self) -> None:
375+
"""Re-allocate the caller sensor's env-sized buffers to match the binding count.
376+
377+
HACK: Works around ``SensorBase.__init__`` deriving ``_num_envs`` from
378+
``len(_parent_prims)`` -- under OVPhysX's ``clone_usd=False`` scenes only
379+
env_0 exists in USD, so the sensor sees ``_num_envs = 1`` while the
380+
underlying RIGID_BODY_POSE binding correctly exposes all envs. Without
381+
this patch, any sensor that calls ``reset(env_ids=[0..N-1])`` indexes
382+
past its 1-element ``_reset_mask_torch`` and triggers a CUDA assert.
383+
384+
Mirrors the local fix the OVPhysX ``ContactSensor`` applies to itself at
385+
``contact_sensor.py:240-248``.
386+
"""
387+
sensor = self._caller_sensor
388+
if sensor is None or sensor._num_envs == self.count:
389+
return
390+
new_count = self.count
391+
device = sensor._device
392+
sensor._num_envs = new_count
393+
sensor._ALL_ENV_MASK = wp.ones((new_count,), dtype=wp.bool, device=device)
394+
sensor._reset_mask = wp.zeros((new_count,), dtype=wp.bool, device=device)
395+
sensor._reset_mask_torch = wp.to_torch(sensor._reset_mask)
396+
sensor._is_outdated = wp.ones(new_count, dtype=wp.bool, device=device)
397+
sensor._timestamp = wp.zeros(new_count, dtype=wp.float32, device=device)
398+
sensor._timestamp_last_update = wp.zeros_like(sensor._timestamp)
399+
logger.info(
400+
"OvPhysxFrameView: resized %s._num_envs from prior value to %d to match "
401+
"OVPhysX binding count (clone_usd=False scene).",
402+
type(sensor).__name__,
403+
new_count,
404+
)
405+
337406
def _on_physics_ready(self, _event) -> None:
338407
"""Callback invoked when the OVPhysX ``PhysX`` instance becomes available."""
339408
physx = self._try_get_physx()
@@ -473,6 +542,11 @@ def _initialize_impl(self, physx: Any) -> None:
473542
self._local_pos_ta = ProxyArray(self._local_pos_buf)
474543
self._local_quat_ta = ProxyArray(self._local_quat_buf)
475544

545+
# 8. HACK: patch the caller sensor's env-sized buffers if it derived
546+
# its env count from the (under-counting) USD prim list. See
547+
# :meth:`_patch_caller_sensor_env_count` for context.
548+
self._patch_caller_sensor_env_count()
549+
476550
def _resolve_rigid_body_ancestor(
477551
self,
478552
prim: Usd.Prim,

0 commit comments

Comments
 (0)