Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ Guidelines for modifications:
* Ruben Grandia
* Ryan Gresia
* Ryley McCarroll
* ruziniuuuuu
* Sahara Yuta
* Sergey Grizan
* Shafeef Omar
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Added
^^^^^

* Added a renderer-neutral API for synchronizing targeted scene attributes at runtime.
24 changes: 24 additions & 0 deletions source/isaaclab/isaaclab/renderers/base_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,30 @@ def update_geometries(self) -> None:
"""
pass

def update_scene_attribute(
self,
prim_paths: list[str],
attribute_name: str,
values: Any,
*,
is_asset_path: bool = False,
) -> None:
"""Update one scalar scene attribute across a set of prims.

This hook is for authored scene state that is neither a transform nor mutable geometry,
such as light parameters and material inputs. Backends that render directly from the live
USD stage may keep the default no-op implementation. Backends with a private scene
representation override it to synchronize the values.

Args:
prim_paths: Target prim paths. Each path corresponds to one element in ``values``.
attribute_name: USD attribute name shared by the target prims.
values: Per-prim scalar values as a NumPy array, tensor, or list.
is_asset_path: Whether string values carry the USD ``asset`` semantic rather than the
default numeric or token-string semantic.
"""
return

@abstractmethod
def update_camera(
self,
Expand Down
29 changes: 29 additions & 0 deletions source/isaaclab/isaaclab/renderers/render_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,35 @@ def update_scene_state(self, physics_step_count: int) -> None:

self._last_scene_state_step = physics_step_count

def update_scene_attribute(
self,
prim_paths: list[str],
attribute_name: str,
values: Any,
*,
is_asset_path: bool = False,
) -> None:
"""Synchronize one scalar scene attribute to every registered renderer.

Environment code should author the same values on the live USD stage before calling this
method. Renderers that consume live USD need no additional work, while renderers that own
a private stage receive the targeted update through :class:`BaseRenderer`.

Args:
prim_paths: Target prim paths. Each path corresponds to one element in ``values``.
attribute_name: USD attribute name shared by the target prims.
values: Per-prim scalar values as a NumPy array, tensor, or list.
is_asset_path: Whether string values carry the USD ``asset`` semantic rather than the
default numeric or token-string semantic.
"""
for _cfg, renderer in self._renderer_entries:
renderer.update_scene_attribute(
prim_paths,
attribute_name,
values,
is_asset_path=is_asset_path,
)

def render_into_camera(
self,
renderer: BaseRenderer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class _FakeBackend(BaseRenderer):
"_prepare_hits",
"_update_transforms_hits",
"_update_geometries_hits",
"_scene_attribute_updates",
"_event_log",
"_close_hits",
"_close_raises",
Expand All @@ -47,6 +48,7 @@ def __init__(
prepare_hits: list[int] | None = None,
update_transforms_hits: list[int] | None = None,
update_geometries_hits: list[int] | None = None,
scene_attribute_updates: list[tuple[list[str], str, Any, bool]] | None = None,
event_log: list[str] | None = None,
close_hits: list[Any] | None = None,
close_raises: bool = False,
Expand All @@ -55,6 +57,7 @@ def __init__(
self._prepare_hits = prepare_hits
self._update_transforms_hits = update_transforms_hits
self._update_geometries_hits = update_geometries_hits
self._scene_attribute_updates = scene_attribute_updates
self._event_log = event_log
self._close_hits = close_hits
self._close_raises = close_raises
Expand Down Expand Up @@ -84,6 +87,17 @@ def update_geometries(self) -> None:
if self._event_log is not None:
self._event_log.append("geo")

def update_scene_attribute(
self,
prim_paths: list[str],
attribute_name: str,
values: Any,
*,
is_asset_path: bool = False,
) -> None:
if self._scene_attribute_updates is not None:
self._scene_attribute_updates.append((prim_paths, attribute_name, values, is_asset_path))

def update_camera(self, render_data: Any, positions: Any, orientations: Any, intrinsics: Any) -> None:
pass

Expand Down Expand Up @@ -185,6 +199,37 @@ def test_update_scene_state_dedupes_per_physics_step():
assert len(geometry_hits) == 2


def test_update_scene_attribute_dispatches_to_every_backend():
"""Targeted scene updates reach all registered renderers without backend inspection."""
ctx = RenderContext()
updates: list[tuple[list[str], str, Any, bool]] = []
first = _FakeBackend(scene_attribute_updates=updates)
second = _FakeBackend(scene_attribute_updates=updates)
_set_entries(ctx, (IsaacRtxRendererCfg(), first), (NewtonWarpRendererCfg(), second))

paths = ["/World/envs/env_0/Light", "/World/envs/env_1/Light"]
values = [1000.0, 1200.0]
ctx.update_scene_attribute(paths, "intensity", values)

assert updates == [
(paths, "intensity", values, False),
(paths, "intensity", values, False),
]


def test_update_scene_attribute_marks_asset_path_values():
"""The USD asset-path semantic is forwarded explicitly to renderer backends."""
ctx = RenderContext()
updates: list[tuple[list[str], str, Any, bool]] = []
_set_entries(ctx, (IsaacRtxRendererCfg(), _FakeBackend(scene_attribute_updates=updates)))

paths = ["/World/envs/env_0/Light"]
values = ["/textures/studio.hdr"]
ctx.update_scene_attribute(paths, "texture:file", values, is_asset_path=True)

assert updates == [(paths, "texture:file", values, True)]


def test_render_into_camera_calls_update_render_read_order():
"""render_into_camera runs scene sync then render then read_output; dedupes sync per step."""
ctx = RenderContext()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Added
^^^^^

* Added runtime scene-attribute synchronization for legacy and ovstage OVRTX stages.
125 changes: 125 additions & 0 deletions source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ def _xform_tensor_from_numpy(xforms: np.ndarray) -> Any:
# via DLPack, so a lanes=3 override on a host numpy array is required to match the column.
_OVSTAGE_POINT_DTYPE = ovstage.DLDataType(code=ovstage.DLDataTypeCode.kDLFloat, bits=32, lanes=3)

# Scalar USD asset paths are stored by ovstage as one authored/resolved token-id pair per
# prim. Absolute paths have the same authored and resolved value, which is the only form this
# targeted runtime-update API accepts.
_OVSTAGE_ASSET_PATH_DTYPE = ovstage.DLDataType(code=ovstage.DLDataTypeCode.kDLUInt, bits=64, lanes=2)

def _points_tensor_from_numpy(points: np.ndarray) -> Any:
"""Wrap an ``(N, 3)`` float32 array as a 3-lane DLTensor for ``points`` writes.

Expand Down Expand Up @@ -383,6 +388,7 @@ def __init__(self, cfg: OVRTXRendererCfg):
self._particle_visual_offsets: list[int] = []
self._particle_visual_counts: list[int] = []
self._initialized_scene = False
self._pending_scene_attribute_updates: list[tuple[list[str], str, Any, bool]] = []
self._exported_usd_string: str | None = None
self._camera_rel_path: str | None = None
self._output_id_color_buffers: dict[str, wp.array] = {}
Expand Down Expand Up @@ -541,6 +547,7 @@ def _initialize_from_spec_legacy(self, spec: CameraRenderSpec):
)

self._initialized_scene = True
self._flush_pending_scene_attribute_updates()

self._camera_xform_binding = self._renderer.bind_attribute(
prim_paths=camera_paths,
Expand Down Expand Up @@ -1455,6 +1462,7 @@ def _safe_unbind(binding, name: str) -> None:
self._render_product_paths.clear()
self._output_id_color_buffers.clear()
self._initialized_scene = False
self._pending_scene_attribute_updates.clear()

# ---------------------------------------------------------------------------
# Dispatch methods — route to ovstage or legacy implementation
Expand Down Expand Up @@ -1504,6 +1512,121 @@ def update_geometries(self) -> None:
else:
self._update_geometries_legacy()

def update_scene_attribute(
self,
prim_paths: list[str],
attribute_name: str,
values: Any,
*,
is_asset_path: bool = False,
) -> None:
"""Synchronize one scalar scene attribute to the private OVRTX stage.

Updates submitted before the stage is initialized are copied and applied after OVRTX has
loaded and cloned the scene. This lets pre-startup environment events use the same API as
reset and interval events.

Args:
prim_paths: Target prim paths. Each path corresponds to one element in ``values``.
attribute_name: USD attribute name shared by the target prims.
values: Per-prim scalar values as a NumPy array, tensor, or list.
is_asset_path: Whether string values carry the USD ``asset`` semantic.
"""
if not self._initialized_scene:
self._pending_scene_attribute_updates.append(
(list(prim_paths), attribute_name, self._copy_scene_attribute_values(values), is_asset_path)
)
return
self._write_scene_attribute(prim_paths, attribute_name, values, is_asset_path)

@staticmethod
def _copy_scene_attribute_values(values: Any) -> Any:
"""Copy caller-owned values retained until scene initialization."""
if isinstance(values, np.ndarray):
return values.copy()
if isinstance(values, torch.Tensor):
return values.clone()
if isinstance(values, list):
return list(values)
if isinstance(values, tuple):
return tuple(values)
return values

def _flush_pending_scene_attribute_updates(self) -> None:
"""Apply queued scene updates in submission order after initialization."""
while self._pending_scene_attribute_updates:
prim_paths, attribute_name, values, is_asset_path = self._pending_scene_attribute_updates[0]
self._write_scene_attribute(prim_paths, attribute_name, values, is_asset_path)
self._pending_scene_attribute_updates.pop(0)

def _write_scene_attribute(
self,
prim_paths: list[str],
attribute_name: str,
values: Any,
is_asset_path: bool,
) -> None:
"""Write an attribute through the selected OVRTX stage API."""
if self._use_ovstage:
if self._stage is None or self._stage_paths is None:
raise RuntimeError("OVRTX ovstage scene is marked initialized without a stage")
path_list = self._stage_paths.create_path_list_from_strings(prim_paths)
try:
with self._stage.query_from_path_list(path_list) as query:
if is_asset_path:
if not isinstance(values, (list, tuple)) or not all(isinstance(value, str) for value in values):
raise TypeError("Asset-path scene attributes require a list or tuple of strings")
if not all(os.path.isabs(value) for value in values):
raise ValueError("Asset-path scene attributes require absolute paths")
token_ids = np.array(
[token for value in values for token in [self._stage_paths.intern_token(value)] * 2],
dtype=np.uint64,
)
tensors = ovstage.make_dltensor(
token_ids,
dtype=_OVSTAGE_ASSET_PATH_DTYPE,
shape=[len(values)],
)
self._stage.write_attribute(
query,
attribute_name,
ordinal=self._current_ordinal,
tensors=tensors,
is_array=False,
).wait()
elif isinstance(values, (list, tuple)) and all(isinstance(value, str) for value in values):
token_ids = np.array(
[self._stage_paths.intern_token(value) for value in values],
dtype=np.uint64,
)
self._stage.write_attribute(
query,
attribute_name,
ordinal=self._current_ordinal,
tensors=token_ids,
is_array=False,
semantic=ovstage.AttributeSemantic.TOKEN_ID,
).wait()
else:
self._stage.write_attribute(
query,
attribute_name,
ordinal=self._current_ordinal,
tensors=values,
is_array=False,
).wait()
finally:
self._stage_paths.destroy_path_list(path_list)
return

if self._renderer is None:
raise RuntimeError("OVRTX scene is marked initialized without a renderer")
self._renderer.write_attribute(
prim_paths=prim_paths,
attribute_name=attribute_name,
tensor=values,
)

def update_camera(
self,
render_data: OVRTXRenderData,
Expand Down Expand Up @@ -1631,6 +1754,7 @@ def _initialize_from_spec_ovstage(self, spec: CameraRenderSpec) -> None:
self._update_scene_partitions_after_clone_ovstage(num_envs)

self._initialized_scene = True
self._flush_pending_scene_attribute_updates()

camera_paths = [f"/World/envs/env_{i}/{self._camera_rel_path}" for i in range(num_envs)]

Expand Down Expand Up @@ -2244,3 +2368,4 @@ def _safe_destroy_path_list(path_list, name: str) -> None:
self._output_id_color_buffers.clear()
self._initialized_scene = False
self._current_ordinal = 0
self._pending_scene_attribute_updates.clear()
Loading