Repository navigation
fix(producer): let buildReady setup that awaits a frame finish - #7213
jrusso1020 wants to merge 1 commit into
Conversation
The render-time virtual-time shim queues every requestAnimationFrame callback until the next seekToTime. The first seek only happens after render-ready, and render-ready waits on window.__hf.buildReady, so a composition whose setup awaits one animation frame (a common shader warm-up or compile-poll pattern) deadlocked and timed out. When the page has registered window.__hf.buildReady and capture has not started, give each frame request a real 16 ms tick and run it at the frozen virtual time. Nothing is captured before the first seek, so frame output is unchanged. Pages without buildReady keep the old behavior, so a frame-counting loop there still sees no frames before the first seek. Once capture starts, frames queue for the next seek exactly as before. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
There was a problem hiding this comment.
Verdict: REQUEST CHANGES · 1 blocker · 0 important · 0 nits
Pre-capture frames keep running after setup settles, so on any buildReady page an rAF loop ticks a wall-clock-dependent number of times before the first captured frame (↓ inline, with repro). CI was still running at review time.
— Genesis
| // Only pages with buildReady setup can deadlock on a frame. Everywhere else | ||
| // a frame-counting loop must keep seeing no frames before the first seek. | ||
| var buildReady = window.__hf && window.__hf.buildReady; | ||
| if (!buildReady || Object.keys(buildReady).length === 0) return; |
There was a problem hiding this comment.
blocker (determinism): this gate checks that buildReady has keys, not that one is still pending. The runtime never removes settled keys (only teardown resets them, core/src/runtime/init.ts:6459). So from registration to the first seek, every rAF gets a real 16 ms tick, and a self-rescheduling loop runs once per 16 ms of setup time plus readiness polling.
That loop is the normal Three.js shape: setAnimationLoop, or a path tracer adding one sample per frame. The first captured frame then holds a load-time-dependent number of samples. Each parallel worker and distributed chunk loads its own page, so neighbouring chunks disagree at their boundaries. It also changes buildReady pages that never wait on a frame, like this PR's setTimeout(50) control: on main their loops saw exactly one frame per seek.
Repro: I loaded this head's shim in node:vm, with setup that waits one frame and function loop() { samples++; requestAnimationFrame(loop) }. After 3 / 40 / 200 timer ticks, seekToTime(0) leaves samples at 4 / 41 / 201. On main it is 1 / 1 / 1.
Second issue, same root: the gate is read when the timer fires. A frame requested just before the page registers buildReady gets served only if registration lands within 16 ms.
Suggested fix: serve pre-capture frames only while a registered promise is still unsettled. Track settlement per promise, the way the runtime's keyedBuildReadyPromises does, and stop once every promise settles. That limits the window to setup itself. A loop started during setup still ticks a timing-dependent number of times, so document "start render loops after buildReady resolves" in the recipe. Add a test with a self-rescheduling loop and a settled buildReady that pins how many frames run before the first seek.
|
Closing. As Genesis reproduced, ticking frames before the first seek makes frame-counting and sample-accumulating loops depend on load time, and that breaks HyperFrames' determinism guarantee. Even ticking only while setup is pending would still let wall-clock-paced setup (shader compiles) decide how many loop ticks happen. So the render clock stays strict. #7211 now tells authors that animation frames and the clock wait for the first frame, and to wait with |
Edit accuracy: accurate 267 (base branch 267), smooth 132 of thoseThe gate passes. Quarantined, measured but not gated (8)
Unstable (1)
|
|
Correction to my closing note: don't wait with |
What
A composition that registers
window.__hf.buildReadycan now await an animation frame during setup without hanging the render. Until the first seek, the virtual-time shim gives each frame request a real 16 ms tick and runs it at the frozen render time (0).Why
The shim queues every
requestAnimationFramecallback until the nextseekToTime. The first seek happens only after render-ready, and render-ready waits for everybuildReadypromise. So setup likeawait new Promise((r) => requestAnimationFrame(r))deadlocks, and the render fails at the 45 s readiness timeout.Waiting a frame during setup is a common shader and path-tracer pattern, used to poll compile status or give the GPU a turn. We hit it on a Three.js path-tracing study.
Repro: a 1 s, 320x180 Three.js composition whose setup awaits one frame before resolving
buildReady.scene. Rendered with the CLI on software GL:setTimeout(50)(control)requestAnimationFrameperformance.now()advancingRelated work
Companion to #7211, which renames this failure from "Composition has zero duration" to "Composition setup did not finish".
How
requestAnimationFramestill pushes ontorafQueue. Before capture starts, it also schedules a 16 ms timer on the originalsetTimeout.virtualNowMs, which is still 0, but only if all of the following hold:window.__hf.buildReadykey.seekToTimesetscaptureStarted. From then on, frames queue for the next seek exactly as before. A pre-capture frame still pending at the first seek runs once, in that seek's flush.Design notes:
buildReady: a frame-counting loop (x += 1per frame) would otherwise tick a load-time-dependent number of times before the first seek, which would make the first captured frame nondeterministic. Pages withoutbuildReadykeep today's behavior byte for byte. Only pages that opted into async setup, where a frame wait would otherwise deadlock, get pre-capture frames.performance.nowandDatestay frozen on purpose: advancing them would make output depend on how long setup took. fix(engine): report a pending setup as a setup timeout, not zero duration #7211's diagnostic tells authors that setup waiting on the clock never finishes.Test plan
fileServer-preCaptureFrames.test.tsloads the shim in anode:vmcontext with a manual timer queue. It covers:buildReadyregistered, the frame waits for the first seek;cancelAnimationFrameis honored;setTimeoutcontrol.bun run test:classificationpasses, and the producer bun unit lane passes except oneplanV2.test.tscase ("publishes the compiled tree while the audio mixes"). That case hits its 60 s timeout on this box with main'sfileServer.tstoo, so it's environmental.🤖 Generated with Claude Code