Merge pull request #773 from heygen-com/feat/producer-plan-validate-no-gpu

feat(producer): plan-time validator — reject GPU encode
This commit is contained in:
James Russo
2026-05-13 16:37:47 -04:00
committed by GitHub
2 changed files with 165 additions and 0 deletions
@@ -0,0 +1,89 @@
/**
* Tests for plan-time validators. Each validator pins both branches:
*
* - PASS — the config is acceptable; no throw.
* - FAIL — the config trips a banned-in-distributed-mode rule; throws
* PlanValidationError with the expected typed `code`.
*/
import { describe, expect, it } from "bun:test";
import {
BROWSER_GPU_NOT_SOFTWARE,
PlanValidationError,
validateNoGpuEncode,
} from "./planValidation.js";
describe("PlanValidationError", () => {
it("preserves the typed `code` field", () => {
const err = new PlanValidationError("EXAMPLE_CODE", "msg");
expect(err.code).toBe("EXAMPLE_CODE");
expect(err.message).toBe("msg");
expect(err.name).toBe("PlanValidationError");
expect(err).toBeInstanceOf(Error);
});
});
describe("validateNoGpuEncode", () => {
it("accepts a software-only config (no fields set)", () => {
expect(() => validateNoGpuEncode({})).not.toThrow();
});
it("accepts useGpu=false + browserGpuMode='software'", () => {
expect(() => validateNoGpuEncode({ useGpu: false, browserGpuMode: "software" })).not.toThrow();
});
it("accepts useGpu=undefined (in-process default) + browserGpuMode='software'", () => {
expect(() => validateNoGpuEncode({ browserGpuMode: "software" })).not.toThrow();
});
it("throws BROWSER_GPU_NOT_SOFTWARE when useGpu === true", () => {
let caught: unknown;
try {
validateNoGpuEncode({ useGpu: true });
} catch (err) {
caught = err;
}
expect(caught).toBeInstanceOf(PlanValidationError);
expect((caught as PlanValidationError).code).toBe(BROWSER_GPU_NOT_SOFTWARE);
expect((caught as PlanValidationError).code).toBe("BROWSER_GPU_NOT_SOFTWARE");
expect((caught as Error).message).toContain("GPU encode is banned");
expect((caught as Error).message).toContain("useGpu === true");
});
it("throws BROWSER_GPU_NOT_SOFTWARE when browserGpuMode === 'auto'", () => {
let caught: unknown;
try {
validateNoGpuEncode({ browserGpuMode: "auto" });
} catch (err) {
caught = err;
}
expect(caught).toBeInstanceOf(PlanValidationError);
expect((caught as PlanValidationError).code).toBe(BROWSER_GPU_NOT_SOFTWARE);
expect((caught as Error).message).toContain("Hardware browser GPU is banned");
expect((caught as Error).message).toContain(`"auto"`);
});
it("throws BROWSER_GPU_NOT_SOFTWARE for any non-'software' browserGpuMode value", () => {
for (const mode of ["hardware", "discrete", "any", "swiftshader-fallback"]) {
let caught: unknown;
try {
validateNoGpuEncode({ browserGpuMode: mode });
} catch (err) {
caught = err;
}
expect(caught).toBeInstanceOf(PlanValidationError);
expect((caught as PlanValidationError).code).toBe(BROWSER_GPU_NOT_SOFTWARE);
}
});
it("checks useGpu BEFORE browserGpuMode so the useGpu message wins when both trip", () => {
let caught: unknown;
try {
validateNoGpuEncode({ useGpu: true, browserGpuMode: "auto" });
} catch (err) {
caught = err;
}
expect(caught).toBeInstanceOf(PlanValidationError);
expect((caught as Error).message).toContain("GPU encode is banned");
});
});
@@ -0,0 +1,76 @@
/**
* Plan-time validators for the distributed render pipeline. Each validator
* is invoked before freezing the plan, so banned configurations fail fast
* with a typed non-retryable error instead of being baked into a planDir
* and only surfacing on the chunk worker.
*/
import { BROWSER_GPU_NOT_SOFTWARE } from "@hyperframes/engine";
/**
* Re-export the BROWSER_GPU_NOT_SOFTWARE code so distributed adapters and
* Step Functions / Temporal retry policies can match it without a
* cross-package import.
*/
export { BROWSER_GPU_NOT_SOFTWARE } from "@hyperframes/engine";
/**
* Typed plan-validation error. Workflow adapters key retry policies off the
* `code` field to mark errors as non-retryable.
*/
export class PlanValidationError extends Error {
readonly code: string;
constructor(code: string, message: string) {
super(message);
this.name = "PlanValidationError";
this.code = code;
}
}
/**
* Subset of the merged plan / engine / render config that the GPU validator
* inspects. Both `useGpu` (RenderConfig) and `browserGpuMode` (EngineConfig)
* are optional so callers can pass any of the surrounding config shapes
* without an adapter layer.
*
* - `useGpu === true` → encoder GPU acceleration (NVENC/QSV/VAAPI). Banned
* because GPU encoders produce non-byte-identical output across machines.
* - `browserGpuMode !== "software"` → headless Chrome's WebGL is allowed
* to use hardware GL. Banned because hardware GL is bitwise unstable
* across drivers. Pairs with the runtime `assertSwiftShader` check that
* catches workers whose environment ignores Chrome's `--use-gl=swiftshader`.
*/
export interface ValidateNoGpuEncodeInput {
useGpu?: boolean;
browserGpuMode?: string;
}
/**
* Reject any config that would let GPU encode or hardware-GL slip into a
* distributed render. Throws {@link PlanValidationError} with
* `code === BROWSER_GPU_NOT_SOFTWARE` when either gate trips. The message
* names the offending field so the caller can surface a clean error.
*/
export function validateNoGpuEncode(config: ValidateNoGpuEncodeInput): void {
if (config.useGpu === true) {
throw new PlanValidationError(
BROWSER_GPU_NOT_SOFTWARE,
"[planValidation] GPU encode is banned in distributed mode: " +
"config.useGpu === true. " +
"Distributed retries must be byte-identical, but NVENC/QSV/VAAPI " +
"produce different output across machines. Set useGpu=false (the " +
"default) — software libx264/libx265 is the only supported encoder " +
"in distributed mode.",
);
}
if (config.browserGpuMode !== undefined && config.browserGpuMode !== "software") {
throw new PlanValidationError(
BROWSER_GPU_NOT_SOFTWARE,
`[planValidation] Hardware browser GPU is banned in distributed mode: ` +
`config.browserGpuMode === ${JSON.stringify(config.browserGpuMode)}. ` +
`Hardware GL is bitwise unstable across drivers. Set browserGpuMode="software" ` +
`so Chrome launches with --use-gl=swiftshader.`,
);
}
}