Skip to content

Commit 3929056

Browse files
Strip env_1..N from the USD handed to physx.add_usd
InteractiveScene now USD-replicates every env's asset subtree for all backends (clone_usd=True from the previous commit), but OvPhysxManager was still handing the full N-env stage to physx.add_usd. The wheel loaded all N USD-defined bodies as independent prims, so the subsequent physx.clone() ran onto already-populated targets and never produced the clone-lineage that the wheel's create_tensor_binding fast path expects. At 4096 envs this turned every binding-creation call into a multi-second USD enumeration -- the hang in articulation init. Re-shape _warmup_and_load to export an env_0-scoped USD file: 1. Export the full stage to disk (existing flatten-and-write). 2. Re-open the exported file as an Sdf.Layer. 3. Delete every /World/envs/env_<i> prim spec for i != 0 from the layer. 4. Re-export. The live USD stage held by SimulationContext is untouched -- sensors (RayCaster, Camera, ContactSensor discovery) still see N envs and discover _num_envs = N correctly. Only the file passed to the wheel is scoped to env_0 + globals. physx.clone() then repopulates env_1..N at the physics layer with proper lineage, and create_tensor_binding walks a 1-USD-path result that auto-extends across the N clones -- the fast path that clone_usd=False used to give us implicitly. Net effect: keeps the previous commit's clone_usd=True flip (sensor parity across backends) while restoring OVPhysX's per-env scaling. No test changes required; the FrameView/ContactSensor suite stays at 27/27 pass on CPU.
1 parent 66d3325 commit 3929056

1 file changed

Lines changed: 76 additions & 2 deletions

File tree

source/isaaclab_ovphysx/isaaclab_ovphysx/physics/ovphysx_manager.py

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,64 @@ def get_scene_data_backend(cls) -> SceneDataBackend:
374374
# Internal helpers
375375
# ------------------------------------------------------------------
376376

377+
@staticmethod
378+
def _export_env0_only_stage(sim_stage: Any, target_file: str) -> None:
379+
"""Export the simulation stage to ``target_file`` with env_1..N stripped.
380+
381+
Writes a USD file containing every prim under the live stage **except**
382+
``/World/envs/env_<i>`` for ``i != 0``. Globals (``/physicsScene``,
383+
``/World/ground``, lights, materials, etc.) and ``/World/envs/env_0`` are
384+
retained. ``physx.clone()`` is then expected to repopulate env_1..N at
385+
the physics layer with proper clone lineage so that subsequent
386+
``create_tensor_binding`` calls hit the wheel's fast path.
387+
388+
Implementation: export the full stage to disk, then re-open the result
389+
as an :class:`Sdf.Layer` and delete env_1..N prim specs in place. This
390+
avoids mutating the live stage (which other consumers -- sensors,
391+
visualizers -- still see in its full N-env form).
392+
393+
Args:
394+
sim_stage: Live USD stage held by ``SimulationContext``.
395+
target_file: Output ``.usda`` file path. Overwritten if it exists.
396+
"""
397+
from pxr import Sdf # noqa: PLC0415
398+
399+
# Step 1: full flatten-export of the live stage. We pass the full file
400+
# to ``Sdf.Layer.OpenAsAnonymous`` so the edits below don't write back
401+
# to the source layer on disk.
402+
sim_stage.Export(target_file)
403+
404+
# Step 2: open the exported file as an editable Sdf layer and delete
405+
# ``/World/envs/env_<digits>`` children for digits != 0. Walking the
406+
# ``/World/envs`` ``PrimSpec``'s ``nameChildren`` keeps us scoped to
407+
# the env-namespace and leaves the rest of the stage untouched.
408+
layer = Sdf.Layer.FindOrOpen(target_file)
409+
if layer is None:
410+
raise RuntimeError(
411+
f"OvPhysxManager: failed to re-open exported USD layer at {target_file!r} for env-scoping."
412+
)
413+
envs_spec = layer.GetPrimAtPath("/World/envs")
414+
if envs_spec is None or not envs_spec:
415+
# No /World/envs in the stage (single-env or non-IsaacLab scene); nothing to scope.
416+
logger.debug("OvPhysxManager: no /World/envs prim — exported stage as-is.")
417+
return
418+
419+
env_name_re = re.compile(r"^env_(\d+)$")
420+
names_to_remove = [
421+
child_name
422+
for child_name in list(envs_spec.nameChildren.keys())
423+
if (match := env_name_re.match(child_name)) and match.group(1) != "0"
424+
]
425+
for child_name in names_to_remove:
426+
del envs_spec.nameChildren[child_name]
427+
428+
if names_to_remove:
429+
layer.Export(target_file)
430+
logger.info(
431+
"OvPhysxManager: stripped %d env_<i!=0> subtrees from exported USD (kept env_0 + globals)",
432+
len(names_to_remove),
433+
)
434+
377435
@classmethod
378436
def _warmup_and_load(cls) -> None:
379437
"""Export the USD stage and load it into the ovphysx runtime.
@@ -417,11 +475,27 @@ def _warmup_and_load(cls) -> None:
417475
cls._configure_physx_scene_prim(scene_prim, PhysicsManager._cfg, ovphysx_device)
418476

419477
# Export the current USD stage to a temporary file so ovphysx can load it.
478+
#
479+
# When ``InteractiveScene`` runs with ``clone_usd=True``, the live USD
480+
# stage carries env_0..N's full asset subtrees as authored copies.
481+
# Handing that stage to ``physx.add_usd`` would make the wheel ingest
482+
# all 4096 envs as independent USD-defined bodies, defeating the
483+
# ``physx.clone()`` fast path and turning every subsequent
484+
# ``create_tensor_binding`` call into an O(N) USD enumeration -- the
485+
# hang you'd see at large env counts.
486+
#
487+
# The workaround: strip ``/World/envs/env_<i>`` for i != 0 from the
488+
# exported file before handing it to the wheel. Sensors that read
489+
# USD directly (RayCaster, Camera, ContactSensor discovery) still see
490+
# the full N-env stage; only the wheel-side physics ingestion is
491+
# scoped to env_0, and ``physx.clone()`` re-populates env_1..N in
492+
# the physics runtime with proper clone lineage (which is what the
493+
# binding fast path expects).
420494
cls._tmp_dir = tempfile.TemporaryDirectory(prefix="isaaclab_ovphysx_")
421495
stage_file = os.path.join(cls._tmp_dir.name, "scene.usda")
422-
sim.stage.Export(stage_file)
496+
cls._export_env0_only_stage(sim.stage, stage_file)
423497
cls._stage_path = stage_file
424-
logger.info("OvPhysxManager: exported USD stage to %s", stage_file)
498+
logger.info("OvPhysxManager: exported env_0-scoped USD stage to %s", stage_file)
425499

426500
if cls._physx is None:
427501
cls._construct_physx(ovphysx_device, gpu_index)

0 commit comments

Comments
 (0)