Skip to content

feat(cli): add --resolution flag to hyperframes render for one-line 4k#663

Open
jrusso1020 wants to merge 2 commits into05-07-fix_engine_byte-budget_the_frame_data_uri_cache_to_bound_memory_at_4kfrom
05-07-feat_cli_add_--resolution_flag_to_hyperframes_render_for_one-line_4k
Open

feat(cli): add --resolution flag to hyperframes render for one-line 4k#663
jrusso1020 wants to merge 2 commits into05-07-fix_engine_byte-budget_the_frame_data_uri_cache_to_bound_memory_at_4kfrom
05-07-feat_cli_add_--resolution_flag_to_hyperframes_render_for_one-line_4k

Conversation

@jrusso1020
Copy link
Copy Markdown
Collaborator

@jrusso1020 jrusso1020 commented May 7, 2026

What

Adds a --resolution flag to hyperframes render. Pass --resolution 4k to render any composition (1080p or otherwise) at 4K — Chrome deviceScaleFactor does the supersampling, the composition HTML is never touched.

hyperframes render --resolution 4k --output 4k.mp4

This is the missing piece for the simple 4K UX. PRs #660#662 added the type system, scaffold flag, and memory safety; this one delivers the user-facing one-liner for existing projects.

Why

After PRs #660#662 a brand-new project can be 4K via init --resolution 4k, and the engine no longer OOMs at 4K. But for someone with an existing 1080p project, the only path to 4K was hand-editing five locations in the composition (data-width, data-height, data-resolution, #stage CSS, <meta viewport>) and re-rendering. That's not "simple UX."

This PR closes the gap. Now every existing user gets:

"Render in 4K? Pass --resolution 4k. Done."

How

Orchestrator (renderOrchestrator.ts):

  • RenderConfig.outputResolution?: CanvasResolution — accepts landscape | portrait | landscape-4k | portrait-4k.
  • New exported resolveDeviceScaleFactor() helper takes composition dims + outputResolution + HDR flag and returns the integer DPR. Centralizes the validation:
    • HDR + outputResolution → reject (HDR layered compositor needs parallel pixel-buffer scaling).
    • Aspect ratio mismatch (e.g. landscape comp → portrait-4k) → reject.
    • Downsample (output < composition) → reject.
    • Non-integer scale → reject (avoids subpixel-text aliasing).
  • deviceScaleFactor is wired into both CaptureOptions construction sites (probe + render).
  • RenderPerfSummary.resolution reports the output dims, not composition dims, so telemetry stays correct.
  • The screenshot capture path uses image2pipe to ffmpeg, which auto-detects dimensions from the image bytes, so the encoder needs zero changes.

CLI (render.ts):

  • --resolution flag with the same alias map as init (1080p, 4k, uhd, etc.).
  • Validation up-front: invalid presets exit before any heavy work.
  • Render plan banner now prints Output resolution: landscape-4k (supersampled via DPR) so users see what's about to happen.
  • Threaded into RenderOptionscreateRenderJob (local) and buildDockerRunArgs (docker).

Docker arg builder (dockerRunArgs.ts):

  • New outputResolution?: string field, forwarded as --resolution <preset> to the in-container CLI. Two new tests in dockerRunArgs.test.ts cover present + absent cases — the test file's existing tripwire pattern guards against silent flag drops.

Docs (docs/guides/4k-rendering.mdx):

  • New guide covering both 4K paths (author at 4K with init, supersample at render). Quickstart, presets table, how deviceScaleFactor works, all four constraints with example error messages, performance notes (capture/encode/memory), and a pointer to upcoming Studio support.
  • Linked from docs/packages/cli.mdx's render flag table and registered in docs/docs.json next to the HDR guide.

Test plan

  • Unit tests added/updated:
    • 7 new orchestrator tests for resolveDeviceScaleFactor (default = 1, 1080p → 4K = 2, portrait → portrait-4k, native 4K = 1, HDR rejection, aspect mismatch, downsample, non-integer).
    • 2 new dockerRunArgs tests (forwards --resolution when set, omits when not). Uses the existing tripwire pattern.
  • Manual testing performed:
    • bun run --cwd packages/cli test — 283/283 pass
    • bunx vitest run packages/producer/src/services/renderOrchestrator.test.ts — 50/51 pass; the 1 failing test (rejects a maliciously crafted key…) also fails on clean main and is unrelated to this change.
  • Documentation updated — new guide at docs/guides/4k-rendering.mdx, render flag table updated in docs/packages/cli.mdx, navigation registered in docs/docs.json.

Copy link
Copy Markdown
Collaborator Author

jrusso1020 commented May 7, 2026

Copy link
Copy Markdown
Collaborator

@vanceingalls vanceingalls left a comment

Choose a reason for hiding this comment

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

Verdict: approve. This is the right shape: keep the composition pristine, lift output dims via Chrome DPR, validate the constraints up-front. The aspect / integer-scale / no-downsample / no-HDR guards are all the right rejections to fail loudly on. Tests cover each branch. Docs guide is genuinely useful.

Important

  • importantpackages/producer/src/services/renderOrchestrator.ts:619-624 — The aspect-ratio check uses widthRatio !== heightRatio with float division. For all the canonical preset pairs in CANVAS_DIMENSIONS the ratios are exact integers (so === holds), but if anyone adds a non-power-of-2 preset later (e.g. cinema 4K 4096×2160 with a 1.8963 ratio), the check becomes float-fragile. Use target.width * input.compositionHeight === target.height * input.compositionWidth (cross-multiplication is integer-safe). Also: when the check fails, the error message lists "aspect ratio" but the actual cause might be "non-integer scale" if both dims are off. Today's code lumps both into the aspect message via short-circuit ordering — a 1500×844 → 4K case throws "aspect ratio mismatch" rather than the more helpful "non-integer scale." Test at line 822 acknowledges this with /aspect ratio|non-integer/. Worth ordering the checks integer-scale-first, OR being explicit in the message that "aspect-or-non-integer-scale rejection."

  • importantpackages/cli/src/commands/render.ts:240-248 — The flag-validation path is before --composition resolution and --workers validation. Good. But: there's no validation that the user hasn't combined --resolution with --hdr at the CLI level. Today the rejection happens inside resolveDeviceScaleFactor deep in the orchestrator, which means by the time the user sees the error, ffmpeg may have been spun up, work directories may have been created, and the error path is a thrown exception bubbling up rather than the friendly errorBox you use for other invalid combinations. Add the HDR-incompatibility check next to the resolution validation.

  • importantpackages/cli/src/commands/render.ts:494-532outputResolution is threaded into RenderOptions and forwarded to renderLocal and renderDocker separately. Two places to update if anything changes. The dockerRunArgs test catches the docker case — good. The local createRenderJob path is not covered by an end-to-end test that proves the orchestrator actually receives it. The 7 unit tests cover resolveDeviceScaleFactor in isolation, but not "CLI flag → orchestrator config." Add at minimum a test that asserts createRenderJob receives outputResolution: "landscape-4k" when the flag is --resolution 4k.

  • importantdocs/guides/4k-rendering.mdx:104-108 — Doc claim: "ffmpeg auto-detects the dimensions from the screenshot stream and encodes at 4K." This is correct for image2pipe with PNG/JPEG, but worth verifying the encoder isn't being passed an explicit -s 1920x1080 somewhere in the ffmpeg invocation. If it is, the encoded video downscales silently and the user gets a 1080p file labeled "4k.mp4". I'd want to see one end-to-end test that actually runs ffprobe on the output (the doc shows this as a verification step — it should be part of CI). Without that, "renders 4K" is a doc claim, not a tested guarantee.

  • importantpackages/cli/src/commands/render.ts:343-345 — The render plan banner prints "Output resolution: landscape-4k (supersampled via DPR)" but only if outputResolution is set. For users who pass --resolution 4k against a native 4K composition (where DPR ends up as 1, no supersampling), the banner is misleading. Either suppress the "(supersampled via DPR)" suffix when the composition already matches, or print "(no-op — composition is already 4K)". Minor UX paper cut, but the wrong information is worse than no information.

Nits

  • nitpackages/producer/src/services/renderOrchestrator.ts:603-619resolveDeviceScaleFactor JSDoc still says "Throws on: HDR + outputResolution combination" — accurate. The comment about non-integer "(e.g. 720p composition, 4K output → 3× height but the width ratio is also 3× ✓; 1080p portrait → 4K landscape would mismatch)" is confusing because 720p → 4K is an integer scale (3×) and the example accidentally argues the opposite. #665 fixes this. Fine to leave as-is in this PR since #665 is queued.

  • nitpackages/cli/src/commands/render.ts:60-77RENDER_RESOLUTION_ALIASES lacks "portrait-4k": "portrait-4k" (which init.ts has, also needlessly). The asymmetry between init.ts and render.ts alias maps is exactly the smell #665 fixes. Same nit — drop in a future PR (or wait for #665 to clean it).

  • nitdocs/guides/4k-rendering.mdx:130-138 — "Performance" section gives multiplicative estimates (3-4×, 2-3×, 3-5×). These are believable but uncited; if there's a benchmark we can link to, that's better than orders-of-magnitude estimates that may drift.

  • nitpackages/producer/src/services/renderOrchestrator.test.ts:822-832 — The non-integer test comment notes the regex /aspect ratio|non-integer/ is permissive because the orchestrator throws aspect first. Once you re-order checks (see "important" above), tighten to /non-integer/.

  • nitdocs/guides/4k-rendering.mdx:103 — Three tildes/apostrophes in "Steps" component — verify that mintlify renders this correctly. (Not a code review concern; just flagging for the docs reviewer.)

Cross-PR

  • Consumes CanvasResolution from #660 — clean.
  • Consumes the frameDataUriCacheBytesLimitMb from #662 implicitly: 4K renders at scale will exercise that budget. The combo is the actual prod risk. End-to-end test of "render at 4K with default config, prove cache eviction stats are sane" would close the loop on the whole stack.
  • The buildDockerRunArgs test pattern (assert flag forwarded + assert flag absent) is the right tripwire for a feature that fans out to a docker arg — well done.

Praise

resolveDeviceScaleFactor as a pure, testable, exported helper is exactly the right shape — the orchestrator has too many call sites for inline validation to be safe. The four guards (HDR, aspect, downsample, non-integer) are the correct set of "fail loudly" rejections. Centralizing the supersample log + perf summary at one site is clean. Good docs guide — concrete examples, ffprobe verification, and the "two paths" framing (author at 4K vs supersample) makes the UX legible.

— Vai

Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen left a comment

Choose a reason for hiding this comment

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

Requesting changes for the HDR boundary around --resolution.

The new guard only passes hdrRequested: job.config.hdrMode === "force-hdr" into resolveDeviceScaleFactor(). That blocks --hdr --resolution 4k, but it does not block the default auto-HDR path. Auto-detected HDR is decided later from extracted video/image color spaces, so an HDR-source composition rendered with --resolution 4k can still enter the HDR layered compositor.

That path is not just a docs mismatch. The DOM capture path receives the scaled deviceScaleFactor, but the HDR video encoder/compositor setup still uses the authored width / height. So the render can either hit mismatched layer dimensions or produce a non-4K HDR output while the earlier log/perf metadata says the output is scaled.

Please reject outputResolution after effectiveHdr is known, or fully scale the HDR layered compositor path. Either way, add a regression test for auto-detected HDR plus outputResolution; forced HDR coverage alone does not catch this.

I rechecked the live head before posting: 00da353e21ac88862820ee60ad9f8e7fe81e59fe.

@jrusso1020 jrusso1020 force-pushed the 05-07-feat_cli_add_--resolution_flag_to_hyperframes_render_for_one-line_4k branch from 00da353 to 41dc94a Compare May 7, 2026 05:05
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.

3 participants