mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(render): avoid empty WAAPI scans and llvmpipe auto GPU (#1775)
Avoid the screenshot-path #1715 regression by skipping empty WAAPI/CSS animation scans per seek and classifying known software WebGL renderers correctly in browserGpuMode=auto.\n\nAddresses #1715.
This commit is contained in:
@@ -67,12 +67,24 @@ describe("buildChromeArgs browser GPU mode", () => {
|
||||
});
|
||||
|
||||
describe("resolveBrowserGpuMode", () => {
|
||||
const setMockWebGlProbe = (info: { hasWebGL: boolean; vendor: string; renderer: string }) => {
|
||||
const close = vi.fn().mockResolvedValue(undefined);
|
||||
const evaluate = vi.fn().mockResolvedValue(info);
|
||||
const launch = vi.fn().mockResolvedValue({
|
||||
newPage: vi.fn().mockResolvedValue({ evaluate }),
|
||||
close,
|
||||
});
|
||||
_setPuppeteerForTests({ launch } as unknown as PuppeteerNode);
|
||||
return { close, launch };
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
_resetAutoBrowserGpuModeCacheForTests();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
_setPuppeteerForTests(undefined);
|
||||
_resetAutoBrowserGpuModeCacheForTests();
|
||||
});
|
||||
|
||||
@@ -138,6 +150,74 @@ describe("resolveBrowserGpuMode", () => {
|
||||
const results = await Promise.all([p1, p2, p3]);
|
||||
expect(results).toEqual(["software", "software", "software"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"llvmpipe",
|
||||
"Google Inc. (Mesa/X.org)",
|
||||
"ANGLE (Mesa/X.org, llvmpipe (LLVM 12.0.0 256 bits), OpenGL ES 3.2)",
|
||||
],
|
||||
[
|
||||
"Microsoft Basic Render Driver",
|
||||
"Google Inc. (Microsoft)",
|
||||
"ANGLE (Microsoft, Microsoft Basic Render Driver Direct3D11 vs_5_0 ps_5_0)",
|
||||
],
|
||||
["Mesa offscreen", "Google Inc. (Mesa)", "ANGLE (Mesa, Mesa offscreen, OpenGL ES 3.2)"],
|
||||
[
|
||||
"lavapipe",
|
||||
"Google Inc. (Mesa)",
|
||||
"ANGLE (Mesa, llvmpipe/lavapipe Vulkan software rasterizer)",
|
||||
],
|
||||
])("treats %s WebGL as software in auto mode", async (_label, vendor, renderer) => {
|
||||
const { close, launch } = setMockWebGlProbe({
|
||||
hasWebGL: true,
|
||||
vendor,
|
||||
renderer,
|
||||
});
|
||||
|
||||
const mode = await resolveBrowserGpuMode("auto", {
|
||||
chromePath: "/mock/chrome-headless-shell",
|
||||
browserTimeout: 2000,
|
||||
});
|
||||
|
||||
expect(mode).toBe("software");
|
||||
expect(launch).toHaveBeenCalledTimes(1);
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("treats empty WebGL renderer metadata as software in auto mode", async () => {
|
||||
const { close, launch } = setMockWebGlProbe({
|
||||
hasWebGL: true,
|
||||
vendor: "",
|
||||
renderer: "",
|
||||
});
|
||||
|
||||
const mode = await resolveBrowserGpuMode("auto", {
|
||||
chromePath: "/mock/chrome-headless-shell",
|
||||
browserTimeout: 2000,
|
||||
});
|
||||
|
||||
expect(mode).toBe("software");
|
||||
expect(launch).toHaveBeenCalledTimes(1);
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps real hardware WebGL as hardware in auto mode", async () => {
|
||||
const { close, launch } = setMockWebGlProbe({
|
||||
hasWebGL: true,
|
||||
vendor: "Google Inc. (NVIDIA Corporation)",
|
||||
renderer: "ANGLE (NVIDIA, NVIDIA A10G, OpenGL 4.6)",
|
||||
});
|
||||
|
||||
const mode = await resolveBrowserGpuMode("auto", {
|
||||
chromePath: "/mock/chrome-headless-shell",
|
||||
browserTimeout: 2000,
|
||||
});
|
||||
|
||||
expect(mode).toBe("hardware");
|
||||
expect(launch).toHaveBeenCalledTimes(1);
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveHeadlessShellPath", () => {
|
||||
|
||||
@@ -15,6 +15,25 @@ import { getSystemTotalMb, LOW_MEMORY_TOTAL_MB_THRESHOLD } from "./systemMemory.
|
||||
|
||||
let _puppeteer: PuppeteerNode | undefined;
|
||||
|
||||
interface WebGlProbeInfo {
|
||||
hasWebGL: boolean;
|
||||
vendor: string;
|
||||
renderer: string;
|
||||
}
|
||||
|
||||
function isSoftwareWebGlRenderer(rendererInfo: string): boolean {
|
||||
const renderer = rendererInfo.trim().toLowerCase();
|
||||
return (
|
||||
renderer.includes("swiftshader") ||
|
||||
renderer.includes("llvmpipe") ||
|
||||
renderer.includes("lavapipe") ||
|
||||
renderer.includes("softpipe") ||
|
||||
renderer.includes("mesa offscreen") ||
|
||||
renderer.includes("microsoft basic render driver") ||
|
||||
renderer.includes("software rasterizer")
|
||||
);
|
||||
}
|
||||
|
||||
async function getPuppeteer(): Promise<PuppeteerNode> {
|
||||
if (_puppeteer) return _puppeteer;
|
||||
try {
|
||||
@@ -28,6 +47,55 @@ async function getPuppeteer(): Promise<PuppeteerNode> {
|
||||
return _puppeteer;
|
||||
}
|
||||
|
||||
async function probeHardwareWebGlInfo(
|
||||
ppt: PuppeteerNode,
|
||||
options: {
|
||||
args: string[];
|
||||
browserTimeout: number;
|
||||
executablePath: string | undefined;
|
||||
},
|
||||
): Promise<WebGlProbeInfo> {
|
||||
let probeBrowser: Browser | undefined;
|
||||
try {
|
||||
probeBrowser = await ppt.launch({
|
||||
headless: true,
|
||||
args: options.args,
|
||||
defaultViewport: { width: 64, height: 64 },
|
||||
executablePath: options.executablePath,
|
||||
timeout: options.browserTimeout,
|
||||
});
|
||||
const page = await probeBrowser.newPage();
|
||||
return await page.evaluate(() => {
|
||||
const unavailable = { hasWebGL: false, vendor: "", renderer: "" };
|
||||
const c = document.createElement("canvas");
|
||||
let gl = c.getContext("webgl") as WebGLRenderingContext | null;
|
||||
if (gl === null) {
|
||||
gl = c.getContext("experimental-webgl") as WebGLRenderingContext | null;
|
||||
}
|
||||
if (gl === null) return unavailable;
|
||||
const ext = gl.getExtension("WEBGL_debug_renderer_info") as {
|
||||
UNMASKED_VENDOR_WEBGL: number;
|
||||
UNMASKED_RENDERER_WEBGL: number;
|
||||
} | null;
|
||||
let vendorParam: number = gl.VENDOR;
|
||||
let rendererParam: number = gl.RENDERER;
|
||||
if (ext !== null) {
|
||||
vendorParam = ext.UNMASKED_VENDOR_WEBGL;
|
||||
rendererParam = ext.UNMASKED_RENDERER_WEBGL;
|
||||
}
|
||||
const vendor = gl.getParameter(vendorParam);
|
||||
const renderer = gl.getParameter(rendererParam);
|
||||
return {
|
||||
hasWebGL: true,
|
||||
vendor: vendor == null ? "" : String(vendor),
|
||||
renderer: renderer == null ? "" : String(renderer),
|
||||
};
|
||||
});
|
||||
} finally {
|
||||
await probeBrowser?.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// "beginframe" = atomic compositor control via HeadlessExperimental.beginFrame (Linux only)
|
||||
// "screenshot" = renderSeek + Page.captureScreenshot (all platforms)
|
||||
export type CaptureMode = "beginframe" | "screenshot";
|
||||
@@ -165,6 +233,70 @@ export function _resetAutoBrowserGpuModeCacheForTests(): void {
|
||||
_autoBrowserGpuModeCache = undefined;
|
||||
}
|
||||
|
||||
async function getPuppeteerOrNull(): Promise<PuppeteerNode | null> {
|
||||
try {
|
||||
return await getPuppeteer();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getHardwareGpuProbeArgs(platform: NodeJS.Platform): string[] {
|
||||
return [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--enable-webgl",
|
||||
"--ignore-gpu-blocklist",
|
||||
...getBrowserGpuArgs("hardware", platform),
|
||||
];
|
||||
}
|
||||
|
||||
function resolveWebGlProbeMode(info: WebGlProbeInfo): "software" | "hardware" {
|
||||
if (!info.hasWebGL) return "software";
|
||||
if (!info.vendor.trim() && !info.renderer.trim()) return "software";
|
||||
return isSoftwareWebGlRenderer(info.renderer) ? "software" : "hardware";
|
||||
}
|
||||
|
||||
function describeWebGlProbe(info: WebGlProbeInfo): string {
|
||||
if (!info.hasWebGL) return "WebGL unavailable";
|
||||
return `WebGL renderer vendor=${JSON.stringify(info.vendor)} renderer=${JSON.stringify(info.renderer)}`;
|
||||
}
|
||||
|
||||
function formatProbeFailure(err: unknown): string {
|
||||
return `probe failed (${err instanceof Error ? err.message : String(err)})`;
|
||||
}
|
||||
|
||||
async function probeAutoBrowserGpuMode(options: {
|
||||
chromePath?: string;
|
||||
browserTimeout?: number;
|
||||
platform?: NodeJS.Platform;
|
||||
}): Promise<"software" | "hardware"> {
|
||||
const platform = options.platform ?? process.platform;
|
||||
const browserTimeout = options.browserTimeout ?? DEFAULT_CONFIG.browserTimeout;
|
||||
const executablePath = options.chromePath ?? resolveHeadlessShellPath({});
|
||||
const ppt = await getPuppeteerOrNull();
|
||||
|
||||
if (ppt === null) {
|
||||
logResolvedBrowserGpuMode("software", "puppeteer unavailable");
|
||||
return "software";
|
||||
}
|
||||
|
||||
try {
|
||||
const info = await probeHardwareWebGlInfo(ppt, {
|
||||
args: getHardwareGpuProbeArgs(platform),
|
||||
browserTimeout,
|
||||
executablePath,
|
||||
});
|
||||
const resolved = resolveWebGlProbeMode(info);
|
||||
logResolvedBrowserGpuMode(resolved, describeWebGlProbe(info));
|
||||
return resolved;
|
||||
} catch (err) {
|
||||
logResolvedBrowserGpuMode("software", formatProbeFailure(err));
|
||||
return "software";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve `browserGpuMode` to a concrete `"software" | "hardware"` answer.
|
||||
*
|
||||
@@ -191,61 +323,7 @@ export function resolveBrowserGpuMode(
|
||||
if (mode !== "auto") return Promise.resolve(mode);
|
||||
if (_autoBrowserGpuModeCache) return _autoBrowserGpuModeCache;
|
||||
|
||||
_autoBrowserGpuModeCache = (async () => {
|
||||
const platform = options.platform ?? process.platform;
|
||||
const browserTimeout = options.browserTimeout ?? DEFAULT_CONFIG.browserTimeout;
|
||||
const executablePath = options.chromePath ?? resolveHeadlessShellPath({});
|
||||
|
||||
const probeArgs = [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--enable-webgl",
|
||||
"--ignore-gpu-blocklist",
|
||||
...getBrowserGpuArgs("hardware", platform),
|
||||
];
|
||||
|
||||
const ppt = await getPuppeteer().catch(() => null);
|
||||
if (!ppt) {
|
||||
logResolvedBrowserGpuMode("software", "puppeteer unavailable");
|
||||
return "software" as const;
|
||||
}
|
||||
|
||||
let probeBrowser: Browser | undefined;
|
||||
try {
|
||||
probeBrowser = await ppt.launch({
|
||||
headless: true,
|
||||
args: probeArgs,
|
||||
defaultViewport: { width: 64, height: 64 },
|
||||
executablePath,
|
||||
timeout: browserTimeout,
|
||||
});
|
||||
const page = await probeBrowser.newPage();
|
||||
const hasWebGL = await page.evaluate(() => {
|
||||
try {
|
||||
const c = document.createElement("canvas");
|
||||
const gl =
|
||||
c.getContext("webgl") ||
|
||||
(c.getContext("experimental-webgl") as RenderingContext | null);
|
||||
return gl !== null;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const resolved = hasWebGL ? ("hardware" as const) : ("software" as const);
|
||||
logResolvedBrowserGpuMode(resolved, hasWebGL ? "WebGL probe succeeded" : "WebGL unavailable");
|
||||
return resolved;
|
||||
} catch (err) {
|
||||
logResolvedBrowserGpuMode(
|
||||
"software",
|
||||
`probe failed (${err instanceof Error ? err.message : String(err)})`,
|
||||
);
|
||||
return "software" as const;
|
||||
} finally {
|
||||
await probeBrowser?.close().catch(() => {});
|
||||
}
|
||||
})();
|
||||
|
||||
_autoBrowserGpuModeCache = probeAutoBrowserGpuMode(options);
|
||||
return _autoBrowserGpuModeCache;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user