Files
feuerwerk/tools/lib/browser.mjs
T
adminandClaude Opus 5.5 857e23c69b
game-maker / gate (push) Successful in 3m18s
game-maker / suite (push) Failing after 14m15s
Run CI on a WSL2 runner with the RTX 4070 (#283)
stein2 should stop carrying the full e2e run. The new runner lives in
the WSL2 distro game-maker-ci on melektaus-atx and uses the same image
and fonts. Its GPU sits behind /dev/dxg and Mesa's d3d12 driver, which
headless chromium cannot reach, so the harness gets a second way to
start the browser.

- GAME_MAKER_BROWSER=wsl starts an invisible Xvfb and an X11 chromium
  on ANGLE/GL. Without the variable, stein2 keeps the headless Vulkan
  launch exactly as before.
- The harness logs the renderer and stops on software rendering.
  Under WSL a missing d3d12 driver falls back to llvmpipe, which still
  offers WebGL 2 and would pass unnoticed.
- The CI image gains xvfb. The font set is unchanged: fc-list gives
  the same 149 fonts with the same hash.
- ci.yml runs both jobs on the new label game-maker-wsl. The stein2
  runner keeps game-maker-ci, so moving back is one line per job.

Findings:
- Wayland on weston reached the GPU too, but drew at about 40 fps and
  let the editor clock stall in long runs (GM_STEPS=1-17: 2 failed).
- Vulkan through a self-built dzn caps ANGLE at ES 2.0 because
  VK_EXT_provoking_vertex is missing, so chromium drops the GPU.
- The flaky check in e2e 41 (#282) fails more often under WSL: two of
  three solo runs were red.

Tests:
- ./check.sh: 195 files, 2823 tests passed
- WSL container, GM_STEPS=1-17: ALL PASSED, 553 checks
- WSL container, full run: 1589 of 1590, the one FAIL is #282
- WSL container, smokes: 24 / 19 / 21 checks
- stein2 headless, e2e 39+40: ALL PASSED, 100 checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:56:50 +02:00

118 lines
4.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };
}