Repository navigation
fix(producer): seed random values in every render so re-renders give the same film - #7410
Conversation
There was a problem hiding this comment.
Verdict: REQUEST CHANGES · 1 blocker · 1 important · 0 nits · head b5e9204a
- blocker (
packages/producer/tests/shard-schedule.json, not in the diff): theregressionworkflow fails at Detect changes withFixtures are neither scheduled nor excluded: random-stagger-workers, so no regression shard ran on this head. That means the new fixture (the PR's second fails-without-the-fix proof) has not run in CI. Neither has the rest of the suite, which the body says CI covers. That matters here because every in-process render now replacesMath.random/crypto.getRandomValues, so any existing golden that draws randomness during a render can shift. Add the fixture totimings, then read the full regression result. The golden was also made outside the test Docker image, so this first run is its real check.
Scope
Read the full diff. Every producer site that sets preHeadScripts now gets the seeded shim: renderOrchestrator, probeStage, planReferenceBackground, renderChunk and planFrameCost. No other seekToTime shim exists. The PR has a fails-without-the-fix line, for the page-load unit test and the fixture. I did not render locally. CI was still running when I posted; regression had already failed.
| if (safeTimeMs > 0) seekToTime(0); | ||
| } | ||
| ${seekToTimeReseedCall}fireDueTimers(safeTimeMs); | ||
| reseedRngFromTime(safeTimeMs); |
There was a problem hiding this comment.
important: "the same random values at any worker count" holds for draws at page load, not for draws GSAP makes when a tween first renders. Take a .to(el, { x: "random(-200, 200)" }) (or a function-based value) that starts at 0.5 s in a paused timeline. GSAP resolves it in _initTween on the first render that crosses its start (gsap-core 3.12.2 _parseFuncOrString → _replaceRandom).
With one worker, that render is the 0.5 s frame, seeded reseedRngFromTime(500). With two workers, a worker whose chunk starts at 1 s jumps from 0 to 1 s in its first render, so it initializes the tween under seed 1000 and gets a different x from the frame the boundary falls on. The film then differs by worker count, and the chunk boundary jumps.
This is better than main (native random), not a regression. Either narrow the title, body and fixture description to load-time draws (plus per-frame draws, which reseeding fixes), or add a fixture with a lazily resolved random() value that spans a chunk boundary and make it pass.
There was a problem hiding this comment.
Partly fixed in 921fa5a: the shim comment is now accurate. The PR title and the What section still say "at any worker count" without qualification, and the title becomes the squash commit message. Worth narrowing before merge; not blocking.
What
Every render now seeds
Math.randomandcrypto.getRandomValues, so re-rendering a composition with the same settings gives the same film. A GSAPstagger: { from: "random" }grid used to come out in a different order on each render.Draws at page load, like that stagger, and draws made inside a frame also match across worker counts. One case does not: a value resolved when a tween first renders, such as
x: "random(-400, 400)". A worker resolves it under the seed of the first frame it reaches past the tween's start, so the film can differ between worker counts and jump where one worker hands over to the next. I rendered this: it repeats exactly at 1 worker and fails against that golden at 2 and at 4. It is no worse than main, where such values were random anyway. Fixing it means changing when tweens initialize, which is not in this PR.This reverses a deliberate choice, at Miguel's call. The seeding option's doc said single-machine renders stay random to preserve "non-deterministic behavior for compositions that rely on it". That option is gone: a composition that wanted a different result on every render now gets the same result each time.
Preview does not change. It has no virtual-time shim and still uses native randomness, so a random draw in preview can differ from the render.
Why
Pages render in several places: each worker loads the page itself, and chunked renders already seeded while single-machine renders did not. So the same composition could give different frames on two renders, and could differ between workers within one render, since each worker's page drew its own order. The same page also rendered differently chunked and unchunked.
Related work
Refs #7390 (page timers on the film's schedule, in the same shim).
How
Main already had the seeded generator:
buildVirtualTimeShim({ seedRandomFromFrame: true })(Mulberry32, seeded from virtual time at load and on each seek), used only by chunked renders. Every render path already injects a virtual-time shim before page scripts, through the file server'spreHeadScripts, so that is the one place to fix.seedRandomFromFrameoption andbuildVirtualTimeShimare deleted.VIRTUAL_TIME_SHIMis now the only shim, always seeded.renderChunk.tsandplanFrameCost.tsuse that constant. No new generator was added.Math.randomnative" are removed with the option, along with one that string-matched the gated block.Every composition starts from the same seed; there is no per-composition seed.
Test plan
draws the same Math.random sequence at page load in two fresh pages. fails without the fix: on main the two pages draw different sequences. Passes with it.random-stagger-workers: a 100-cell grid withstagger: { from: "random" }, rendered with 2 workers against a golden made with the fix. It has shapes only, no text. fails without the fix: with main'sfileServer.tsit fails at the visual comparison. Passes twice in a row with the fix.fileServer-seededRandom7/7,fileServer58/58,fileServer-virtualTimers9/9,renderChunk38/38,renderChunkFallback17/17,renderChunkFrameRange12/12,renderChunkVideoMetadata1/1 under bun;renderChunk.stall1/1 andrenderChunk.verifyInWorker5/5 under vitest.tsc --noEmit, oxfmt and lint pass.random-stagger-workersis inshard-schedule.json, andplan-regression-shards.mjsschedules it.Math.random()" stays right, because preview is still unseeded.Not exercised locally: the rest of the regression suite (CI runs it), the fixture in distributed mode, and Windows. The golden was made on Linux outside the test Docker image.