Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added regression coverage for initial object poses in camera observations returned by environment reset.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Added
^^^^^

* Added render scene-state invalidation tracking so backends can request a fresh frame without
exposing backend-specific synchronization through the physics interface.
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 @@ -60,6 +60,8 @@ class RenderContext:
"_prepared_renderer_ids",
"_prepared_num_envs",
"_last_scene_state_step",
"_scene_state_revision",
"_rendered_scene_state_revision",
"_visual_materials",
"_visual_material_batches",
"_visual_material_batches_by_channel",
Expand All @@ -76,6 +78,8 @@ def __init__(self) -> None:
self._prepared_renderer_ids: set[int] = set()
self._prepared_num_envs: int | None = None
self._last_scene_state_step: int | None = None
self._scene_state_revision: int = 0
self._rendered_scene_state_revision: int = -1
self._visual_materials: list[Any] = []
self._visual_material_batches: tuple[VisualMaterialBatch, ...] = ()
self._visual_material_batches_by_channel: dict[str, VisualMaterialBatch] = {}
Expand Down Expand Up @@ -104,6 +108,16 @@ def renderer_types(self) -> tuple[str, ...]:
"""Return the registered camera renderer types."""
return tuple(cfg.renderer_type for cfg, _renderer in self._renderer_entries)

@property
def scene_state_revision(self) -> int:
"""Return the revision incremented by scene mutations outside physics steps."""
return self._scene_state_revision

@property
def scene_state_is_rendered(self) -> bool:
"""Return whether the latest external scene mutations reached a rendered frame."""
return self._rendered_scene_state_revision == self._scene_state_revision

def get_renderer(self, cfg: RendererCfg) -> BaseRenderer:
"""Return a backend for this configuration, reusing a matching instance if present.

Expand Down Expand Up @@ -346,6 +360,19 @@ def render_into_camera(
renderer.render(render_data)
renderer.read_output(render_data, camera_data)

def mark_scene_state_dirty(self) -> None:
"""Invalidate rendered scene state after a mutation outside a physics step."""
self._scene_state_revision += 1
self._last_scene_state_step = None

def mark_scene_state_rendered(self, scene_state_revision: int) -> None:
"""Record that a scene revision reached a rendered frame.

Args:
scene_state_revision: Scene revision captured immediately before producing the frame.
"""
self._rendered_scene_state_revision = max(self._rendered_scene_state_revision, scene_state_revision)

def reset_stage_prepare_flag(self) -> None:
"""Allow :meth:`ensure_prepare_stage` to run ``prepare_stage`` again (e.g. a new USD stage)."""
self._prepared_renderer_ids.clear()
Expand Down Expand Up @@ -384,6 +411,8 @@ def close(self) -> None:
self._prepared_renderer_ids.clear()
self._prepared_num_envs = None
self._last_scene_state_step = None
self._scene_state_revision = 0
self._rendered_scene_state_revision = -1
self._physics_initialized = False
self._visual_materials.clear()
self._visual_material_batches = ()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

"""Regression tests for the PhysX tensor-pose write barrier."""

from unittest.mock import patch

import pytest
import warp as wp
from _articulation_iface_test_utils import BACKENDS as ARTICULATION_BACKENDS
from _articulation_iface_test_utils import get_articulation
from _rigid_object_collection_iface_test_utils import BACKENDS as COLLECTION_BACKENDS
from _rigid_object_collection_iface_test_utils import get_rigid_object_collection
from _rigid_object_iface_test_utils import BACKENDS as RIGID_OBJECT_BACKENDS
from _rigid_object_iface_test_utils import get_rigid_object
from isaaclab_physx.assets.articulation import articulation as articulation_module
from isaaclab_physx.assets.rigid_object import rigid_object as rigid_object_module
from isaaclab_physx.assets.rigid_object_collection import rigid_object_collection as collection_module
from isaaclab_physx.sim.views._pose_tracking_view import _PoseTrackingView

pytestmark = [
pytest.mark.integration,
pytest.mark.skipif(
not all(
"physx" in backends for backends in (RIGID_OBJECT_BACKENDS, COLLECTION_BACKENDS, ARTICULATION_BACKENDS)
),
reason="PhysX backend is unavailable",
),
]


@pytest.mark.parametrize(
("asset_kind", "method_name"),
[
("rigid_object", "write_root_link_pose_to_sim_index"),
("rigid_object", "write_root_com_pose_to_sim_index"),
("collection", "write_body_link_pose_to_sim_index"),
("collection", "write_body_com_pose_to_sim_index"),
("articulation", "write_root_link_pose_to_sim_index"),
("articulation", "write_root_com_pose_to_sim_index"),
],
)
def test_pose_writer_marks_tensor_pose_write(asset_kind: str, method_name: str):
if asset_kind == "rigid_object":
module = rigid_object_module
argument_name = "root_pose"
elif asset_kind == "collection":
module = collection_module
argument_name = "body_poses"
else:
module = articulation_module
argument_name = "root_pose"

with patch.object(module.SimulationManager, "_mark_tensor_pose_write") as mark_pose_write:
if asset_kind == "rigid_object":
asset, _ = get_rigid_object("physx", num_instances=2, device="cpu")
elif asset_kind == "collection":
asset, _ = get_rigid_object_collection("physx", num_instances=2, num_bodies=3, device="cpu")
else:
asset, _ = get_articulation("physx", num_instances=2, num_joints=3, num_bodies=4, device="cpu")
asset._root_view = _PoseTrackingView(asset._root_view, mark_pose_write)
if asset_kind == "collection":
pose = wp.zeros((asset.num_instances, asset.num_bodies), dtype=wp.transformf, device="cpu")
else:
pose = wp.zeros((asset.num_instances,), dtype=wp.transformf, device="cpu")
getattr(asset, method_name)(**{argument_name: pose})

mark_pose_write.assert_called_once_with()
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,32 @@ def test_reset_scene_state_cadence_allows_repeat_update_scene_state_same_step():
assert len(hits) == 2


def test_scene_state_revision_tracks_dirty_and_rendered_state():
"""Rendering an older revision must not clear a newer scene mutation."""
ctx = RenderContext()

assert ctx.scene_state_revision == 0
assert not ctx.scene_state_is_rendered

ctx.mark_scene_state_dirty()
first_revision = ctx.scene_state_revision

assert first_revision == 1
assert not ctx.scene_state_is_rendered

ctx.mark_scene_state_dirty()

assert ctx.scene_state_revision == 2

ctx.mark_scene_state_rendered(first_revision)

assert not ctx.scene_state_is_rendered

ctx.mark_scene_state_rendered(ctx.scene_state_revision)

assert ctx.scene_state_is_rendered


def test_close_closes_every_backend_once_and_drops_them():
"""``close`` closes each registered backend exactly once and empties the context."""
ctx = RenderContext()
Expand Down
Loading
Loading