import { spawn } from 'node:child_process'; const COMMON = [ '--hide-scrollbars', '--ignore-gpu-blocklist', '--enable-gpu', // None of this serves a headless check, and its stderr was full of GCM sign-in retries. '--disable-background-networking', '--disable-sync', '--disable-extensions', '--no-first-run', // Nothing is meant to be heard here – but the sounds still have to *run*, or the sound checks // would be measuring a browser that refuses to start a voice rather than an editor that never // asked for one. Muted plus autoplay-allowed is exactly that: it plays, silently. '--mute-audio', '--autoplay-policy=no-user-gesture-required', ]; const HEADLESS = [ '--headless', /** * Measured (idle editor, 20 s): software WebGL 373% of a core at 8 fps, HD 630 via ANGLE/Vulkan 69% at * 56 fps; no software fallback on purpose. `--use-angle=gl` and `--use-gl=egl` get no WebGL context at all. */ '--use-angle=vulkan', '--enable-features=Vulkan', ]; /** * GAME_MAKER_BROWSER=wsl, for a runner inside WSL2 (#283). There the GPU is /dev/dxg behind Mesa's * d3d12 driver: no DRM render node, no Vulkan. Headless chromium finds no surface and gets no WebGL, * an X11 chromium on ANGLE/GL does. So this mode brings its own invisible X server. * * Measured on an RTX 4070: WebGL 2.0 on "D3D12 (NVIDIA …), OpenGL 4.2", 60 frames a second while * drawing. A Wayland chromium on weston reached the GPU too, but drew at ~40 and let the editor's * clock stall for good in a long run – its frames hang on the compositor's callbacks. * A real window can be backgrounded, headless never is, hence the three flags that keep it drawing. */ const WSL = [ '--ozone-platform=x11', '--use-angle=gl', '--disable-renderer-backgrounding', '--disable-backgrounding-occluded-windows', '--disable-background-timer-throttling', ]; /** Starts chromium with the CDP port open; `proc.stderr` carries its last words. */ export async function launchBrowser({ port, width, height }) { const wsl = process.env.GAME_MAKER_BROWSER === 'wsl'; const display = wsl ? await startXvfb({ width, height }) : null; const proc = spawn( 'chromium', // --no-sandbox: this container runs as root, where chromium refuses to start otherwise. [ ...COMMON, ...(wsl ? WSL : HEADLESS), '--no-sandbox', `--remote-debugging-port=${port}`, `--window-size=${width},${height}`, 'about:blank', ], { stdio: ['ignore', 'ignore', 'pipe'], env: display ? display.env : process.env }, ); if (display) proc.once('exit', display.stop); return { proc, stop: () => proc.kill('SIGTERM'), where: wsl ? `local chromium on Xvfb ${display.env.DISPLAY} (WSL, d3d12)` : 'local chromium', }; } /** * An X server that shows nothing. It only has to hold the window: the drawing happens in Mesa's * d3d12 driver, which hands finished frames over by copy. `-displayfd` lets it pick a free display * and report it, so two runs never share one. */ async function startXvfb({ width, height }) { const xvfb = spawn( 'Xvfb', ['-displayfd', '3', '-screen', '0', `${width}x${height}x24`, '-nolisten', 'tcp'], { stdio: ['ignore', 'ignore', 'pipe', 'pipe'], }, ); const log = []; xvfb.stderr.on('data', (b) => log.push(String(b))); const stop = () => xvfb.kill('SIGTERM'); const number = await new Promise((resolve) => { let got = ''; const timer = setTimeout(() => resolve(null), 10_000); xvfb.stdio[3].on('data', (b) => { got += String(b); if (got.includes('\n')) { clearTimeout(timer); resolve(got.trim()); } }); xvfb.once('exit', () => { clearTimeout(timer); resolve(null); }); }); if (!number) { stop(); throw new Error(`Xvfb never reported a display:\n${log.join('').trim().slice(-2000)}`); } const env = { ...process.env, DISPLAY: `:${number}`, // Without both, Mesa picks llvmpipe: the d3d12 driver is not its default, and it needs the // Windows side's libd3d12/libdxcore, which WSL mounts there. GALLIUM_DRIVER: 'd3d12', LD_LIBRARY_PATH: ['/usr/lib/wsl/lib', process.env.LD_LIBRARY_PATH].filter(Boolean).join(':'), }; delete env.WAYLAND_DISPLAY; return { env, stop }; }