From 8deb871ccad7a1f095ff87fa53fcf33b5887b2b5 Mon Sep 17 00:00:00 2001 From: Diogo Chaves Date: Tue, 20 Oct 2026 12:00:55 -0300 Subject: [PATCH 1/2] fix(engine): software renders reach SwiftShader when VK_DRIVER_FILES pins a GPU ANGLE (and Dawn) load SwiftShader by pointing the Vulkan loader at its ICD through VK_ICD_FILENAMES. The loader ignores VK_ICD_FILENAMES while VK_DRIVER_FILES is set, and still applies VK_LOADER_DRIVERS_SELECT/DISABLE, so on a machine that pins its Vulkan driver that way a --no-browser-gpu render gets ANGLE on the real GPU and assertSwiftShader refuses it. Launch SwiftShader browsers without those three variables. Hardware launches, and environments that set none of them, are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../services/browserManager-vulkanEnv.test.ts | 73 +++++++++++++++++++ .../engine/src/services/browserManager.ts | 22 +++++- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 packages/engine/src/services/browserManager-vulkanEnv.test.ts 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 { From a1963ca03584f49e65825023fc6d2debf578b845 Mon Sep 17 00:00:00 2001 From: Diogo Chaves Date: Sat, 24 Oct 2026 13:28:25 -0300 Subject: [PATCH 2/2] chore(engine): raise the packed size budget by the 342 bytes launchEnv adds The increase sits on top of main's engine budget, 456055 after #6194. Main packs the engine at 435883 bytes and this branch at 436205. Co-Authored-By: Claude Opus 5.5 --- scripts/package-size-budgets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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": {