Skip to content

static-frame dedup: onUpdate-driven motion in tween-free windows is predicted static and passes verification #3793

Description

@jbernard077

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 (staticFrameSet is built from the tween ranges of window.__timelines).
  • The verifier seeks the verification page with hf.seek(t, { suppressEvents: true }).
  • Sequential capture seeks with events enabled (the gsap_callback_dom_measurement diagnostic 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

  1. added a commit that references this issue on Sep 9, 2026
    1aaa35d
  2. jbernard077 commented on Sep 9, 2026

    @jbernard077
    ContributorAuthor

    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 onUpdate span was excluded, and outcome=verified still holds. We have removed our HF_STATIC_DEDUP=false opt-out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions