Skip to content

Commit 1b58019

Browse files
committed
fix(mjlab): settle CGL draws after scene changes
1 parent f5a16ee commit 1b58019

6 files changed

Lines changed: 32 additions & 7 deletions

File tree

docs/env/mjlab.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,13 @@ truncation, physics state, and public rendering at multiple steps.
262262
The oracle uses the upstream ``auto_reset=False`` option to retain terminal
263263
observations; EnvPool resets a completed slot on its next step.
264264

265+
On macOS, native and oracle rendering settle each new frame with a second GPU
266+
draw. A captured scene with identical camera, geometry, and lighting bytes
267+
reproduces a one-level color difference on CGL/Metal's first draw; repeated
268+
readback alone does not remove it. This does not advance physics or change the
269+
scene, and pixel comparisons remain exact. Other families and platforms keep
270+
their existing rendering path.
271+
265272
Independent tests observe native resets without oracle synchronization. They
266273
check different seeds, consecutive resets, parallel slots, and replay of the
267274
whole reset sequence. Independently randomized goals, poses, model properties,

envpool/mujoco/mjlab/mjlab_env.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ class MjlabEnv : public Env<MjlabEnvSpec>, public RenderableEnv {
146146
}
147147
renderer_->Render(simulation_.physics.Model(),
148148
simulation_.physics.RenderData(), width, height, camera,
149-
output, &camera_, &option_, true);
149+
output, &camera_, &option_, true, true);
150150
}
151151

152152
#ifdef ENVPOOL_TEST

envpool/mujoco/mjlab/mjlab_test.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ def test_rollout_and_render(self, task: str) -> None:
261261
self.assertTrue(
262262
left.observation_space[name].contains(lo[name][0]), name
263263
)
264-
if step in (0, 32, 97, 194):
264+
if step in (0, 1, 32, 97, 194):
265265
a, b = (
266266
left.render(env_ids=[1, 0]),
267267
right.render(env_ids=[1, 0]),
@@ -275,6 +275,9 @@ def test_rollout_and_render(self, task: str) -> None:
275275
np.testing.assert_array_equal(
276276
a[:1], left.render(env_ids=[1])
277277
)
278+
np.testing.assert_array_equal(
279+
a, left.render(env_ids=[1, 0])
280+
)
278281
self.assertGreater(int(a.max()) - int(a.min()), 20)
279282
control = np.repeat(control[None], 2, axis=0)
280283
lo, lr, lt, lx, li = left.step(control, env_id=li["env_id"])

envpool/mujoco/mjlab/oracle_probe.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,14 @@ def record(
251251
# geometry without advancing or synchronizing physics.
252252
renderer.update(env.sim.data)
253253
frame = renderer.render()
254-
if step == 0 and platform.system() == "Darwin":
255-
for _ in range(4):
256-
renderer.update(env.sim.data)
254+
if platform.system() == "Darwin":
255+
# Match the native CGL warmup and per-frame draw
256+
# settling, without advancing or synchronizing physics.
257+
if step == 0:
258+
for _ in range(4):
259+
renderer.update(env.sim.data)
260+
frame = renderer.render()
261+
else:
257262
frame = renderer.render()
258263
frames.append(frame.copy())
259264
frame_steps.append(step)

envpool/mujoco/offscreen_renderer.cc

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,8 @@ void OffscreenRenderer::Render(const mjModel* model, mjData* data, int width,
817817
int height, int camera_id, unsigned char* rgb,
818818
const mjvCamera* camera_override,
819819
const mjvOption* option_override,
820-
bool update_camera_first) {
820+
bool update_camera_first,
821+
bool settle_cgl_frame) {
821822
if (!initialized_) {
822823
Initialize(model);
823824
}
@@ -873,9 +874,18 @@ void OffscreenRenderer::Render(const mjModel* model, mjData* data, int width,
873874
}
874875
cgl_first_frame_settled_ = true;
875876
} else {
877+
if (settle_cgl_frame) {
878+
// CGL/Metal can leave the first draw of a new scene unsettled, even
879+
// after context warmup. Replaying identical camera/geom/light bytes
880+
// reproduces one-quantum color changes; another readback does not help.
881+
// Only callers requiring per-frame settling pay for this second draw.
882+
mjr_finish();
883+
mjr_render(viewport, &scene_, &context_);
884+
}
876885
read_pixels();
877886
}
878887
#else
888+
(void)settle_cgl_frame;
879889
read_pixels();
880890
#endif
881891

envpool/mujoco/offscreen_renderer.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class OffscreenRenderer {
5555
int camera_id, unsigned char* rgb,
5656
const mjvCamera* camera_override = nullptr,
5757
const mjvOption* option_override = nullptr,
58-
bool update_camera_first = false);
58+
bool update_camera_first = false, bool settle_cgl_frame = false);
5959

6060
private:
6161
void Initialize(const mjModel* model);

0 commit comments

Comments
 (0)