Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions packages/engine/src/services/browserManager-vulkanEnv.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
22 changes: 21 additions & 1 deletion packages/engine/src/services/browserManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<BrowserLaunchFingerprint>): 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",
Expand Down Expand Up @@ -847,7 +867,7 @@ async function launchBrowser(
fingerprint: Readonly<BrowserLaunchFingerprint>,
): 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 {
Expand Down
2 changes: 1 addition & 1 deletion scripts/package-size-budgets.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"unpacked": 3965000
},
"@hyperframes/engine": {
"packed": 456055,
"packed": 456397,
"unpacked": 1526873
},
"@hyperframes/gcp-cloud-run": {
Expand Down
Loading