Repository navigation
Description
While frame-comparing a 46 s, 9-mount project at v0.8.32 (default dedup vs HF_STATIC_DEDUP=false) I found one window the verifier still passes (the visibility-kill case from #3767 is fixed by #3768 and no longer reproduces; this is a different mechanism):
- Frames 1314-1366 (30 fps, t = 43.80-45.53 s), 53 frames, per-frame PSNR 26.5-33.1 dB against the dedup-off render. Every other frame is at >= 43.0 dB, i.e. inside the encoder noise floor (the v0.8.31 vs v0.8.32 dedup-off renders sit at >= 48.0 dB).
- In the default render all 53 frames hold the pose of frame 1311 (>= 64 dB against it); motion resumes at 1367. The same 53-frame signature was already present at v0.8.31 underneath the larger drops, so this is a different gap, not the visibility kill.
Mechanism, from dist/cli.js in the published package:
- The predictor marks a frame static when no tween is active on it (
staticFrameSetis built from the tween ranges ofwindow.__timelines). - The verifier seeks the verification page with
hf.seek(t, { suppressEvents: true }). - Sequential capture seeks with events enabled (the
gsap_callback_dom_measurementdiagnostic says so: "The renderer seeks with suppressEvents=false, so callbacks re-fire on every seek").
So motion written from a timeline onUpdate callback inside a tween-free window is invisible to both prediction and verification but present in the real capture. Here it is a seeded, seek-deterministic 1.4 px "boil" jitter on a title with nothing tweened between 3.7 s and 5.59 s of a 6 s child. Amplitude does not matter; 12 px reproduces the same way.
Upstream impact: the registry's hw-* items drive their boil from a timeline onUpdate in exactly this shape, so those blocks render with a frozen boil inside their static windows under default dedup.
Repro (same shape as the original: two files, generic content). sub-a fades a square in over 0.2-0.8 s, then tl.eventCallback("onUpdate", ...) sets x/y from floor(tl.time() * 30 / 3) (a new pose every 3 frames), plus a tl.set(root, { autoAlpha: 0 }, 2.98) tail so the child timeline spans its 3 s mount (without the tail, seeks clamp at 0.8 s and the jitter legitimately freezes in both modes). Mounted at 5.5 s for 3 s in a 10 s root.
- default:
static-frame dedup: 235/235 predicted frame(s) reusable (outcome=verified, runs=5/5, ...); frames 192-253 differ from the dedup-off render (56 of 62 frames < 45 dB, min 30.5 dB); the square holds one pose for the whole window. HF_STATIC_DEDUP=false: the square steps every 3 frames as authored.
Two fixes look possible from the outside: seek the verification page with events on, matching capture; or have the predictor treat a timeline that carries an onUpdate (or any per-seek callback) as non-static across its whole span. Repro files and the two repro MP4s are attached.
repro-onupdate-static-dedup.zip
Default dedup, square holds one pose for the whole window:
https://github.com/user-attachments/assets/31502e80-e821-43e5-a421-c0b58200de74
Dedup off (HF_STATIC_DEDUP=false), square steps every 3 frames as authored:
https://github.com/user-attachments/assets/929e22e3-650c-4ac2-89c3-43efe292dffb
Activity
fix(engine): detect onUpdate callbacks and enable verification events (…
jbernard077 commentedon Sep 9, 2026
Confirmed fixed on v0.8.33, thanks for the fast turnaround. With the attached repro, the default-dedup render is now byte-identical to the dedup-off render (same MD5). On the 46 s, 9-mount project that surfaced this, the default render is within encoder noise of the dedup-off render on all 1380 frames (min 48.0 dB per-frame PSNR), and frames 1314-1366, the window that used to hold one pose, are bit-identical. The dedup log reflects the predictor change too: the predicted-static count for that project dropped from 248/252 to 190/194 once the
onUpdatespan was excluded, andoutcome=verifiedstill holds. We have removed ourHF_STATIC_DEDUP=falseopt-out.