Skip to content

Commit 1997ac6

Browse files
committed
Allocate both staging buffers lazily
The camera buffers were pre-sized from the environment count while the object buffer was allocated on first use, although the camera staging call already reallocates on a row-count mismatch. Allocate both on first use. This removes the asymmetry and the strategy's camera-count bookkeeping.
1 parent 068fe6b commit 1997ac6

2 files changed

Lines changed: 21 additions & 36 deletions

File tree

source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer_strategies.py

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from collections import deque
2828
from collections.abc import Callable, Iterator
2929
from contextlib import AbstractContextManager, contextmanager
30-
from dataclasses import dataclass
30+
from dataclasses import dataclass, field
3131
from typing import TYPE_CHECKING, Any, TypeAlias
3232

3333
import warp as wp
@@ -209,12 +209,15 @@ def render(
209209

210210
@dataclass
211211
class _AsyncRenderSlot:
212-
"""A reusable set of transform staging buffers for one in-flight async update."""
212+
"""A reusable set of transform staging buffers for one in-flight async update.
213213
214-
camera_transforms: wp.array
215-
camera_quats: wp.array
216-
object_transforms: wp.array | None
217-
write_ops: list[Operation]
214+
All buffers are allocated on first use, since only the staging calls know the row counts.
215+
"""
216+
217+
camera_transforms: wp.array | None = None
218+
camera_quats: wp.array | None = None
219+
object_transforms: wp.array | None = None
220+
write_ops: list[Operation] = field(default_factory=list)
218221

219222
def record_write(self, binding: Any, data: wp.array, cuda_stream: int) -> None:
220223
"""Write ``data`` to ``binding`` asynchronously and remember the write op.
@@ -272,7 +275,6 @@ def try_create(cls, cfg: OVRTXRendererCfg) -> _AsyncRenderStrategy | None:
272275

273276
def __init__(self) -> None:
274277
super().__init__()
275-
self._num_envs = 0
276278
self._render_queue_depth = self._LATENCY_FRAMES + 1
277279
self._ring: deque[_AsyncRenderEntry] = deque()
278280
self._slots: list[_AsyncRenderSlot] = []
@@ -302,14 +304,11 @@ def _enqueue_render_op(
302304

303305
def initialize(self, num_envs: int) -> None:
304306
"""Reset the staging slots for a new scene. See :meth:`_RenderStrategy.initialize`."""
305-
self._reset_slots(num_envs)
307+
del num_envs # Staging buffers are allocated on first use, sized by their staging calls.
308+
self._reset_slots()
306309

307-
def _reset_slots(self, num_envs: int) -> None:
308-
"""Finish all queued work, then drop the staging slots.
309-
310-
``num_envs`` is the camera count for future slot builds. ``0`` means the renderer is
311-
unbinding.
312-
"""
310+
def _reset_slots(self) -> None:
311+
"""Finish all queued work, then drop the staging slots."""
313312
# Deliver queued renders rather than dropping them. Each op is its buffer's only keepalive,
314313
# and a re-initialize must not discard a frame that is still executing. This is a no-op
315314
# from cleanup(), which drains the ring first.
@@ -319,7 +318,6 @@ def _reset_slots(self, num_envs: int) -> None:
319318
self._slots.clear()
320319
self._slot_index = 0
321320
self._current_slot = None
322-
self._num_envs = num_envs
323321
self._primed = False
324322
self._ring.clear()
325323

@@ -328,22 +326,13 @@ def _create_slots(self) -> None:
328326
# The other slot still backs the frame in flight. :meth:`_advance_slot` waits out the
329327
# incoming slot's writes. Those writes were submitted before the render that has just
330328
# drained, so they are already complete.
331-
assert self._warp_device is not None
332-
for _ in range(self._NUM_SLOTS):
333-
self._slots.append(
334-
_AsyncRenderSlot(
335-
camera_transforms=wp.zeros(self._num_envs, dtype=wp.mat44d, device=self._warp_device),
336-
camera_quats=wp.empty(self._num_envs, dtype=wp.quatf, device=self._warp_device),
337-
object_transforms=None,
338-
write_ops=[],
339-
)
340-
)
329+
self._slots = [_AsyncRenderSlot() for _ in range(self._NUM_SLOTS)]
341330

342331
def _staging_slot(self) -> _AsyncRenderSlot:
343332
"""The slot that receives this frame's staged transforms, in any staging order.
344333
345-
The slot pool is built on first use, when the device and camera count are known. After
346-
that, only :meth:`_advance_slot` rotates slots. Staging calls never rotate them.
334+
The slot pool is built on first use. After that, only :meth:`_advance_slot` rotates
335+
slots. Staging calls never rotate them.
347336
"""
348337
if not self._slots:
349338
self._create_slots()
@@ -387,11 +376,11 @@ def stage_camera_transforms(self, binding: Any, num_rows: int) -> Iterator[tuple
387376
"""Stage camera transforms into the frame's slot and write them to ``binding`` on exit.
388377
389378
See :meth:`_RenderStrategy.stage_camera_transforms`. Camera and object updates share the
390-
frame's slot in any order. The camera buffers are reallocated when ``num_rows`` differs
391-
from their pre-sized ``num_envs``.
379+
frame's slot in any order. The camera buffers are allocated on first use and reallocated
380+
when ``num_rows`` changes.
392381
"""
393382
slot = self._staging_slot()
394-
if slot.camera_transforms.shape[0] != num_rows:
383+
if slot.camera_transforms is None or slot.camera_transforms.shape[0] != num_rows:
395384
slot.camera_transforms = wp.zeros(num_rows, dtype=wp.mat44d, device=self._warp_device)
396385
slot.camera_quats = wp.empty(num_rows, dtype=wp.quatf, device=self._warp_device)
397386
yield slot.camera_quats, slot.camera_transforms
@@ -466,5 +455,5 @@ def cleanup(self) -> list[Exception]:
466455
except Exception as e:
467456
logger.warning("Error completing OVRTX async binding write during cleanup: %s", e, exc_info=True)
468457
errors.append(e)
469-
self._reset_slots(0)
458+
self._reset_slots()
470459
return errors

source/isaaclab_ov/test/test_ovrtx_scene_write_barrier.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -273,11 +273,7 @@ class _FailingWriteOp:
273273
def wait(self) -> None:
274274
raise RuntimeError("device lost")
275275

276-
strategy._slots.append(
277-
_AsyncRenderSlot(
278-
camera_transforms=None, camera_quats=None, object_transforms=None, write_ops=[_FailingWriteOp()]
279-
)
280-
)
276+
strategy._slots.append(_AsyncRenderSlot(write_ops=[_FailingWriteOp()]))
281277
errors = strategy.cleanup()
282278

283279
assert consumed == [0, 1]

0 commit comments

Comments
 (0)