feat(core): harden the capture motion blur runtime lifecycle by vanceingalls · Pull Request #5968 · heygen-com/hyperframes · GitHub
Skip to content

feat(core): harden the capture motion blur runtime lifecycle - #5968

Open
vanceingalls wants to merge 1 commit into
feat/capture-blur-runtimefrom
feat/capture-blur-render
Open

vanceingalls wants to merge 1 commit into
feat/capture-blur-runtimefrom
feat/capture-blur-render

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

What

Hardens the runtime lifecycle from #5967 so it holds up on real compositions, in preview and in renders.

  • Device pixels. The capture and output canvases and the compositor's buffers follow the page's scale (a render's injected scale wins over a devicePixelRatio that fell back to 1), so a 2x or 4k render is not blurred at half resolution.
  • A render never gives up mid-video. A frame whose capture throws is plain DOM and the next frame wraps again (counted in failedFrames, logged once per 25). In Studio preview, three capture exceptions in a row turn blur off for the session. Paint timeouts, which render plain and never end blur, are handled since feat(core): capture motion blur runtime lifecycle #5967; here failedFrames also counts capture exceptions. A tab that stops painting while the clock keeps running (hidden or minimized) used to turn blur off for good after about 6 s.
  • A hidden preview tab rests on plain DOM. While document.visibilityState is hidden the preview does not wrap and requests no paints (a hidden tab delivers none); blur resumes when the tab is visible again. Engine mode is unaffected.
  • Timeline callbacks stay out of the scan. The mover scan and its restore seek always suppress events, so tl.call() callbacks no longer fire during discovery even with data-motion-blur-events="on"; only the sub-frame pose reads honour -events.
  • Wrapping the root no longer disturbs the page. Wrap and unwrap move the root in the document; each move now runs through the runtime's record filter (host.ignoreMutations) and the adapters re-seek at the current time right after it. Measured before the fix (Chrome 152, 6 play/pause cycles): CSS animations restarted at 0 for one displayed frame on every unwrap, and the runtime's MutationObserver read each move as a composition change (18 stage-size and 12 timeline posts instead of 2 and 1).
  • Studio preview holds the playback seek while a blurred frame is in flight, so the poses and both captures belong to the same time and a fast box keeps its full width. init.ts gets a small hold (the transport skips a playback seek while a blurred frame is busy; the next tick catches up), with its own test.
  • adapters/waapi.ts (shared runtime code, a small separate hunk): after a wrap Chrome cancels the moved element's CSS animation and starts a new one, and the adapter kept reviving the cancelled (idle) one. The adapter now drops an idle animation, only when it is browser-created (CSSAnimation / CSSTransition); script-created animations are left alone. Please review that hunk on its own: it is the one change here that affects every composition, not only blurred ones.
  • The host exposes now() for the re-seek.

Why

Part 3 of 9 in the capture motion blur stack: #5875 → #5967 → #5968 → #5969 → #5970 → #5971 → #5990 → #5991 → #5992. It builds on #5967.

Stack goal: opt-in capture motion blur for compositions, in Studio preview and in renders, drawn with Chrome's html-in-canvas. The correct frame is always preferred over a blurred wrong one: when the capture cannot be trusted for a frame, that frame renders sharp from the plain DOM.

Notes

Testing

  • captureMotionBlur/index.test.ts: wrapping and unwrapping (including the quiet-then-re-seek order and no re-seek when there is nothing to unwrap), the two-phase frames, painting and unwrap-on-pause, the render fallback, the hidden-tab rest, the opt-in switches that moved here from feat(core): capture motion blur runtime lifecycle #5967 (shutter clamp, a browser without drawElementImage, the reason logged for a root that asked for blur, a nested capture canvas, opting back out, a composition where nothing moves; the paint-timeout tests now live in feat(core): capture motion blur runtime lifecycle #5967), the callback spy for the scan, device-pixel canvases, and the guards that survived a deliberate break (exactly three failures, no seek below zero, unwrap() removes the hide rule, canvases and hide attributes, invisible movers are not blurred; each was shown to fail with its mutation applied).
  • compositor device-pixel test in composite.test.ts; init.blurHold.test.ts for the preview hold; adapters/waapi.test.ts for the narrowed idle skip.
  • bun run build, core tsc --noEmit, and the core runtime tests pass; comment ratchet against feat(core): capture motion blur runtime lifecycle #5967's branch passes; core stays under its size budget.

🤖 Generated with Claude Code

@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.

Delta review at full head68737fe2aa6c4b3b6f4357fc6af54583a19baa13, relative to69441a0e.

Audited: previewMotionBlur flow from useRenderQueue through studio-server/CLI adapter/producer to engine installPageMotionBlur before navigation; CSS/device pixel conversions in composite.ts and density resets; runtime thrown-capture fallback. Explicit false propagates to render-off, while absent/true preserves opt-in. Large see-through movers stay sharp. The no-paint deadline is distinct from the repaired exception path.
Trusting: authored browser/render measurements and the private review's broader test/caller audit; no new pixel rendering by Home.

The inherited #5875 pose and #5967 timeout findings remain; this delta approval does not clear the complete stack. Saved/fresh prior human reviews and inline comments were empty. Exact-head hosted checks pass/skip; the private focused core run passed124 authored tests. Inspected child regression logs execute3 visual/audio cases, not every new unit test. Browser-version screenshots, HDR output, worker parity, colour and performance measurements were not independently rerun. No provider call, merge or release.

— Magi (Home)
Verdict: APPROVE
Reasoning: No additional blocking defect found in this change relative to its stack parent; the foundation fixes remain required.

@vanceingalls
vanceingalls marked this pull request as draft October 20, 2026 21:58
@vanceingalls
vanceingalls force-pushed the feat/capture-blur-runtime branch from 69441a0 to 9e77fa5 Compare October 21, 2026 01:55
@vanceingalls
vanceingalls force-pushed the feat/capture-blur-render branch from 68737fe to c47bef3 Compare October 21, 2026 01:55
@vanceingalls vanceingalls changed the title feat: render and Studio plumbing for capture motion blur feat(core): harden the capture motion blur runtime lifecycle Oct 21, 2026
@vanceingalls
vanceingalls force-pushed the feat/capture-blur-runtime branch from 9e77fa5 to 16b4b2e Compare October 21, 2026 03:52
@vanceingalls
vanceingalls force-pushed the feat/capture-blur-render branch from c47bef3 to 27f076c Compare October 21, 2026 03:53
@vanceingalls
vanceingalls marked this pull request as ready for review October 21, 2026 04:16
Make the lifecycle from the previous change safe for real compositions.

- Frames are drawn in device pixels: the canvases and the compositor buffers
  follow the page's scale, so a 2x render is not blurred at half resolution.
- A render never gives up mid-video: a frame whose capture throws is plain
  DOM and the next one wraps again; in Studio, three capture exceptions in a
  row turn blur off. (Paint timeouts render plain and never count; they are
  handled in the previous change.)
- A hidden preview tab rests on plain DOM instead of waiting on paints that
  never come.
- Timeline callbacks no longer fire during the mover scan, even with
  data-motion-blur-events="on".
- Wrapping and unwrapping the root no longer reload iframes, restart CSS
  animations or leak timing invalidations: each move runs through the
  runtime's record filter and the adapters re-seek right after it. The waapi
  adapter drops animations Chrome cancelled, limited to browser-created CSS
  animations (a small, separate hunk in adapters/waapi.ts).
- Studio preview holds the playback seek while a blurred frame is in flight,
  so the poses and both captures belong to the same time.
- The host exposes now() for the re-seek.
- Tests for the wrap, two-phase and painting paths, and for the guards that
  survived a deliberate break.

Folded review fixes: R4, R8, R9 (runtime, init and waapi parts), R11, T1.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@vanceingalls
vanceingalls force-pushed the feat/capture-blur-runtime branch from 16b4b2e to 254e20a Compare October 21, 2026 05:18
@vanceingalls
vanceingalls force-pushed the feat/capture-blur-render branch from 27f076c to 3a35ca0 Compare October 21, 2026 05:18

@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.

Reviewed exact head 3a35ca04264c42a87c610b9ecfa15620b3629569 relative to 254e20a5d84aafbb29dea1e9744694aa1b258326.

Audited exception versus timeout fallback, DPR, hidden-preview rest, event-suppressed discovery, quiet mutation/re-seek, preview clock hold and the shared WAAPI hunk. The latter drops only idle browser-created CSSAnimation/CSSTransition, preserving script animations; its25 authored tests pass locally. The init.blurHold suite could not load from the existing cached parser generated-output dependency; I source-inspected the integration rather than claim its local execution.

Independent revision-bound review of the nine-PR composition. Earlier approvals/requests covered older heads. Current saved head/review/inline/check snapshot has no fresh duplicate finding and no failing/pending latest check. Stacked broad-suite skips are not executed tests.

Audited: DPR propagation, exception fallback versus timeout, hidden preview, event-suppressed discovery, mutation filtering and re-seek, preview clock hold; narrowed browser-created CSS idle handling in WAAPI.
Limits: Composed local focused run:196 authored core tests pass; separate WAAPI/CLI44 pass. Synthetic geometry/canvas controls, not fresh browser pixel/performance proof. No app/model/provider call, source fix, merge or release.

— Magi / desktop-review

Verdict: APPROVE
Reasoning: No additional blocker in this own delta. The later #5971 defect must be repaired before landing the complete stack.

This branch has not been deployed

No deployments
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