Skip to content

fix(sim): stamp camera output at render time and pace on the monotonic clock - #3690

Merged
mustafab0 merged 6 commits into
mainfrom
mustafa/fix/sim-camera-timing
Aug 26, 2026
Merged

fix(sim): stamp camera output at render time and pace on the monotonic clock#3690
mustafab0 merged 6 commits into
mainfrom
mustafa/fix/sim-camera-timing

Conversation

@mustafab0

@mustafab0 mustafab0 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

What

The camera publish loop stamped images, TF, and camera_info with time.time() sampled when the frame was consumed, not with the timestamp the frame already carries from when it was rendered.

Why it matters

Every message for a frame was skewed later than the observation it describes by the render-to-publish latency, measured live at 4.5-101.7 ms (mean 52.7 ms), which is the same order as the 0.1 s TF lookup tolerance and can register object poses against the wrong TF sample.

…c clock

The camera publish loop stamped images, TF, and camera_info with time.time()
sampled when the frame was *consumed*, while the frame itself carries the
timestamp taken when it was *rendered* (MujocoEngine._sim_loop passes
loop_start into _render_cameras). Everything published for one frame was
therefore skewed later than the observation it describes by the render->publish
latency, up to a full publish interval (~67ms at the default fps=15). That is
the same order as the 0.1s TF lookup tolerance, so perception could match an
image against a neighbouring TF sample and register object poses at the wrong
depth.

Stamp images, TF, and camera_info from frame.timestamp so all three agree on
the instant the frame was rendered.

Pacing now measures a monotonic loop_start rather than time.time() - ts. With
ts sampled from the frame, wall-minus-stamp is no longer ~0, so reusing it
would have driven sleep_time negative and dropped the loop to render rate
instead of config.fps. Measuring the loop body directly keeps pacing correct
whatever frame.timestamp means, including if the engine later moves to sim time.

camera_info is published on its own subscription rather than per frame, so the
latest frame stamp is tracked and reused, falling back to wall time only before
the first frame has arrived. The get_color_camera_info/get_depth_camera_info
RPCs stamp the same way; they were also on wall clock and would otherwise have
disagreed with the published topic.

Tests cover both halves: stamps equal frame.timestamp for images, TF, and
camera_info, and pacing holds at config.fps for frame timestamps near zero and
at 1e6, which fails if pacing is derived from the stamp.
@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.11111% with 8 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
dimos/simulation/engines/mujoco_sim_module.py 74.19% 6 Missing and 2 partials ⚠️
@@            Coverage Diff             @@
##             main    #3690      +/-   ##
==========================================
+ Coverage   77.79%   77.85%   +0.06%     
==========================================
  Files        1289     1291       +2     
  Lines      123511   123747     +236     
  Branches    10823    10856      +33     
==========================================
+ Hits        96091    96349     +258     
+ Misses      24278    24247      -31     
- Partials     3142     3151       +9     
Flag Coverage Δ
OS-ubuntu-24.04-arm 72.85% <91.11%> (+0.07%) ⬆️
OS-ubuntu-latest 74.69% <91.11%> (+0.06%) ⬆️
Py-3.10 74.68% <91.11%> (+0.06%) ⬆️
Py-3.11 74.69% <91.11%> (+0.06%) ⬆️
Py-3.12 74.68% <91.11%> (+0.06%) ⬆️
Py-3.13 74.69% <91.11%> (+0.06%) ⬆️
Py-3.14 74.69% <91.11%> (+0.06%) ⬆️
Py-3.14t 74.68% <91.11%> (+0.06%) ⬆️
SelfHosted-Large 29.97% <13.33%> (-0.02%) ⬇️
SelfHosted-Linux 35.18% <13.33%> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
dimos/simulation/engines/test_mujoco_sim_module.py 76.84% <100.00%> (+5.71%) ⬆️
dimos/simulation/engines/mujoco_sim_module.py 46.03% <74.19%> (+7.09%) ⬆️

... and 9 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@greptile-apps

greptile-apps Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This update makes camera images, transforms, camera metadata, and camera-info responses use the rendered frame timestamp, while publication pacing uses a monotonic clock.

Camera behavior was exercised across a reset. Although the duplicate-frame guard rejects decreasing timestamps in isolation, the MuJoCo renderer supplies increasing wall-clock render timestamps after reset, and the next frame continues to publish. No defects were found.

Confidence Score: 5/5

Safe to merge based on the exercised camera publication and reset behavior.

Focused coverage verifies timestamp alignment, metadata behavior before and after rendering begins, and pacing independent of timestamp magnitude. An executed reset check also confirmed that the renderer's timestamp source remains increasing across a reset.

Files Needing Attention: No files require follow-up.

T-Rex T-Rex Logs

What T-Rex did

  • Executed the repository publish-loop body with a frame at timestamp 100.0, a decreasing frame at 1.0, and a later frame at 101.0; the guard rejected the decreasing frame and resumed publishing at the later timestamp.
  • Executed the repository reset and publish-loop bodies using the MuJoCo renderer timestamp model; reset completed and the next increasing wall-clock frame published.
  • This confirms that reset does not silence camera publication because MuJoCo camera frames are stamped from the wall-clock render loop rather than resettable simulation time.
  • Validated that the timestamp gate is real for engines that supply decreasing camera timestamps; the MuJoCo reset/respawn path does not supply such timestamps because frame creation uses loop_start = time.time().
  • Collected artifacts including the exact-path timestamp harness source and related logs to support inspection.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (1): Last reviewed commit: "fix(sim): stamp camera output at render ..." | Re-trigger Greptile

@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Aug 26, 2026
Comment thread dimos/simulation/engines/mujoco_sim_module.py
_camera_info_ts checked _latest_frame_ts for None and then read it again to
return it. The attribute is written by the MujocoSimPublish thread and cleared
by stop(), and the rx.interval subscription that drives _publish_camera_info is
still live when stop() nulls it (super().stop() runs afterwards), so the second
read could return None after the first saw a stamp, handing None to
CameraInfo(ts=...) and to with_ts() on the camera_info RPCs.

Read the attribute once into a local and branch on that. Each attribute load is
atomic under the GIL, so a single read is sufficient and no lock is needed; the
adjacent _publish_camera_info already uses this pattern for _camera_info_base.

The regression test drives _latest_frame_ts with a PropertyMock that yields a
stamp then None, so a check-then-use returns None and fails.
@github-actions github-actions Bot added ready-to-merge Required CI checks have passed on this PR and removed ready-to-merge Required CI checks have passed on this PR labels Aug 26, 2026
Per review: _camera_info_base and _latest_frame_ts are touched by the
MujocoSimPublish thread, the rx.interval that drives _publish_camera_info, the
RPC thread, and stop(). Add a single threading.Lock covering both.

This also fixes a pre-existing check-then-use that predates this branch:
get_color_camera_info and get_depth_camera_info tested _camera_info_base for
None and then read it again to call with_ts() on it, and _generate_pointcloud
did the same before passing it to PointCloud2.from_rgbd. stop() clears that
field, so either could raise AttributeError on None. Each site now snapshots
under the lock and uses the local.

The lock is held only across the field access. It is not reentrant and is never
held while calling _camera_info_ts(), which takes it again, nor across the
publish-thread join in stop(), which would deadlock until the 2s timeout.

Smoke-checked on xarm-perception-sim: /color_image still publishes at 14.3Hz
against config.fps=15 with 232/233 image stamps matching their camera TF stamp,
so the added contention does not disturb the publish loop.
@github-actions github-actions Bot removed the ready-to-merge Required CI checks have passed on this PR label Aug 26, 2026
@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Aug 26, 2026
@mustafab0
mustafab0 added this pull request to the merge queue Aug 26, 2026
Merged via the queue into main with commit 764e4a9 Aug 26, 2026
30 checks passed
@mustafab0
mustafab0 deleted the mustafa/fix/sim-camera-timing branch August 26, 2026 18:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-merge Required CI checks have passed on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants