Skip to content

Commit 2af0251

Browse files
ooctipusisaaclab-bot[bot]
authored andcommitted
[Newton] Refresh FK before ray-cast sensor reads (#7523)
# Description Fixes #7236. Newton ray-cast tasks read `NewtonManager.get_state_0().body_q` inside their graph-capturable query pipeline. After an in-step joint or root-state write, the reset masks are current but the derived `body_q` remains stale until forward kinematics runs. This made the first sensor observation after a reset use the previous pose. This fixes the stale read at its ownership boundary instead of adding eager simulator synchronization to the RL environment loops: - `NewtonManager._update_sensor_tasks()` obtains state through the guarded `get_state()` accessor before BVH refit and sensor graph capture/replay. - The graph-captured raycast callback keeps using raw `get_state_0()` state, so `forward()` is never captured. - The ray-caster's direct `get_world_poses()` accessor applies the same lazy-FK rule. - The renderer's now-redundant state refresh is removed because the sensor-task scheduler owns freshness for both renderer and raycast consumers. This is an alternative to the in-step synchronization part of #7516. Its regex and legacy tracked-target fixes are independent of this PR. ## Architecture and performance The state access boundary owns FK freshness; environment stepping remains unaware of individual sensor requirements. The guard runs only when a sensor or renderer update is requested. Rendering does not gain an additional refresh because its existing call moved into the shared scheduler, and Newton's device-resident reset masks limit the actual FK work to invalidated worlds/articulations. ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Release backport - [x] <!-- backport-active-release --> This PR already targets the active release branch; do not backport it again. ## Screenshots Not applicable. ## Validation - Added observable regressions for sensor-data reads and direct pose-getter reads immediately after a carrier root-pose write, with no intervening simulation step or FK-sensitive asset getter. Both tests failed before the fix by exactly the authored displacement in eager and CUDA-graph modes. - `uv run --frozen --extra test python -m pytest source/isaaclab_newton/test/sensors/test_newton_raycast_sensor.py -vv` (16 passed) - `uv run --frozen --extra test python -m pytest source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py source/isaaclab/test/sim/test_newton_manager_visualization_state.py -q` (194 passed) - `uv run --frozen isaaclab -f` (all checks passed) ## Checklist - [x] I have read and understood the contribution guidelines - [x] I have run the pre-commit checks - [x] No standalone documentation change is required; the ownership rule is documented at the access boundaries - [x] My changes generate no new warnings - [x] I have added tests that prove the fix is effective - [x] I have added a changelog fragment for the touched package - [x] My name already exists in `CONTRIBUTORS.md` (cherry picked from commit 8365c57)
1 parent d719f9f commit 2af0251

6 files changed

Lines changed: 56 additions & 10 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed Newton ray-caster updates reading stale carrier poses after joint or root state writes.

source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2658,10 +2658,10 @@ def get_state_0(cls) -> State:
26582658

26592659
@classmethod
26602660
def get_state(cls, scene_data_provider: SceneDataProvider | None = None) -> State:
2661-
"""Get the current Newton state for visualization.
2661+
"""Get the current Newton state with derived transforms refreshed.
26622662
2663-
Use this method from visualizers/renderers/video recorders that need a
2664-
backend-agnostic Newton ``State``. When the sim backend is PhysX this
2663+
Use this method from sensors, visualizers, renderers, and video recorders that need
2664+
a backend-agnostic Newton ``State``. When the sim backend is PhysX this
26652665
refreshes the shadow ``_state_0.body_q`` from the live PhysX scene via
26662666
:meth:`update_visualization_state` before returning, so callers never
26672667
observe stale transforms. Under the Newton sim backend, pending
@@ -2708,12 +2708,13 @@ def _unregister_sensor_task(cls, name: str) -> None:
27082708

27092709
@classmethod
27102710
def _update_sensor_tasks(cls, *names: str) -> None:
2711-
"""Refit the shape and particle BVHs and run the requested scene-query tasks."""
2711+
"""Refresh derived state, refit the BVHs, and run the requested scene-query tasks."""
27122712
for name in names:
27132713
if name not in cls._sensor_tasks:
27142714
raise KeyError(f"Newton sensor task '{name}' is not registered.")
27152715

2716-
state = cls.get_state_0()
2716+
# Resolve pending FK before entering the graph-capturable sensor pipeline.
2717+
state = cls.get_state()
27172718
if state is not cls._sensor_state:
27182719
cls._sensor_state = state
27192720
cls._sensor_state_dirty = True

source/isaaclab_newton/isaaclab_newton/renderers/newton_warp_renderer.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -551,8 +551,6 @@ def update_camera(
551551
def render(self, render_data: RenderData):
552552
"""Render and write to output buffers. See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.render`."""
553553

554-
# Refresh the shadow state under PhysX before the manager refits the BVH.
555-
NewtonManager.get_state()
556554
if render_data.sensor_task_name is None:
557555
render_data.sensor_task_name = f"newton_warp_render:{id(render_data)}"
558556
NewtonManager._register_sensor_task(render_data.sensor_task_name, lambda: self._launch_render(render_data))

source/isaaclab_newton/isaaclab_newton/sensors/ray_caster/newton_raycast_sensor.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ def _update_ray_infos(self: Any, env_mask: wp.array) -> None:
166166
)
167167

168168
def get_world_poses(self: Any, indices=None) -> tuple[ProxyArray, ProxyArray]:
169-
"""Return world poses for legacy camera helpers."""
169+
"""Return current world poses after resolving pending FK."""
170+
NewtonManager.get_state()
170171
self._update_newton_site_transforms(
171172
self._sensor_site_indices, self._newton_pose_w, self._newton_pos_w.warp, self._newton_quat_w.warp
172173
)
@@ -192,7 +193,7 @@ def _update_newton_site_transforms(
192193
pos_buf: wp.array,
193194
quat_buf: wp.array,
194195
) -> None:
195-
"""Update site transforms using the manager-bound model and state."""
196+
"""Update site transforms from manager state already refreshed by the caller."""
196197
model = NewtonManager.get_model()
197198
state = NewtonManager.get_state_0()
198199
wp.launch(

source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ def test_sensor_task_builds_and_refits_bvhs_before_rendering(monkeypatch):
293293
"""Shape and particle BVHs are built and refit before a render task runs."""
294294

295295
state = object()
296-
status = {"shape_refit": False, "particle_refit": False, "rendered": False}
296+
status = {"state_refreshed": False, "shape_refit": False, "particle_refit": False, "rendered": False}
297297

298298
class FakeModel:
299299
shape_count = 1
@@ -320,14 +320,20 @@ def bvh_refit_particles(self, current_state):
320320
model = FakeModel()
321321

322322
def render():
323+
assert status["state_refreshed"]
323324
assert model.bvh_shapes is not None
324325
assert model.bvh_particles is not None
325326
assert status["shape_refit"]
326327
assert status["particle_refit"]
327328
status["rendered"] = True
328329

330+
def get_state(cls):
331+
status["state_refreshed"] = True
332+
return state
333+
329334
monkeypatch.setattr(NewtonManager, "get_model", classmethod(lambda cls: model))
330335
monkeypatch.setattr(NewtonManager, "get_state_0", classmethod(lambda cls: state))
336+
monkeypatch.setattr(NewtonManager, "get_state", classmethod(get_state))
331337
monkeypatch.setattr(NewtonManager, "_model", model, raising=False)
332338
monkeypatch.setattr(NewtonManager, "_sensor_tasks", {}, raising=False)
333339
monkeypatch.setattr(NewtonManager, "_sensor_state", None, raising=False)

source/isaaclab_newton/test/sensors/test_newton_raycast_sensor.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,42 @@ def test_bvh_refit_tracks_moving_geometry(sim):
186186
torch.testing.assert_close(distances, torch.full_like(distances, RAY_START_HEIGHT - 1.0), atol=1e-3, rtol=0)
187187

188188

189+
def test_sensor_read_refreshes_fk_after_carrier_pose_write(sim):
190+
"""The first sensor read after a pose write uses the refreshed carrier pose."""
191+
scene = InteractiveScene(RaycastTestSceneCfg(num_envs=1))
192+
sim.reset()
193+
sensor = _step_and_read(sim, scene)
194+
initial_distances = sensor.data.ray_distances.torch.clone()
195+
196+
sensor_body: RigidObject = scene["sensor_body"]
197+
target_pose = sensor_body.data.root_link_pose_w.torch.clone()
198+
target_pose[:, 2] += 1.0
199+
sensor_body.write_root_pose_to_sim_index(root_pose=target_pose)
200+
sensor.reset()
201+
202+
# Read the sensor first: no simulation step or FK-sensitive asset getter may hide stale body_q.
203+
distances = sensor.data.ray_distances.torch
204+
torch.testing.assert_close(distances, initial_distances + 1.0, atol=1e-3, rtol=0)
205+
206+
207+
def test_world_pose_getter_refreshes_fk_after_carrier_pose_write(sim):
208+
"""The ray-caster pose getter resolves pending FK before reading ``body_q``."""
209+
scene = InteractiveScene(RaycastTestSceneCfg(num_envs=1))
210+
sim.reset()
211+
sensor = _step_and_read(sim, scene)
212+
initial_positions = sensor.get_world_poses()[0].torch.clone()
213+
214+
sensor_body: RigidObject = scene["sensor_body"]
215+
target_pose = sensor_body.data.root_link_pose_w.torch.clone()
216+
target_pose[:, 0] += 1.0
217+
sensor_body.write_root_pose_to_sim_index(root_pose=target_pose)
218+
219+
positions = sensor.get_world_poses()[0].torch
220+
expected_positions = initial_positions.clone()
221+
expected_positions[:, 0] += 1.0
222+
torch.testing.assert_close(positions, expected_positions, atol=1e-3, rtol=0)
223+
224+
189225
@configclass
190226
class RaycastCameraSceneCfg(RaycastTestSceneCfg):
191227
"""Adds a downward-looking Newton tiled camera next to the ray-cast sensor."""

0 commit comments

Comments
 (0)