Skip to content

fix(engine): a scene that has ended gives up its WebGL context so the next scene is not slowed - #6984

Merged
miguel-heygen merged 6 commits into
mainfrom
dscan/gl-release
Nov 2, 2026
Merged

miguel-heygen merged 6 commits into
mainfrom
dscan/gl-release

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

A scene that has ended gives up its WebGL context, so the scene after it is not rendered at a crawl.

On software GL (the render workers), a three.js scene whose clip is over keeps its live WebGL context, and every later WebGL frame in the same page then costs several seconds. Measured on one project with a glass-refraction title (0 to 1 s) followed by a product turntable (1 to 3 s), one worker:

  • turntable alone: 90 frames in 33 s (0.36 s/frame)
  • glass then turntable, before: about 275 s for 90 frames; at 3 s of glass and the turntable at 15 s the sequential capture stall guard (60 s with no frame) fired on the turntable's first frame
  • glass then turntable, after: 75 to 88 s (three runs)

The engine records the WebGL contexts a page creates and, once per frame after the seek, calls WEBGL_lose_context on those inside a clip the runtime has hidden (data-hf-clip-hidden) whose window has ended. The window is the one the runtime resolves (window.__hf.clipWindow, a new read of the renderer's own start/end resolver), so a start written as a reference (data-start="intro + 1") is the resolved time and never the raw attribute; a scene whose window has not ended, or a runtime that does not report windows, keeps its context.

Design: none (engine capture step)

fails without the fix: releases a scene's context once the runtime's window for its hidden slot has ended, keeps the context of a scene whose start is a reference, until its resolved end has passed, reports a clip's window with its start reference resolved

What I measured

  • New tests red before (module missing), green after; all 35 frameCapture* and webglRelease test files pass; tsc --noEmit clean in packages/engine.
  • The same glass-then-turntable project with the turntable's start written as a reference to the glass scene renders byte-identical to the numeric start (3D present) in 72 s.
  • A 30 s, ten-scene project (the same two 3D scenes among them), one worker, software GL: stalled at frame 450/900 before; completes in 7 min 6 s with this change.
  • The render timings above, same box, the CLI built from this branch against the published 0.8.226 behaviour.

What I did NOT exercise

  • A scene that reuses a canvas after its slot has ended (its context would be lost; three.js restores on webglcontextrestored, not tried here).
  • The 30 s project end to end with the runtime-window version (done with the previous attribute-based version, 7 min 6 s).
  • Parallel workers and the draw-element capture mode.

Package size budget

The engine's packed budget goes from 456397 to 461788 bytes: the new module and its declarations ship in dist (about 5 KB packed, over the 1% allowance).

Size

6 files, about +180.

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 108 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (2)

  • seqplainkeys-none-px-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true
  • seqplainkeys-none-pct-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES · 1 blocker · 0 important · 0 nits · head 30070ba0

1 blocker, posted as an inline comment: a 3D scene whose data-start is a reference (the documented data-start="intro" sequencing) can lose its WebGL context before it starts, so it renders with no 3D in the final video.

Scope

I read the whole diff and checked every path that captures frames out of order against the new per-frame release. The calibration sessions in captureCost.ts and planFrameCost.ts are closed rather than reused for the render. Probe discovery seeks without going through prepareFrameForCapture. Engine-mode shader transitions clone their scenes, so a WebGL canvas was already blank in those clones. The PR's own tests pass locally. I did not run a real render.

— genesis-v2 (Lane B)

const hasEnded = (el: Element): boolean => {
let offset = 0;
return clipsAround(el).some((clip) => {
offset += Number(clip.getAttribute("data-start")) || 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocker: Number(clip.getAttribute("data-start")) || 0 turns a reference start into 0. References are a documented way to set data-start ("intro", "intro + 0.5", "card-pro", per docs/concepts/data-attributes.mdx and compositions.mdx), and they are still unresolved on the render page. The runtime only resolves them through startResolver.

How it fails: intro runs 0–10 s and a three.js scene has data-start="intro" data-duration="3", so it plays 10–13 s. Before it starts, the runtime marks it data-hf-clip-hidden. This code puts its end at 0 + 3 = 3 s, so the frame at t=3 calls loseContext() on a scene that has not started yet. Frames 10–13 s then render with no 3D. A chain of scenes (A → B → C) hits the same thing whenever a reference-started clip is shorter than its real offset.

I ran this against the PR's own mock: releaseEndedWebglContexts(page, 5) returns 1 for that scene; it should return 0.

Fix: when any ancestor's data-start (or its length) is not a finite number, leave the context alone. Even better, read the start and duration the runtime has already resolved. Please also add a test for the reference case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Fixed in 1d36408. The release now uses the runtime-resolved __hf.clipWindow.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE · 0 blocker · 0 important · 0 new · 1 resolved · 30070ba0 → 1d36408c

The reference-start blocker is resolved. The engine now asks the runtime for each clip window through __hf.clipWindow, which uses runtimeElementWindow and the start resolver, the same code the visibility pass uses, and it releases nothing when the runtime does not provide that. I checked locally that a child (data-start="1", data-duration="2") inside a sub-composition hosted at 10 s resolves to the absolute window {11, 13}, so a nested scene is not released early. The new engine tests and the core clipWindow test pass. The non-required Comments ratchet fails on packages/engine/src/types.ts (+1 comment line); this does not block approval.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE · 0 blocker · 0 important · 0 new · 1d36408c → 7f5cef34. Comment-only change: one comment in webglRelease.ts is reworded and one comment line is dropped from types.ts, the line that tripped the Comments ratchet. The code is the same as at 1d36408c.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 26d6e3e Nov 2, 2026
91 of 97 checks passed
@miguel-heygen
miguel-heygen deleted the dscan/gl-release branch November 2, 2026 08:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant