feat(core,engine): report why blur is off, and what each render session did by vanceingalls · Pull Request #5991 · heygen-com/hyperframes · GitHub
Skip to content

feat(core,engine): report why blur is off, and what each render session did - #5991

Open
vanceingalls wants to merge 1 commit into
feat/capture-blur-scan-hdrfrom
feat/capture-blur-report
Open

vanceingalls wants to merge 1 commit into
feat/capture-blur-scan-hdrfrom
feat/capture-blur-report

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

What

Reports why blur is off, and what each render session did, so tooling and logs can tell.

  • Reason codes. The runtime's report() carries a stable reasonCode next to the readable reason for every way blur ends up off: a defect code (from feat(core): block capture motion blur on html-in-canvas defects #5971), unsupported-browser, renderer-off and capture-failures.
  • probe() (on window.__hfMotionBlur) makes the decision on demand without wrapping or painting; it is cheap once scanned and puts the playhead back.
  • suspend() stops all painting for the rest of a session without changing the reported decision (and never sets the page's off switch, which would change the probe's answer). It outlives a re-init of the same root and does nothing where blur is off.
  • Engine log line. One line per capture session from what the page reports: [motion-blur] on reasonCode=null movers=N failedFrames=K, after the first frame. The first-frame line cannot know about later failures, so the session end prints the same line with (session end) when any frame fell back to plain DOM (read with a 1.5 s timeout so a stuck page cannot hold up closing); a clean session stays quiet.
  • The first test that reads blurred pixels. captureMotionBlur.pixels.browser.test.ts drives a real Chrome with html-in-canvas through the engine's frame order (seek, wait, composite, screenshot) and checks: the trail along the motion line has the hard edges of the sharp frame spread out without adding or losing ink; what does not move matches the sharp frame exactly; a box centered with translate: -50% -50% is blurred around where it really is; a backface-hidden card in a 3D flip renders the plain frame. distributed/chunkFrames.test.ts (moved here from feat(core,producer): decide blur defects once per timeline, off on routes that cannot host it #5990 to keep that PR under 1000 lines): a chunk session stands page blur down and asks for no engine blur. Also a face whose backface-visibility: hidden comes only from @keyframes or element.animate(), next to an ordinary mover, turns blur off with backface-visibility and renders the plain frame; a face that is hidden only between two scan samples renders just that frame plain; a flat animated backface hack stays blurred. It skips when Chrome or html-in-canvas is missing.

Why

Part 8 of 9 in the capture motion blur stack: #5875 → #5967 → #5968 → #5969 → #5970 → #5971 → #5990 → #5991 → #5992. It builds on #5990, whose scan decides the state this reports.

Stack goal: opt-in capture motion blur for compositions, in Studio preview and in renders, drawn with Chrome's html-in-canvas. Blur turns itself off in several cases, and without this an author or an operator cannot tell which; hyperframes check (#5992) builds on probe() and suspend().

Notes

  • state stays on while individual frames fall back to plain DOM (a paint timeout, a missed defect, a failed capture): read failedFrames for those. A paint timeout counts in failedFrames but not in failures, which only counts capture exceptions in a row (documented, not fixed).
  • Why blur is off is reported three ways after this PR: window.__hfMotionBlur.report() (state, reason, reasonCode, movers, failedFrames), the per-session log line, and, in feat(cli): tell authors in check why motion blur is off #5992, hyperframes check.
  • The pixel test runs against the Chrome the engine pins (152 headless shell) with the CanvasDrawElement feature; it is a guard for the pipeline, not a visual-quality benchmark. Limits it does not cover: trails drawn over foreground elements, a halo on transparent movers, trails past overflow:hidden, 2D flips through scaleX = 0.
  • Cost: about 1.7x median render time for a blurred composition (20 real compositions, blur on vs off); a few ms of JS per blurred frame in Studio plus a one-time mover scan on first play.

Testing

  • Core captureMotionBlur/index.test.ts: reason codes for every off path (the earlier assertions on reason text now check codes too), probe() finds the movers without any paint, wrap or event and restores the playhead, does not rescan unless the timeline changes or while blur is off, reports the defect with its code and capture-failures after three failed frames; suspend() unwraps a wrapped root, stops painting, is idempotent, outlives a re-init, exists but does nothing where blur is off, and lets the probe still decide; plus the guards that need codes (one warning and the first reason stays when unwrapping throws).
  • Engine pageMotionBlur.test.ts: the per-session line, the session-end line only when frames fell back (including when the failures ended blur), the timeout for a page that does not answer, and closeCaptureSession logging the closing line once before the page closes (driven through the real function); frameCapture-motionBlur.test.ts: the line is logged once after the session's first captured frame, says why blur is off, and stays silent for a page that never asked for blur.
  • Engine captureMotionBlur.pixels.browser.test.ts: 4 pixel tests in Chrome 152 headless shell.
  • bun run build, tsc --noEmit in core and engine, and their tests pass; comment ratchet against feat(core,producer): decide blur defects once per timeline, off on routes that cannot host it #5990's branch passes; every package is within 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.

Reviewed exact head b036deef790b8897bd6c805b94ff861182baec2a relative to 4f40902c27d2c38ee4ee35f73e80983af29dd480.

Audited stable reason codes, probe/restore, sticky suspend/re-init, first-frame and session-end reporting/timeout and close consumer. Own delta has no additional demonstrated blocker. The four pixel tests have explicit browser/API skip conditions; I read their assertions, did not execute a real Chrome pixel run, and do not treat the child's3 generic regression cases as that proof. Inherited #5971 detector gap still holds the composition.

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: Stable reason codes, probe time restoration, sticky suspend/re-init, first/end diagnostics and closing timeout; read pixel-test assertions and skip conditions.
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.

…on did

Tooling and logs need more than "off". Give the runtime's report a reason
code for every way blur can be off (renderer-off, unsupported-browser, each
html-in-canvas defect, capture-failures), and two calls for tooling:
probe() runs the same scan a render would, without painting, and suspend()
stops all painting for the rest of a session without changing the reported
decision.

The engine prints one line per session from what the page reports:
`[motion-blur] on reasonCode=null movers=N failedFrames=K`. The first-frame
line cannot know about later failures, so the session end prints the same
line with "(session end)" when any frame fell back to plain DOM; a clean
session stays quiet. A render where every capture failed no longer reads as
"on" with nothing else.

Add the first test that reads blurred pixels: real Chrome with
html-in-canvas, the engine's frame order (seek, wait, composite, screenshot).
It checks the trail along the motion line, that what does not move matches
the sharp frame, that a box centered with translate is blurred where it
really is, and that a flipped backface card renders the plain frame. It
skips when Chrome or html-in-canvas is missing.

Folded review fixes: R13 (runtime half), R17 (engine half), T1 (the guards
that need reason codes), T3 (log-line and pixel tests).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@vanceingalls
vanceingalls force-pushed the feat/capture-blur-scan-hdr branch from 4f40902 to 83f05ec Compare October 21, 2026 07:39
@vanceingalls
vanceingalls force-pushed the feat/capture-blur-report branch from b036dee to 7693556 Compare October 21, 2026 07:39

@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 7693556e8860654d3b40dd99d238445b58996aee against its actual parent 83f05ec5fd2d0bcb3f8188e7bc1f9cbc5f63ece9 and the previously reviewed tree.

The restack retains the repaired detector/cache; added browser assertions cover CSS/WAAPI faces, between-sample frame fallback and flat-face allowance. The moved chunkFrames test blob matches the previously reviewed test exactly. These browser tests explicitly skip without Chrome/runtime/drawElementImage; I read them but did not run a fresh pixel experiment.

Independent verification: own-head and composed-tip synthetic computed-style controls 4/4 pass; predecessor witness remains red (inline positive passes). Exact-source authored detector suites run locally:31 own-head +47 tip tests pass, including WAAPI containment, named/multiple animations and cached-candidate retention. The computed animation-name seam is explicit because jsdom does not expand animation shorthand. These are deterministic detector controls, not fresh Chrome pixel/performance proof.

Current exact-head check snapshot has no failed or pending check. Read completed regression gate logs and executed #5991 preflight lint/format output; stacked broad-suite skips and aggregate gate success do not prove execution of the new pixel/unit suites. Author Chrome152 experiments, 26ms scan cost and package-budget margin remain attributed.

Known sampling limit remains: a short WAAPI-only face absent at every scan cannot enter the cached set. This bounded sampling residual differs from the repaired omission of ordinary animation-derived candidates; no universal detection guarantee is claimed. No new source blocker found in this successor composition. No browser/app/model call, merge or release.

— Magi / desktop-review

Verdict: APPROVE
Reasoning: The earlier animated-face candidate blocker is repaired and retained through the restacked composition; no additional blocking change was found.

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