Skip to content

render: parallel worker's first frame still captures pre-seek state — #2477 fix never landed (0.8.72) #4435

Description

@digwis

Describe the bug

Re-opening the #2477 symptom on 0.8.72: parallel capture emits a fully transparent frame at each worker shard's first frame. The fix PR #2489 was closed unmerged, and discardWarmupCapture still has zero call sites on current main (a3d7736) — only its definition in frameCapture.ts and the re-export in engine/src/index.ts.

Observed with a transparent ProRes 4444 render of a canvas/DOM overlay composition (120 frames @ 24fps, --workers auto → 4 workers, screenshot capture mode): frames 30, 60, 90 decode fully transparent — exactly shardStart = w * (totalFrames / workers) for workers 1–3. Worker 0's first frame is naturally identical to t=0 so it's invisible in the alpha scan. --workers 1 produces zero blank frames; the anomaly is deterministic in position across parallel runs but intermittent (some runs clean), matching the paint-settle race described in #2477.

Notable difference from the original report's setup: our composition registers a non-GSAP duck-typed RuntimeTimelineLike on window.__timelines ({duration, time, seek, totalTime, pause, paused, play, add, set} — pure seek→redraw, no GSAP). If the post-#2477 warm-up logic engages only for GSAP timelines or on a code path duck timelines don't reach, that would explain why the regression survives for this comp shape.

Steps to reproduce

The race is timing-sensitive — a heavier page (custom fonts, canvas setup, larger script payload) widens the window. Composition shape that fails for us ~half the time at 4 workers:

<div id="root" data-composition-id="repro" data-duration="4"
     data-width="1920" data-height="1080">
  <div id="box"></div>
</div>
<script>
  var t = 0;
  window.__timelines = window.__timelines || {};
  window.__timelines["repro"] = {
    duration: function(){ return 4; },
    time: function(){ return t; },
    seek: function(s){                      // pure seek->redraw contract
      t = Math.max(0, Number(s) || 0);
      // visible whenever t>0; only t=0 state is blank
      document.getElementById("box").style.opacity = t > 0.02 ? "1" : "0";
    },
    totalTime: function(s){ this.seek(s); },
    pause: function(){}, paused: function(){ return true; },
    play: function(){}, add: function(){}, set: function(){}
  };
</script>

Detection (alpha scan, same method as #2477):

ffmpeg -i out.mov -vf "alphaextract,signalstats,metadata=mode=print" -f null -
# frames at w * ceil(totalFrames/workers) show alpha mean ~0 between normal neighbors

Expected behavior

Every worker's first captured frame contains the seeked state — e.g. wire discardWarmupCapture into the parallel worker path (it already exists and is exported), or gate the first captureFrame on a post-seek paint commit rather than the seek alone.

Actual behavior

First frame of each non-zero worker shard captures the pre-seek page state (t=0). For fade-in/transparent overlays that state is fully transparent, producing visible flicker in the baked output.

Environment

  • hyperframes@0.8.72 (managed npm install)
  • macOS arm64, chrome-headless-shell 152.0.7977.30 (CLI-managed cache)
  • screenshot capture mode (drawelement not engaged for this comp)
  • Output: --format mov (ProRes 4444, yuva444p12le), --fps 24

Additional context

Workaround confirmed on our side: --workers 1 removes the anomaly entirely (verified across a 37-template regression suite with per-frame RGBA diffing). Filing mainly so the #2477 fix can land — the reporter there already wrote it.

Activity

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