diff --git a/packages/engine/src/services/browserManager-vulkanEnv.test.ts b/packages/engine/src/services/browserManager-vulkanEnv.test.ts new file mode 100644 index 00000000000..5f68d6e1b1f --- /dev/null +++ b/packages/engine/src/services/browserManager-vulkanEnv.test.ts @@ -0,0 +1,73 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { LaunchOptions, PuppeteerNode } from "puppeteer-core"; +import { + _resetBrowserPoolForTests, + _setPuppeteerForTests, + acquireBrowser, + buildChromeArgs, + drainBrowserPool, +} from "./browserManager.js"; + +const PINS = { + VK_DRIVER_FILES: "/usr/share/vulkan/icd.d/radeon_icd.json", + VK_LOADER_DRIVERS_SELECT: "*radeon*", + VK_LOADER_DRIVERS_DISABLE: "*nvidia*", +}; + +function fakeChrome() { + const launch = vi.fn(async (_options: LaunchOptions) => ({ + connected: true, + version: vi.fn().mockResolvedValue("HeadlessChrome/155.0.8059.84"), + close: vi.fn().mockResolvedValue(undefined), + disconnect: vi.fn(), + process: () => ({ kill: vi.fn(), killed: false }), + })); + _setPuppeteerForTests({ launch } as unknown as PuppeteerNode); + return launch; +} + +async function launchEnvFor(browserGpuMode: "software" | "hardware") { + const launch = fakeChrome(); + const args = buildChromeArgs({ width: 64, height: 64, platform: "linux" }, { browserGpuMode }); + const lease = await acquireBrowser(args, { forceScreenshot: true, enableBrowserPool: false }); + await lease.release(); + return launch.mock.calls[0]?.[0].env; +} + +// A session that pins Vulkan to a GPU (VK_DRIVER_FILES) made --no-browser-gpu renders run ANGLE +// on that GPU, and assertSwiftShader refused them. +describe("SwiftShader launch environment", () => { + const saved = Object.fromEntries(Object.keys(PINS).map((name) => [name, process.env[name]])); + + beforeEach(() => { + vi.spyOn(console, "log").mockImplementation(() => {}); + }); + + afterEach(async () => { + await drainBrowserPool(); + _resetBrowserPoolForTests(); + _setPuppeteerForTests(undefined); + for (const [name, value] of Object.entries(saved)) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + vi.restoreAllMocks(); + }); + + it("drops the Vulkan driver pins for a software render and keeps the rest", async () => { + Object.assign(process.env, PINS); + const env = await launchEnvFor("software"); + expect(env).toMatchObject({ PATH: process.env.PATH }); + for (const name of Object.keys(PINS)) expect(env).not.toHaveProperty(name); + }); + + it("keeps the pins for a hardware render", async () => { + Object.assign(process.env, PINS); + expect(await launchEnvFor("hardware")).toBeUndefined(); + }); + + it("inherits the environment when nothing pins a Vulkan driver", async () => { + for (const name of Object.keys(PINS)) delete process.env[name]; + expect(await launchEnvFor("software")).toBeUndefined(); + }); +}); diff --git a/packages/engine/src/services/browserManager.ts b/packages/engine/src/services/browserManager.ts index 71530f76dab..7d2a7a32aea 100644 --- a/packages/engine/src/services/browserManager.ts +++ b/packages/engine/src/services/browserManager.ts @@ -643,6 +643,26 @@ function envWithoutWaylandDisplay(): NodeJS.ProcessEnv { return env; } +// ANGLE and Dawn load SwiftShader via VK_ICD_FILENAMES, which these override or filter: +// https://github.com/KhronosGroup/Vulkan-Loader/blob/main/docs/LoaderDriverInterface.md +const VULKAN_VARS_THAT_OVERRIDE_SWIFTSHADER = [ + "VK_DRIVER_FILES", + "VK_LOADER_DRIVERS_SELECT", + "VK_LOADER_DRIVERS_DISABLE", +]; + +function launchEnv(fingerprint: Readonly): NodeJS.ProcessEnv | undefined { + if (fingerprint.withoutWaylandDisplay) return envWithoutWaylandDisplay(); + const swiftShader = fingerprint.args.some( + (arg) => arg === "--use-angle=swiftshader" || arg === "--use-vulkan=swiftshader", + ); + const overriding = VULKAN_VARS_THAT_OVERRIDE_SWIFTSHADER.filter((name) => name in process.env); + if (!swiftShader || overriding.length === 0) return undefined; + const env = { ...process.env }; + for (const name of overriding) delete env[name]; + return env; +} + /** True when a `resolvedMode` render must launch without WAYLAND_DISPLAY to reach the GPU. */ export async function captureWithoutWaylandDisplay( resolvedMode: "software" | "hardware", @@ -847,7 +867,7 @@ async function launchBrowser( fingerprint: Readonly, ): Promise<{ browser: Browser; captureMode: CaptureMode }> { const ppt = await getPuppeteer(); - const env = fingerprint.withoutWaylandDisplay ? envWithoutWaylandDisplay() : undefined; + const env = launchEnv(fingerprint); let captureMode = fingerprint.requestedCaptureMode; let browser: Browser | undefined; try { diff --git a/scripts/package-size-budgets.json b/scripts/package-size-budgets.json index 30c1e6278d8..f0e0f56df8f 100644 --- a/scripts/package-size-budgets.json +++ b/scripts/package-size-budgets.json @@ -12,7 +12,7 @@ "unpacked": 3965000 }, "@hyperframes/engine": { - "packed": 456055, + "packed": 456397, "unpacked": 1526873 }, "@hyperframes/gcp-cloud-run": {