refactor: dedupe resolution presets and clean up 4k stack#665
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
vanceingalls
left a comment
There was a problem hiding this comment.
Verdict: approve. Genuine cleanup. The dedupes are real, the dropped parallel Map in the engine cache is a measurable allocation win, and the useRenderQueue.startRender options-object refactor is overdue (4 positional defaulted params is a footgun). Behavior preservation looks right based on the tests.
Important
-
important —
packages/engine/src/services/videoFrameInjector.ts:54-69— Dropping the parallelsizesMap is the right call, but the new code readscache.get(oldestKey)?.length ?? 0before thecache.delete(oldestKey). Correct today (the value is still in the map when we read it), but easy to break if someone reorders. Worth a one-line comment, or invert:const dropped = cache.get(oldestKey); cache.delete(oldestKey); totalBytes = Math.max(0, totalBytes - (dropped?.length ?? 0));— same effect, harder to misread. -
important —
packages/engine/src/services/videoFrameInjector.ts:38-44— JSDoc acknowledges thebytesLimit < single-entryedge case ("the post-insert eviction loop will drop the entry we just inserted") and points to the 64 MB floor as protection. As I noted on #662: 64 MB is not enough for a worst-case 4K PNG (which can exceed 50 MB encoded). Either #662 fixes the underlying invariant before #665 lands, or this PR's JSDoc claim ("safely cacheable") is incorrect. Either fix the floor in #662 or soften the JSDoc here to "in normal operation" rather than "safely." -
important —
packages/core/src/core.types.ts:31-33—VALID_CANVAS_RESOLUTIONS = Object.keys(CANVAS_DIMENSIONS) as readonly CanvasResolution[]— the cast is doing real work.Object.keysreturnsstring[]; the cast trusts that the keys exactly matchCanvasResolution. Today they do because both are derived from the same source, but if anyone adds a key toCANVAS_DIMENSIONSand forgets to updateCanvasResolution, the cast silently lies. Either:- Use
satisfiesto invert the trust: keep the explicit array andsatisfies readonly CanvasResolution[]so TypeScript verifies it matches. - Or define
CanvasResolutionaskeyof typeof CANVAS_DIMENSIONS(single source of truth).
The second is cleaner — the type extends automatically when a preset is added.
- Use
-
important —
packages/studio/src/components/renders/useRenderQueue.ts:14-18— The localResolutionPresettype with the load-bearing comment ("studio's tsconfig doesn't include node types... core barrel transitively pulls in modules withnode:fsimports") is a real architectural smell, but I get why this PR isn't the place to fix it. Worth filing a follow-up ticket:@hyperframes/coreshould have a./types(or./constants) subpath export with zero runtime/node deps, which removes the duplication permanently. Calling this an architecture nit, not a #665 blocker.
Nits
-
nit —
packages/core/src/core.types.ts:31-33—VALID_CANVAS_RESOLUTIONSusesObject.keyswhich has no guaranteed order across JS engines (in practice V8 preserves insertion order for string keys). Tests atindex.test.ts:15-22pin the order — fine, but if anyone reordersCANVAS_DIMENSIONS, they'll get a test failure that looks like an unrelated bug. Worth a comment: "ordering matches insertion order in CANVAS_DIMENSIONS — change with care." -
nit —
packages/studio/src/components/renders/RenderQueue.tsx:20-31— Adding↔to the landscape labels makes orientation explicit (good), but↔is a horizontal-arrow glyph that some monospace fonts render oddly.🖥️/📱are unambiguous but emoji-y. Fine to leave; just a UX note. -
nit —
packages/producer/src/services/renderOrchestrator.ts:2128-2130— HoistingoutputWidth/outputHeightto a single computation site is the right cleanup. Only used in two places (log + perf summary) — fine. IfRenderPerfSummary.resolutionis consumed by metrics downstream, double-check that the change from "composition dims" → "output dims" doesn't break dashboards. From the JSDoc on #663 ("RenderPerfSummary.resolution reports the output dims, not composition dims, so telemetry stays correct") it sounds intended — just confirming. -
nit —
packages/cli/src/commands/render.ts:524-525— JSDoc trim is fine; the contract really does live onresolveDeviceScaleFactor. But the previous version was helpful for readers tracing CLI → orchestrator without jumping packages. A one-line "see resolveDeviceScaleFactor in producer" pointer is what you have — that's enough.
Cross-PR / Stack hygiene
This PR being needed at all is a stack-hygiene smell, and the team should know it: three review agents flagged the same duplications, which means #661 and #663 shipped near-identical code that had already started to drift ("portrait-4k": "portrait-4k" self-mapping in init.ts, missing in render.ts). The right move would have been:
- #660 lands
CanvasResolution,CANVAS_DIMENSIONS, andVALID_CANVAS_RESOLUTIONS+normalizeResolutionFlag— i.e., everything that's pure data + pure functions. The latter two are 10 lines of code; landing them in the foundation costs nothing. - #661 and #663 import the helpers from core. No duplication, no drift.
- #665 doesn't exist (or is a much smaller PR for the JSDoc cleanups + parallel-Map drop only).
The pattern to internalize: if a helper is going to be needed by N consumers, land the helper with the foundation, not in the first consumer. This stack would have been 4 PRs instead of 5, and the merge order would have been more flexible. I'd note this in the next retro.
That said, given the duplication did happen, this PR is the correct way to clean it up — and the discipline of including dedicated tests for the new core exports (VALID_CANVAS_RESOLUTIONS, normalizeResolutionFlag) is right.
Praise
- The options-object refactor of
useRenderQueue.startRenderis a real ergonomic improvement. Positional booleans + defaults are the kind of thing that breaks subtly six months later when someone reorders. - The parallel-Map drop in
videoFrameInjectoris the kind of tiny perf win that adds up across a million-frame render — good catch. - Honest, thorough PR description (acknowledging the dedup smell rather than papering over it). That signal is valuable.
— Vai
miguel-heygen
left a comment
There was a problem hiding this comment.
Approving this refactor layer itself.
I did not find a new correctness issue in the #665 diff against #664. The shared CanvasResolution / normalizeResolutionFlag cleanup is the right direction, and the focused top-of-stack verification passed after bootstrapping generated runtime artifacts:
bun test packages/core/src/index.test.ts packages/core/src/studio-api/routes/render.test.ts packages/producer/src/services/renderOrchestrator.test.ts packages/engine/src/services/videoFrameInjector.test.ts packages/cli/src/commands/init.test.ts packages/cli/src/utils/dockerRunArgs.test.tsbun run --filter @hyperframes/studio typecheckbun run --filter @hyperframes/cli typecheckbun run --filter @hyperframes/core typecheckbunx oxlinton the touched files
Caveats before the stack lands: this PR inherits the lower-layer blockers I requested changes on (#661, #663, #664), and several #665 regression shards were still pending when I rechecked. My approval is for the refactor layer, not for landing the full stack as-is.
I rechecked the live head before posting: 1753b493f854eac35b393afcd2a26e3f9fa9ca4c.
1753b49 to
994d013
Compare
246b1d2 to
9a6dedc
Compare
994d013 to
b39bb8d
Compare
vanceingalls
left a comment
There was a problem hiding this comment.
Re-review — APPROVE
Verdict shift: COMMENT (1 important) → APPROVE.
Reference: prior review #665 (review)
Addressed since prior review (commit b39bb8d0)
- Important — type derivation from canonical table:
packages/core/src/core.types.ts:31—export type CanvasResolution = keyof typeof CANVAS_DIMENSIONS;. Adding a row toCANVAS_DIMENSIONSnow extends the union automatically; the prioras readonly CanvasResolution[]cast onVALID_CANVAS_RESOLUTIONSno longer hides drift because the source-of-truth is the table. Inline comment explains why.- The cast on
VALID_CANVAS_RESOLUTIONS = Object.keys(CANVAS_DIMENSIONS) as readonly CanvasResolution[]is still present, but now it's a runtime-list-of-keys-of-table — the type is derived from the same table, so drift is structurally impossible. Order is pinned by tests inindex.test.tsper the comment. This matches the prior review's recommendation.
- The cast on
- JSDoc 64 MB claim corrected:
packages/engine/src/config.ts:79-92— the misleading "64 MB safely caches 4K" wording is gone. New doc explains the budget is sized so a worker stays "comfortably under a few GB even at 4K (where each PNG frame is ~25 MB and the base64 data URI is ~33 MB)" — accurate. TheMath.max(64, …)floor inresolveConfigis now just an absolute hard floor without the misleading explanation. - Dedupe achieved at the right surfaces:
init.tsandrender.tsboth importnormalizeResolutionFlagfrom@hyperframes/core— no local copies.studio-api/routes/render.tsimportsVALID_CANVAS_RESOLUTIONSfrom canonical core types.
Still open
None of the prior items.
New findings (nit)
useRenderQueue.ts:14keeps a localResolutionPresetcopy with an explicit JSDoc explaining studio's tsconfig avoids the core barrel (node types). Documented exception — fine.vite.config.ts:80keeps an inline literal union for thecreateRenderJobreturn type. Same justification (Vite SSR loader avoids importing the module it dynamically loads). 4th drift surface, but documented intent. Not blocking — both are stable and the canonical source is now firmly incore.types.ts.
— Vai
9a6dedc to
1ef961a
Compare
b39bb8d to
2a097c4
Compare

What
Post-review cleanup of the 4K rendering stack (#660–#664). De-duplicates resolution constants/aliases/normalizer across 5 packages, simplifies
useRenderQueue.startRenderto an options object, removes verbose JSDoc, and drops a redundant parallel Map in the engine's frame cache.Stacked on #664.
Why
Three review agents (reuse / quality / efficiency) flagged the same handful of issues:
RESOLUTION_ALIASES+normalizeResolutionFlagwere duplicated verbatim incli/init.tsandcli/render.ts. Theinit.tscopy had already drifted (added a redundant"portrait-4k": "portrait-4k"self-mapping).VALID_RESOLUTIONSset/array appeared in 4 places (two CLI files, the studio-api route, and an inline string-union instudio-api/types.ts).useRenderQueue.startRender(fps, quality, format, resolution)had grown to 4 positional params, all defaulted, all string-shaped — fragile.Mapeven thoughcache.get(key).lengthreturns the same value. Real allocation pressure during a 4K render with thousands of frames.resolveDeviceScaleFactor's contract verbatim.resolveDeviceScaleFactorJSDoc had a confusing parenthetical about non-integer scales that contradicted itself (cited 720p → 4K as non-integer when 720×3 = 2160 is integer).executeRenderJob(once for log, once for perf summary).How
Core (
packages/core/src/core.types.ts,index.ts):VALID_CANVAS_RESOLUTIONS(derived fromObject.keys(CANVAS_DIMENSIONS)) andnormalizeResolutionFlag()next toCANVAS_DIMENSIONS. Re-exported from the core barrel.index.test.tscovering the new exports, including alias resolution and case-insensitivity.CLI (
init.ts,render.ts):import { normalizeResolutionFlag, ... } from "@hyperframes/core".Studio-API route (
render.ts):VALID_RESOLUTIONSnow derives fromVALID_CANVAS_RESOLUTIONSinstead of hard-coded.studio-api/types.tsreplaced withCanvasResolution.Studio (
useRenderQueue.ts,RenderQueue.tsx,App.tsx):startRendernow takesStartRenderOptionsinstead of 4 positional args. Existing call site simplifies from(format, quality, resolution) => renderQueue.startRender(30, quality, format, resolution)to(format, quality, resolution) => renderQueue.startRender({ format, quality, resolution }).ResolutionPresetkept local (4 string literals) with a load-bearing comment explaining why — studio's tsconfig doesn't include node types and the core barrel transitively pulls in modules withnode:fsimports. Adding a./typescore subpath would be more invasive than the duplication is worth.1080p ↔/1080p ↕/4K ↔/4K ↕(was: only the portrait variants had a marker).Engine (
videoFrameInjector.ts):sizes: Map<string, number>— derive byte size fromcache.get(key)?.lengthon demand. Removes one Map allocation + per-insert/per-evict bookkeeping. Adds a JSDoc note about thebytesLimit < single-entryedge case being prevented by the 64MB floor onbytesLimitMb.Producer (
renderOrchestrator.ts):outputWidth/outputHeightonce afterresolveDeviceScaleFactorand reuse for both the supersample log andRenderPerfSummary.resolution.resolveDeviceScaleFactorJSDoc.RenderConfig.outputResolutionJSDoc from 16 lines to 4 (the constraints live onresolveDeviceScaleFactoralready).Trim
applyResolutionPresetJSDoc — drop the bullet list of which fields get rewritten (the function body shows that). Keep the load-bearing "regex not DOM" rationale.Test plan
VALID_CANVAS_RESOLUTIONS+normalizeResolutionFlag(covers alias resolution, case-insensitivity, undefined).bun run --cwd packages/core test— 685/685 pass (2 new)bun run --cwd packages/cli test— 283/283 passbun run --cwd packages/studio test— 271/271 passbun run --cwd packages/engine test— 541/541 passbunx vitest— 50/51 (1 pre-existing failure onmain, unrelated)