fix(engine): supersample screenshot capture to honor deviceScaleFactor#666
Conversation
vanceingalls
left a comment
There was a problem hiding this comment.
Verdict: Approve. Two surgical fixes that close out the actual end-to-end 4K render path. Last piece of the #660–#665 stack: makes --resolution 4k produce real 3840×2160 pixels at the encoder rather than 1080p frames stretched to a 4K MP4 container. Diff is 15/-1 across two files, narrowly scoped, and the body documents both root causes (CDP Page.captureScreenshot ignoring viewport DPR without clip.scale; HeadlessExperimental.beginFrame having no DPR knob at all). Verified the underlying CDP behavior matches the description; both fixes are byte-identical on the dpr === 1 default path.
Note: stack still has open feedback I left on #662 (cache-self-eviction blocker) and #664 (Vite adapter dropping outputResolution) — neither is in scope for this PR, but worth landing them before declaring the 4K stack done.
Blockers
None.
Important
-
important —
packages/engine/src/services/screenshotService.ts:135-137— alpha capture at >1 DPR is unverified.captureAlphaPngandcaptureScreenshotWithAlphaalways passscale: 1and ignoredeviceScaleFactor, so an alpha-format render (webm/mov/png-sequence) combined with--resolution 4kwill silently produce 1080p alpha frames — the same class of bug this PR is fixing for the opaque path.resolveDeviceScaleFactorrejects HDR + supersampling but does not reject alpha + supersampling (packages/producer/src/services/renderOrchestrator.ts:592), so this combination is reachable today. Not a regression introduced by this PR (the path was already broken), but a near-miss given the fix is right next door. Either (a) extend the sameclip.scale = dprtreatment to the alpha helpers, or (b) reject the combination inresolveDeviceScaleFactoruntil it's wired up. Pick one and flag explicitly so users don't get a silent 1080p alpha render. -
important — testing. The PR body acknowledges no unit tests; the verification is one manual
ffproberun. Given how subtle the two-bug interaction was (orchestrator log says "supersampling", page reportsdevicePixelRatio === 2, and the bitmap still came out at CSS dimensions), this is exactly the path that needs a regression test — silent 1080p output with all the right log lines is the precise failure mode that will reappear. The Docker baseline-at-4K follow-up mentioned in "Limitations" is the right shape; please file a follow-up issue and link it here so it doesn't get lost. A cheap intermediate: a unit test that assertspageScreenshotCapturesendsclip: { …, scale: dpr }toclient.sendwhendeviceScaleFactor > 1and omitsclipwhen it's 1. That's a 20-line test using a fakeCDPSessionand would have caught the original silent regression.
Nits
-
nit —
packages/engine/src/services/screenshotService.ts:138-145— theclipis built conditionally then spread conditionally:const clip = dpr > 1 ? { x: 0, y: 0, width: ..., height: ..., scale: dpr } : undefined; // ... ...(clip ? { clip } : {}),
Simpler:
...(dpr > 1 && { clip: { x: 0, y: 0, width: options.width, height: options.height, scale: dpr } }),
or assign
clipunconditionally and pass it to CDP unconditionally —Page.captureScreenshotacceptsclipwithscale: 1and that produces an identical bitmap to omittingclip(both return CSS-sized capture). The double conditional reads as defensive against a behavior that doesn't actually exist. Style only. -
nit —
packages/engine/src/services/frameCapture.ts:120— naming.supersamplingreads as a verb-ish flag describing the render mode; the actual condition is "DPR > 1". A comment is fine, butdpr > 1inline at the use site would also be readable and skip the named binding. Style only. -
nit — comment in
frameCapture.ts:115-119is correct but slightly buries the lede. The reason BeginFrame is bypassed is "BeginFrame'sScreenshotParamshas noclip/scalefield" — that's the load-bearing fact. Worth promoting it to the first sentence.
Praise
-
The PR body is excellent — both root causes explained, the fact that the orchestrator/log layer was lying ("supersampling…" when no supersampling was actually happening) called out, before/after
ffprobeevidence, and the explicit perf trade-off (BeginFrame bypassed at high DPR) named with the rationale. This is what staff-level fix writeups look like. -
Surgical scope. 15 added lines, 1 removed, two files, no drive-by refactors. Trivial to revert.
-
Good defensive choice on the clip path:
dpr === 1produces a byte-identical screenshot call, so the default 1080p path can't regress from this change. That's the right shape for a fix that lands behind a flag the user opts into. -
Honest about limitations — the BeginFrame perf regression at 4K is named, the Docker golden test is named as a follow-up, and the trade-off (correctness > perf) is justified rather than hand-waved.
— Vai
miguel-heygen
left a comment
There was a problem hiding this comment.
Approving this fix. This PR addresses the core end-to-end bug in the stack: DPR was wired through, but screenshots still came out at CSS-pixel dimensions.
What I verified on the live head badfce67c579e0bd47c22042bcf5f153cd5986d4:
- inspected the diff in
frameCapture.tsandscreenshotService.ts bun install --frozen-lockfilebun run build:hyperframes-runtimebun run --filter @hyperframes/engine test -- src/services/frameCapture.test.tsbun run --filter @hyperframes/engine typecheckbunx oxlint packages/engine/src/services/frameCapture.ts packages/engine/src/services/screenshotService.tsbunx oxfmt --check packages/engine/src/services/frameCapture.ts packages/engine/src/services/screenshotService.ts
I also ran a small real render/ffprobe proof on this PR head:
hyperframes render --resolution 4k --quality drafton a 1s blank scaffold produced3840x2160- the same scaffold without
--resolutionproduced1920x1080
That proves the screenshot path is now emitting device-pixel-sized frames for the 4K preset while preserving the default 1080p path.
Caveats: #666 CI still had regression shards pending when I rechecked, and the broader stack still has unresolved lower-layer issues I requested changes on (#661, #663, #664). This approval is for the engine supersampling screenshot fix itself.
1753b49 to
994d013
Compare
badfce6 to
1a59dea
Compare
994d013 to
b39bb8d
Compare
1a59dea to
26f1a4c
Compare
vanceingalls
left a comment
There was a problem hiding this comment.
Re-review — already APPROVED, posting status on follow-ups
Reference: prior review #666 (review) (APPROVED)
This PR was already approved. The two follow-ups I'd called out are now both addressed in commit 26f1a4c7:
Addressed since prior approval (commit 26f1a4c7)
- Follow-up — alpha capture at DPR>1 silently produced composition-resolution frames:
packages/producer/src/services/renderOrchestrator.ts—resolveDeviceScaleFactornow takesalphaRequestedand throws when both alpha + outputResolution are set. The error message is explicit about why: "The alpha screenshot path does not yet apply deviceScaleFactor and would silently produce composition-resolution frames." Hard-rejection is the right call — silent dimension fallback would be the worst failure mode here.- Test:
renderOrchestrator.test.ts:799—rejects alpha + outputResolution (the alpha capture path doesn't apply DPR yet).
- Test:
- Follow-up — unit test for the silent-failure mode this PR fixes:
packages/engine/src/services/screenshotService.test.ts(new file) — 4 cases covering the clip+scale plumbing:- omits
clipwhendeviceScaleFactoris undefined or 1 - passes
clip: {x:0, y:0, width, height, scale: 2}at DPR=2 - propagates non-2 supersample factor (e.g. 720p → 4K = 3×)
- These directly pin the supersample contract that PR #666's original fix established. Future changes to
pageScreenshotCapturecan't silently drop the clip.
- omits
- Bonus — frameCapture forces screenshot mode at DPR>1:
packages/engine/src/services/frameCapture.ts:117—supersampling = (options.deviceScaleFactor ?? 1) > 1; preMode = … && !supersampling ? "beginframe" : "screenshot". Comment explains BeginFrame ignoresdeviceScaleFactorso the screenshot path is required for any supersampled render. This is the systemic fix for the silent-fallback class of bug, not just a test for the original symptom.
— Vai
b39bb8d to
2a097c4
Compare
26f1a4c to
7617f4c
Compare
7617f4c to
453bd6e
Compare
2a097c4 to
0eaa54e
Compare
The base branch was changed.

What
Fixes the actual end-to-end 4K render path. The previous PRs in this stack (#660–#664) wired
--resolution 4kfrom CLI flag →RenderConfig.outputResolution→resolveDeviceScaleFactor→CaptureOptions.deviceScaleFactor→page.setViewport({ deviceScaleFactor }). The page correctly receivedwindow.devicePixelRatio === 2, but the captured screenshot still came out at 1920×1080, so the encoded MP4 was 1080p despite all the orchestrator logging saying "Supersampling via deviceScaleFactor".This PR makes the screenshot path actually emit device-pixel-sized frames, which produces a real 3840×2160 MP4 at the encoder.
Stacked on #665.
Why
Verified end-to-end with a fresh
hyperframes initproject +hyperframes render --resolution 4k:Before this PR:
The orchestrator logged the supersample, the page reported
devicePixelRatio=2, but every captured JPEG was 1920×1080.After this PR:
Captured frames are 3840×2160 native, text renders crisply at 4K (vector, not upscaled).
Two underlying issues:
Page.captureScreenshotignores viewport DPR by default. Even withEmulation.setDeviceMetricsOverridesettingdeviceScaleFactor: 2, callingPage.captureScreenshotwithout an explicitclip.scalereturns at CSS dimensions (1920×1080), not device dimensions (3840×2160). The viewport DPR affects the layout /window.devicePixelRatiobut not the screenshot bitmap.HeadlessExperimental.beginFramescreenshot ignores DPR entirely. BeginFrame'sScreenshotParamstype has noclip/scale/viewportfield — it captures the compositor surface sized by the OS window in CSS pixels regardless ofdeviceScaleFactor. There's no way to ask BeginFrame for a higher-resolution screenshot.How
Two surgical fixes in
packages/engine/src/services/:screenshotService.ts:pageScreenshotCapture— whendeviceScaleFactor > 1, pass an explicitclipparameter{ x: 0, y: 0, width, height, scale: dpr }toPage.captureScreenshot. Chrome then honors the scale and emits awidth × height × dprbitmap. No effect whendpr === 1(default render path is byte-identical).frameCapture.ts:createCaptureSession— whendeviceScaleFactor > 1, force the screenshot capture mode (skip BeginFrame) since BeginFrame can't supersample. The supersample render is already significantly slower than 1080p (4× pixels), and BeginFrame's perf advantage doesn't matter on a path the user explicitly opted into for higher quality.Test plan
hyperframes init 4k-test --example blank, edited the composition to display "4K TEST" centered texthyperframes render --resolution 4k --output out-4k.mp4—ffprobereportswidth=3840 height=2160, JPEG sample frame is3840×2160, text renders crisply at native 4K (vector, not upscaled)hyperframes render --output out-1080p.mp4(no--resolution) — still produces1920×1080(no regression on the default path)Limitations / future work
Emulation.setDeviceMetricsOverridewithwidth: outputWidth, height: outputHeight, deviceScaleFactor: 1(so the layout viewport matches device pixels) — but that breaks composition CSS layouts that assume 1920×1080. The current trade-off favors correctness.CLAUDE.md's baseline-in-Docker rule).