Files
hyperframes/packages/engine/src/utils/gpuEncoder.test.ts
T
Miguel Ángel e5346afdd8 fix(engine): Linux GPU path uses deprecated EGL + NVENC probe fails on data-center GPUs (#1504)
* fix(engine): use ANGLE-EGL for Linux GPU path, bump NVENC probe size

Chrome 131+ rejects --use-gl=egl in headless shell; the GPU process
exits and the renderer silently falls back to SwiftShader. Switch to
(gl=angle, angle=gl-egl) which is on the headless-shell allowlist,
and add --ignore-gpu-blocklist + --disable-software-rasterizer so
data-center GPUs (L4/T4/A10) are not blocked.

Also bump the NVENC probe frame from 16×16 to 320×240 — NVIDIA
data-center cards require ≥257 on each dimension and reject the
smaller size with "Frame Dimension less than the minimum supported
value", causing the encoder probe to silently fall back to libx264.

Closes #1493

* fix(engine): address review feedback — probe test, observability, comments

- Export getProbeArgs and add test pinning 320×240 probe dimensions
  across all 5 GPU encoder backends (nvenc/videotoolbox/vaapi/qsv/amf)
- Add driver/SKU rationale comment on the probe size constant with
  context about NVIDIA data-center card behavior vs documented minimums
- Add rationale comment on --ignore-gpu-blocklist (operator opted into
  hardware mode explicitly)
- Log resolved GL flags at browser launch for GPU fallback observability
2026-06-16 12:07:04 -04:00

141 lines
4.2 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import {
getCompiledGpuEncoders,
getGpuEncoderName,
getProbeArgs,
mapPresetForGpuEncoder,
selectUsableGpuEncoder,
} from "./gpuEncoder.js";
afterEach(() => {
vi.useRealTimers();
});
describe("getCompiledGpuEncoders", () => {
it("recognizes AMD AMF in FFmpeg's encoder list", () => {
expect(
getCompiledGpuEncoders(`
V....D h264_nvenc NVIDIA NVENC H.264 encoder
V....D h264_amf AMD AMF H.264 Encoder
V....D h264_qsv H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (Intel Quick Sync Video)
`),
).toEqual(["nvenc", "qsv", "amf"]);
});
});
describe("selectUsableGpuEncoder", () => {
it("runs probe checks concurrently while preserving candidate priority", async () => {
vi.useFakeTimers();
const started: string[] = [];
const usable = selectUsableGpuEncoder(["nvenc", "amf"], async (encoder) => {
started.push(encoder);
await new Promise((resolve) => setTimeout(resolve, encoder === "nvenc" ? 50 : 1));
return true;
});
await vi.advanceTimersByTimeAsync(1);
expect(started).toEqual(["nvenc", "amf"]);
await vi.advanceTimersByTimeAsync(49);
expect(await usable).toBe("nvenc");
});
it("falls through from compiled-but-unusable NVENC to usable AMD AMF", async () => {
const usable = await selectUsableGpuEncoder(["nvenc", "amf"], async (encoder) => {
return encoder === "amf";
});
expect(usable).toBe("amf");
});
it("treats rejected probe checks as unusable", async () => {
const usable = await selectUsableGpuEncoder(["nvenc", "amf"], async (encoder) => {
if (encoder === "nvenc") {
throw new Error("driver probe failed");
}
return encoder === "amf";
});
expect(usable).toBe("amf");
});
});
describe("getGpuEncoderName", () => {
it("maps AMD AMF to FFmpeg's h264 and hevc encoder names", () => {
expect(getGpuEncoderName("amf", "h264")).toBe("h264_amf");
expect(getGpuEncoderName("amf", "h265")).toBe("hevc_amf");
});
});
describe("mapPresetForGpuEncoder", () => {
describe("nvenc", () => {
it.each([
["ultrafast", "p1"],
["superfast", "p1"],
["veryfast", "p2"],
["faster", "p3"],
["fast", "p4"],
["medium", "p4"],
["slow", "p5"],
["slower", "p6"],
["veryslow", "p7"],
["placebo", "p7"],
])("maps libx264 preset %s to NVENC %s", (input, expected) => {
expect(mapPresetForGpuEncoder("nvenc", input)).toBe(expected);
});
it.each(["p1", "p2", "p3", "p4", "p5", "p6", "p7"])(
"passes NVENC-native preset %s through unchanged",
(preset) => {
expect(mapPresetForGpuEncoder("nvenc", preset)).toBe(preset);
},
);
it("falls back to p4 for unknown preset values", () => {
expect(mapPresetForGpuEncoder("nvenc", "nonsense")).toBe("p4");
});
});
describe("qsv", () => {
it.each([
["ultrafast", "veryfast"],
["superfast", "veryfast"],
["placebo", "veryslow"],
])("rewrites libx264-only preset %s to QSV-supported %s", (input, expected) => {
expect(mapPresetForGpuEncoder("qsv", input)).toBe(expected);
});
it.each(["veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"])(
"passes supported preset %s through unchanged",
(preset) => {
expect(mapPresetForGpuEncoder("qsv", preset)).toBe(preset);
},
);
});
describe("other encoders", () => {
it.each(["videotoolbox", "vaapi", "amf"] as const)(
"passes preset through unchanged for %s",
(encoder) => {
expect(mapPresetForGpuEncoder(encoder, "medium")).toBe("medium");
expect(mapPresetForGpuEncoder(encoder, "ultrafast")).toBe("ultrafast");
},
);
it("passes preset through unchanged when encoder is null (CPU)", () => {
expect(mapPresetForGpuEncoder(null, "ultrafast")).toBe("ultrafast");
});
});
});
describe("getProbeArgs", () => {
it("uses 320x240 probe dimensions for all GPU encoders", () => {
const encoders = ["nvenc", "videotoolbox", "vaapi", "qsv", "amf"] as const;
for (const encoder of encoders) {
const args = getProbeArgs(encoder);
expect(args).toContain("color=size=320x240:rate=1:duration=1");
}
});
});