2727from collections import deque
2828from collections .abc import Callable , Iterator
2929from contextlib import AbstractContextManager , contextmanager
30- from dataclasses import dataclass
30+ from dataclasses import dataclass , field
3131from typing import TYPE_CHECKING , Any , TypeAlias
3232
3333import warp as wp
@@ -209,12 +209,15 @@ def render(
209209
210210@dataclass
211211class _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
0 commit comments