Files
adminandClaude Opus 5.5 6097307b6d
feuerwerk / gate (push) Successful in 4m30s
feuerwerk / suite (push) Successful in 18m38s
Move repo, CI and the video worker over to the name Feuerwerk (#422)
Stage 3 of the rename. The repo now lives at meleki/feuerwerk, the
runners carry feuerwerk labels and the WSL distro on melektaus-atx is
called feuerwerk-ci. The code has to follow, or CI finds no runner and
the suite no longer picks the GPU path.

- ci.yml: workflow name feuerwerk, jobs on feuerwerk-ci (gate, stein2)
  and feuerwerk-wsl (suite, melektaus-atx).
- GAME_MAKER_* variables are now FEUERWERK_*. This includes
  FEUERWERK_BROWSER=wsl, which the runner config on melektaus-atx sets.
- The video worker runs as container feuerwerk-video with the image
  feuerwerk-video and works on /srv/feuerwerk.
- e2e.sh starts the unit feuerwerk-e2e, the CI image keeps its paths
  under the new name.
- README and repo links point to git.nockal.com/meleki/feuerwerk. The
  old URLs still redirect with 301.

Both runners keep their old labels until the first CI run with this
ci.yml is through. Then they go, together with the image tag
game-maker-ci:latest and GAME_MAKER_BROWSER in the atx config.

Tests:
- Gate on melektaus-atx through the renamed distro: CHECK EXIT 0,
  307 files, 4756 tests.
- e2e 60 on melektaus-atx: ALL PASSED, 20 checks. Renderer is the
  RTX 4070 through D3D12, so FEUERWERK_BROWSER reaches the GPU path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:42:52 +02:00

121 lines
4.4 KiB
JavaScript
Raw Permalink 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',
// A take of the Song tab (#342) records chromium's test tone, without asking for leave.
'--use-fake-device-for-media-stream',
'--use-fake-ui-for-media-stream',
];
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',
];
/**
* FEUERWERK_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.FEUERWERK_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 };
}