Skip to content

fix(studio): motion blur falls back for a WebGL canvas Studio reaches first - #7060

Merged
jrusso1020 merged 3 commits into
mainfrom
fix/motion-blur-cross-realm-webgl
Nov 3, 2026
Merged

jrusso1020 merged 3 commits into
mainfrom
fix/motion-blur-cross-realm-webgl

Conversation

@jrusso1020

@jrusso1020 jrusso1020 commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

What

With motion blur on, a scene that draws to a WebGL canvas froze during preview playback in Studio. Paused and seeked frames looked right. This makes capture motion blur fall back for those scenes in Studio, the same way it already does in renders.

Why

Capture blur cannot handle a WebGL or WebGPU canvas (Chrome captures it at the frame it had before the blur wrapped the root). The preview knows a canvas is accelerated because the tracker wraps getContext on the preview's HTMLCanvasElement.prototype.

In Studio the preview is a same-origin iframe, and Studio observes and walks its DOM. When the host page reaches an element first, for example through a MutationObserver record, Chrome creates that element's wrapper in the host's realm, with the host's prototype. The scene's canvas.getContext("webgl") then ran the host's unpatched getContext, the canvas never entered the tracked set, the accelerated-canvas check passed, and capture blur stayed on. Pausing unwraps the root, which is why paused frames were fine.

Related work

None.

How

  • installAcceleratedCanvasTracker (core) records a canvas in the tracked set of the canvas's own window (ownerDocument.defaultView), not the set of the realm whose getContext ran. The __hfAccelTracked guard still keeps repeat installs from wrapping twice.
  • Studio installs the tracker in its own window once, when its Player module loads, before any preview iframe exists. A canvas that picked up Studio's prototype then still lands in the preview's set. Player.tsx is the module every Studio preview goes through (NLEPreview renders it, and it is exported from both the . and ./player entries), so apps that embed Studio's components get the install too, not only the standalone Studio page.
  • Core exposes the tracker at a new @hyperframes/core/page-motion-blur subpath, so the light ./player entry does not pull in the core barrel.

The preview runtime only installs the tracker in its own window. An earlier version of this PR also installed it in a same-origin parent from the runtime, which ran on every preview boot: any page embedding the player got its getContext wrapped and a global added, and a host CSP without unsafe-eval reported a violation on each load. That part is gone.

Test plan

  • Unit tests added/updated

  • Manual testing performed

  • Documentation updated (if applicable)

  • Comments follow CONTRIBUTING.md "Comments": they say why, not what, and a bug fix says what the code must do and how to reproduce the bug

  • packages/engine/src/services/captureMotionBlur.pixels.browser.test.ts (real Chrome, runs in Test (runtime)): the host page installs the tracker in its own window, as Studio does, then hosts a data-motion-blur="on" composition in a same-origin iframe and observes it with a MutationObserver before the runtime boots. The scene mounts a WebGL canvas, the host walks it, then the scene calls getContext. The test asserts the canvas really carries the host's prototype, then that the report falls back with tier: "filter", reasonCode: "accelerated-canvas". With main's core it stays tier: "capture". It also stays tier: "capture" with this change if the host does not install the tracker.

  • packages/core/src/pageMotionBlur.test.ts (jsdom): with the tracker in both windows, a scene canvas carrying the host's prototype lands in the scene's set (fails on main). A new test checks that running the tracker in a scene leaves the embedding page's getContext and globals untouched (fails on the earlier version of this PR).

  • packages/studio/src/player/components/Player.test.ts: loading the Player module wraps getContext in Studio's own window and defines the tracked set.

  • Full core, engine and studio test suites, core/engine/studio typecheck, and bun run lint pass locally.

Manual check in Studio on a project with a three.js globe sub-composition and motion blur on: on main the globe canvas had Studio's prototype, was not tracked, and the report said tier: "capture". With the earlier version of this PR it was tracked and the report said tier: "filter", reasonCode: "accelerated-canvas".

The current version was checked in a real Studio preview, captured below: the Studio dev server running a small test project in headless Chrome 152 with --enable-features=CanvasDrawElement, played with Space. The project is a WebGL test scene, not the globe project: a sub-composition draws a spinning WebGL triangle every animation frame, and a rotating box in the root gives motion blur a mover that needs the capture tier (data-motion-blur="on"). In both runs the canvas carried Studio's prototype. Before is the PR's base on main (d03bdeae9): the report said tier: "capture" and the triangle stayed frozen while the box moved. After is this branch: the report said tier: "filter", reasonCode: "accelerated-canvas", and the triangle keeps spinning. Each strip shows four screenshots of the preview taken during one playback, labeled with the player's time.

Before

Before: during playback the WebGL triangle stays frozen while the box moves

After

After: during playback the WebGL triangle keeps spinning

🤖 Generated with Claude Code

… first

When a same-origin host page (Studio) reaches a preview element before the
scene does, for example through a MutationObserver record, Chrome gives that
element the host's prototype. The scene's getContext("webgl") then ran the
host's unpatched getContext, so the canvas never reached the accelerated
canvas set, capture motion blur stayed on, and the WebGL canvas froze during
preview playback.

The tracker now records a canvas in the set of the canvas's own window, and
installs itself in a same-origin parent too, compiled with the parent's own
Function. Cross-origin parents are skipped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 106 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 (8)

Unstable (3)

  • 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-nested-z100: tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true
  • seqfastkeys-none-px-r0-root-z100: tracking 0.06, pressJump 0.05, drop 0, reload 0, render 0.02, renderKey -, undo false, teleport true / tracking 0.07, pressJump 0.05, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0.05, drop 0, reload 0, render 0.02, 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: COMMENT (needs human) · 0 blockers · 1 important · 0 nits
The Studio fix and its tests look right, and CI is green (the new Chrome test ran in Test (runtime)). The open question is scope: the parent climb runs in every same-origin page that embeds the player, not only in Studio. A maintainer should decide whether that's acceptable. ↓ inline

— Claude

Comment thread packages/core/src/pageMotionBlur.ts Resolved
@jrusso1020

Copy link
Copy Markdown
Collaborator Author

Holding this one. We agree with Genesis's point on the parent-window install and will push a change so Studio installs the tracker in its own window instead. Please don't merge at a2301dc.

jrusso1020 and others added 2 commits November 3, 2026 03:11
…mbedder's

The preview runtime no longer installs the canvas tracker in its parent
window. That ran on every preview boot, so any same-origin page embedding
the player got its getContext wrapped, and a host CSP without
unsafe-eval reported a violation on each load. Studio now installs the
tracker in its own window when its Player module loads, and core
exposes it at @hyperframes/core/page-motion-blur.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jrusso1020 jrusso1020 changed the title fix(core): motion blur notices a WebGL canvas the Studio page touched first fix(studio): motion blur falls back for a WebGL canvas Studio reaches first Nov 3, 2026

@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 blockers · 0 important · 0 nits
Re-review: 1 resolved, 0 open, 0 new · a2301dc → e6107f7.

— Claude

@jrusso1020
jrusso1020 added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 112ba53 Nov 3, 2026
141 of 142 checks passed
@jrusso1020
jrusso1020 deleted the fix/motion-blur-cross-realm-webgl branch November 3, 2026 04:15

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Post-merge advisory review: this PR already merged into main at 112ba53302726f2135dc7b28b03033b5a897ffbd. Findings would be follow-ups, not gating. Reviewed head e6107f749aa90cd1cf8e491540aaf887174c9e84.

No source blocker found in the seven-path diff and affected consumers. The old parent-install concern in Genesis's inline is resolved: Player.tsx:29 installs only in Studio's window, including the root and ./player exports; core runtime installs only in its own window. pageMotionBlur.ts:38-52 records host-prototype canvases in their document window's tracked set for both accelerated contexts and OffscreenCanvas. The existing once-per-prototype guard and weak membership preserve page-lifetime installation without retaining canvases across repeated Player mounts. The new core subpath is present in both source and published export inventories.

The added tests assert scene ownership, embedding-page isolation, Studio module installation and a real-Chrome host-observer race before checking filter / accelerated-canvas. I read their source; I did not run them locally. The Chrome test has a capability skip, so job status alone does not prove that case executed.

Validation limit: original latest CI 41574348425 / attempt 1 binds this head, and its complete catalog marks runtime, Studio, Build, Lint, Typecheck and Format successful. The first runtime log download (125714384405) returned HTTP 404 BlobNotFound; I stopped without retrying. I therefore have not independently verified executed browser results, the author's main-vs-fix controls, or before/after screenshots. Captured historical checks also contain an older failed “Studio and player captures” record alongside a later successful record; this is not an all-green-history claim. No fresh browser, Desktop runtime or deployment validation was performed.

Verdict: COMMENT (post-merge: Ready on audited source; browser execution evidence unverified)
Reasoning: the narrowed implementation addresses the cross-realm tracking defect without restoring the embedding-page side effect. No additional source fix was identified; unavailable native logs limit the validation claim.

— Magi (cutroom-dev)

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.

2 participants