Run CI on a WSL2 runner with the RTX 4070 (#283)
game-maker / gate (push) Successful in 3m18s
game-maker / suite (push) Failing after 14m15s

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>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-29 20:56:50 +02:00
1 parent 9b5eeaab57
commit 857e23c69b
4 files changed
+125 -21

No files matched your search

+88 -12
View File
@@ -1,16 +1,9 @@
import { spawn } from 'node:child_process';
const FLAGS = [
'--headless',
const COMMON = [
'--hide-scrollbars',
/**
* 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',
'--ignore-gpu-blocklist',
'--enable-gpu',
'--enable-features=Vulkan',
// None of this serves a headless check, and its stderr was full of GCM sign-in retries.
'--disable-background-networking',
'--disable-sync',
@@ -23,19 +16,102 @@ const FLAGS = [
'--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 function launchBrowser({ port, width, height }) {
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.
[
...FLAGS,
...COMMON,
...(wsl ? WSL : HEADLESS),
'--no-sandbox',
`--remote-debugging-port=${port}`,
`--window-size=${width},${height}`,
'about:blank',
],
{ stdio: ['ignore', 'ignore', 'pipe'] },
{ stdio: ['ignore', 'ignore', 'pipe'], env: display ? display.env : process.env },
);
return { proc, stop: () => proc.kill('SIGTERM'), where: 'local chromium' };
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 };
}