fix(cli,producer): cross-multiply aspect check, CLI HDR guard, honest banner

This commit is contained in:
James
2026-05-07 16:58:25 +00:00
parent 5260429dd9
commit 1545763ea3
3 changed files with 53 additions and 4 deletions
+29
View File
@@ -207,6 +207,35 @@ describe("renderLocal browser GPU config", () => {
expect(producerState.createdJobs[0]?.entryFile).toBeUndefined();
});
it("forwards outputResolution to createRenderJob when --resolution is set", async () => {
await renderLocal("/tmp/project", "/tmp/out.mp4", {
fps: 30,
quality: "standard",
format: "mp4",
gpu: false,
browserGpuMode: "software",
hdrMode: "auto",
quiet: true,
outputResolution: "landscape-4k",
});
expect(producerState.createdJobs[0]?.outputResolution).toBe("landscape-4k");
});
it("omits outputResolution from createRenderJob by default", async () => {
await renderLocal("/tmp/project", "/tmp/out.mp4", {
fps: 30,
quality: "standard",
format: "mp4",
gpu: false,
browserGpuMode: "software",
hdrMode: "auto",
quiet: true,
});
expect(producerState.createdJobs[0]?.outputResolution).toBeUndefined();
});
it("can force the CLI process to exit after a successful local render", async () => {
vi.useFakeTimers();
const exit = vi
+17 -1
View File
@@ -253,6 +253,18 @@ export default defineCommand({
);
process.exit(1);
}
// Reject the --resolution + --hdr combination at the CLI layer so the
// user sees the friendly errorBox before any work directories or
// ffmpeg processes spin up. The orchestrator also enforces this via
// resolveDeviceScaleFactor — defense in depth.
if (args.hdr) {
errorBox(
"Conflicting flags",
"--resolution cannot be combined with --hdr. The HDR pipeline composites at composition dimensions and does not yet support supersampling.",
"Render in two passes: HDR at composition resolution, then upscale separately with ffmpeg.",
);
process.exit(1);
}
}
// ── Validate workers ──────────────────────────────────────────────────
@@ -369,7 +381,11 @@ export default defineCommand({
);
console.log(c.dim(" " + fps + "fps \u00B7 " + quality + " \u00B7 " + workerLabel));
if (outputResolution) {
console.log(c.dim(" Output resolution: " + outputResolution + " (supersampled via DPR)"));
// Don't claim "supersampled" — when the composition is already at the
// target dimensions, the DPR resolves to 1 and no supersampling
// happens. We don't have the composition's dims at this point in the
// CLI, so describe the intent rather than the mechanism.
console.log(c.dim(" Output resolution: " + outputResolution));
}
if (useGpu || browserGpuMode !== "software") {
const gpuModes = [
@@ -616,9 +616,11 @@ export function resolveDeviceScaleFactor(input: {
);
}
const target = CANVAS_DIMENSIONS[input.outputResolution];
const widthRatio = target.width / input.compositionWidth;
const heightRatio = target.height / input.compositionHeight;
if (widthRatio !== heightRatio) {
// Aspect-ratio compare via cross-multiplication so the equality is integer-
// safe. Float division (`target.width / compositionWidth`) loses precision
// for non-power-of-2 ratios (e.g. cinema 4K 4096×2160 = 1.8963…) and a
// future preset could trip a false-mismatch on otherwise valid input.
if (target.width * input.compositionHeight !== target.height * input.compositionWidth) {
throw new Error(
`outputResolution ${input.outputResolution} (${target.width}×${target.height}) ` +
`does not match the aspect ratio of the composition ` +
@@ -626,6 +628,8 @@ export function resolveDeviceScaleFactor(input: {
`Pick a preset whose orientation matches.`,
);
}
// Aspect ratios match → widthRatio === heightRatio. Compute once.
const widthRatio = target.width / input.compositionWidth;
if (widthRatio < 1) {
throw new Error(
`outputResolution ${input.outputResolution} (${target.width}×${target.height}) ` +