Files
hyperframes/packages/engine/src/config.test.ts
T
James 67bb56c703 feat(engine): browserGpuMode "auto" — probe WebGL once, fall back to software
When the host doesn't have a usable GPU (CI containers, eval rigs without
GPU passthrough, dev VMs), Chrome's hardware-mode WebGL flags
(`--use-gl=egl/metal/d3d11`) silently leave WebGL unavailable —
`getContext("webgl")` returns null, three.js' WebGLRenderer dies, the
canvas stays black. Surfaced today by Abhay's c2v-eval failing on a
docker render of an hf bundle that uses three.js + a custom fragment
shader.

The fix that's been there: `--use-gl=angle --use-angle=swiftshader` (CPU
software WebGL, ~5-50× slower but pixel-identical). The engine already
exposed `browserGpuMode: "software"` for this. The gap was discovery —
users had to know to pass `--no-browser-gpu` on no-GPU hosts.

This change adds `browserGpuMode: "auto"` (now the CLI default for local
renders): on first launch in the process, probe Chrome with hardware
args, check `canvas.getContext("webgl") !== null`, cache the result.
~1-2 s on first render, free on every subsequent render in the same
worker. Hardware GPUs keep their fast path; no-GPU hosts get SwiftShader
without ceremony.

Behaviour matrix:
- No flag, no env, local       → "auto" (NEW default)
- `--browser-gpu`              → "hardware" (force; errors if no GPU)
- `--no-browser-gpu`           → "software" (force SwiftShader)
- `PRODUCER_BROWSER_GPU_MODE`  → "hardware" / "software" / "auto" / unset
- Docker mode                  → forced "software" (unchanged)

Engine-config default stays "software" (conservative for embedders); the
"auto" default lives in the CLI's `resolveBrowserGpuForCli` so producer
embedders aren't surprised by a probe-on-launch.

Also adds `--enable-unsafe-swiftshader` to the software flag set —
Chrome 120+ deprecated implicit SwiftShader fallback and emits a
deprecation warning unless the flag is set explicitly. Despite the
"unsafe" name this is exactly the pre-deprecation behaviour; the rename
is about Chrome's threat model on the open web, not about the rendering
itself.

Verification:
- Engine 535/535 + CLI 256/256 (incl. new probe tests + tri-state CLI test)
- Empirical: probe on this no-GPU devbox returns "software" in 240 ms,
  cached 0 ms on subsequent calls
- Format / lint / typecheck clean across all packages

Refs the Abhay/Slack thread on c2v-eval rendering without a GPU node.
2026-05-06 17:33:55 +00:00

142 lines
4.3 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { resolveConfig, DEFAULT_CONFIG } from "./config.js";
describe("resolveConfig", () => {
const savedEnv = new Map<string, string | undefined>();
function setEnv(key: string, value: string) {
savedEnv.set(key, process.env[key]);
process.env[key] = value;
}
beforeEach(() => {
savedEnv.clear();
});
afterEach(() => {
for (const [key, value] of savedEnv) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
});
it("returns defaults when no overrides or env vars are set", () => {
const config = resolveConfig();
expect(config.fps).toBe(30);
expect(config.quality).toBe("standard");
expect(config.format).toBe("jpeg");
expect(config.jpegQuality).toBe(80);
expect(config.browserGpuMode).toBe("software");
expect(config.enableStreamingEncode).toBe(true);
expect(config.streamingEncodeMaxDurationSeconds).toBe(240);
expect(config.audioGain).toBe(1);
expect(config.debug).toBe(false);
});
it("applies explicit overrides over defaults", () => {
const config = resolveConfig({ fps: 60, debug: true });
expect(config.fps).toBe(60);
expect(config.debug).toBe(true);
// Non-overridden fields remain at defaults
expect(config.quality).toBe("standard");
});
it("reads numeric env vars with PRODUCER_ prefix", () => {
setEnv("PRODUCER_MAX_WORKERS", "4");
setEnv("PRODUCER_CORES_PER_WORKER", "3");
const config = resolveConfig();
expect(config.concurrency).toBe(4);
expect(config.coresPerWorker).toBe(3);
});
it("reads boolean env vars (true/false strings)", () => {
setEnv("PRODUCER_DISABLE_GPU", "true");
setEnv("PRODUCER_ENABLE_BROWSER_POOL", "true");
const config = resolveConfig();
expect(config.disableGpu).toBe(true);
expect(config.enableBrowserPool).toBe(true);
});
it("lets env vars opt out of default streaming encode", () => {
setEnv("PRODUCER_ENABLE_STREAMING_ENCODE", "false");
const config = resolveConfig();
expect(config.enableStreamingEncode).toBe(false);
});
it("reads the streaming encode duration cutoff from env", () => {
setEnv("PRODUCER_STREAMING_ENCODE_MAX_DURATION_SECONDS", "120");
const config = resolveConfig();
expect(config.streamingEncodeMaxDurationSeconds).toBe(120);
});
it("clamps negative streaming encode duration cutoff env values to zero", () => {
setEnv("PRODUCER_STREAMING_ENCODE_MAX_DURATION_SECONDS", "-1");
const config = resolveConfig();
expect(config.streamingEncodeMaxDurationSeconds).toBe(0);
});
it("treats non-'true' boolean env vars as false", () => {
setEnv("PRODUCER_DISABLE_GPU", "yes");
const config = resolveConfig();
expect(config.disableGpu).toBe(false);
});
it("reads browser GPU mode from env", () => {
setEnv("PRODUCER_BROWSER_GPU_MODE", "hardware");
const config = resolveConfig();
expect(config.browserGpuMode).toBe("hardware");
});
it("accepts 'auto' as a valid browser GPU mode env value", () => {
setEnv("PRODUCER_BROWSER_GPU_MODE", "auto");
const config = resolveConfig();
expect(config.browserGpuMode).toBe("auto");
});
it("falls back to software browser GPU mode for invalid env values", () => {
setEnv("PRODUCER_BROWSER_GPU_MODE", "native");
const config = resolveConfig();
expect(config.browserGpuMode).toBe("software");
});
it("explicit overrides take precedence over env vars", () => {
setEnv("PRODUCER_CORES_PER_WORKER", "5");
const config = resolveConfig({ coresPerWorker: 8 });
expect(config.coresPerWorker).toBe(8);
});
it("falls back to defaults for invalid numeric env vars", () => {
setEnv("PRODUCER_CORES_PER_WORKER", "not-a-number");
const config = resolveConfig();
expect(config.coresPerWorker).toBe(DEFAULT_CONFIG.coresPerWorker);
});
it("clamps chunkSizeFrames to minimum of 120", () => {
setEnv("PRODUCER_CHUNK_SIZE_FRAMES", "50");
const config = resolveConfig();
expect(config.chunkSizeFrames).toBe(120);
});
it("clamps frameDataUriCacheLimit to minimum of 32", () => {
setEnv("PRODUCER_FRAME_DATA_URI_CACHE_LIMIT", "10");
const config = resolveConfig();
expect(config.frameDataUriCacheLimit).toBe(32);
});
});