1
0

The capture harness uses the graphics card that was there all along

`launch()` asks for `--use-angle=vulkan` and gets the Radeon RX 6700 XT. On the
real film loop at 1440x900 that is 7.56 s a frame to 0.47, and about twelve
cores pegged down to less than one. A 180-frame film goes from twenty-two
minutes to ninety seconds; the three shipped reels go from over an hour to four
minutes.

The SwiftShader flag was inherited from the share-card script and its comment
said "a box with no display and no GPU". Half of that was never true. The card
has been in this machine the whole time, with the amdgpu driver and a working
RADV ICD, and Chrome reaches it with **no display server at all** — no DISPLAY,
no XDG_RUNTIME_DIR, nothing but read access to /dev/dri/renderD128, which group
`render` already grants. So the portability the flag was protecting is intact:
this still runs over ssh and under cron.

Naming the backend is the whole trick, and most of the plausible spellings are
traps. `--use-angle=gl`, `--use-gl=egl`, `--use-gl=desktop`,
`--enable-features=Vulkan` and passing no GL flags at all were each measured on
this box, and all five land on SwiftShader while reporting success. Only
`--use-angle=vulkan` and `--use-angle=gl-egl` reach the card.

So `launch()` verifies instead of assuming. `--use-angle=vulkan` is a demand,
not a preference: with the driver hidden it yields no WebGL context at all
rather than falling back, the boot curtain never lifts, and the caller sits in
`waitForFunction` until a three-minute timeout before failing with something
that looks nothing like "there is no GPU here". Reading
`UNMASKED_RENDERER_WEBGL` once per run costs about half a second and turns that
into a printed line and a slow, correct render. Both branches are tested; the
sabotaged one was verified with `VK_LOADER_DRIVERS_DISABLE`.

The pictures are the same pictures: mean absolute difference of half a level out
of 255, confined to MSAA edges and to cloud shadow that already drifts between
any two runs of the same renderer.

Also corrects every comment across the four scripts that asserted three frames a
second or twenty-two minutes a reel. Those were true when written and are not
now, and the `reducedMotion` rationale in two of them was resting on the number:
the real reason a chapter flight cannot be waited out is that it is ~40 frames
however fast they are drawn, which was twenty seconds under software GL and is a
shorter race on the GPU — still a race.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-06 19:38:14 -07:00
parent 68a5165d6f
commit 0cc2126e85
4 changed files with 112 additions and 41 deletions
+82 -11
View File
@@ -4,11 +4,11 @@
*
* This file exists because the second consumer arrived. `capture.mjs` shot the
* app to make two share cards; `shots.mjs` shoots it to make the product
* imagery on lumbridgecorp.com. Everything below — the static server, the
* SwiftShader flags, the two-hostnames-one-dist trick, the clock shim — was
* already load-bearing for the first one, and copying it would have meant two
* capture pipelines drifting apart while both claimed to photograph the same
* app. There is one pipeline. The two scripts differ only in what they frame.
* imagery on lumbridgecorp.com. Everything below — the static server, the GL
* flags, the two-hostnames-one-dist trick, the clock shim — was already
* load-bearing for the first one, and copying it would have meant two capture
* pipelines drifting apart while both claimed to photograph the same app.
* There is one pipeline. The two scripts differ only in what they frame.
*/
import { chromium } from "playwright";
@@ -49,19 +49,90 @@ export function serve(dir, port, { spa = false } = {}) {
return new Promise((resolve) => server.listen(port, "127.0.0.1", () => resolve(server)));
}
export const CHROME_ARGS = [
const COMMON_ARGS = [
"--no-sandbox",
"--disable-dev-shm-usage",
// Software GL, so this runs on a box with no display and no GPU.
"--use-gl=angle",
"--use-angle=swiftshader",
// The app reads its own hostname to decide which door it is. Resolving both
// names at the local server is what makes one `dist/` produce both shots.
"--host-resolver-rules=MAP office.lumbridgecorp.com 127.0.0.1, MAP tera.lumbridgecorp.com 127.0.0.1",
];
export function launch() {
return chromium.launch({ channel: "chrome", args: CHROME_ARGS });
/**
* The GPU is real, and headless Chrome will use it if you name the backend
* precisely.
*
* This box has a Radeon RX 6700 XT with the amdgpu driver and a working RADV
* ICD, and Chrome reaches it with **no display server at all** — no `DISPLAY`,
* no `XDG_RUNTIME_DIR`, nothing but read access to `/dev/dri/renderD128`, which
* membership of the `render` group already grants. So this still runs under
* cron and over ssh, which is what the software-GL flag was protecting.
*
* It is worth naming the backend exactly, because most of the plausible spellings
* silently give you SwiftShader instead and report success: `--use-angle=gl`,
* `--use-gl=egl`, `--use-gl=desktop`, `--enable-features=Vulkan` and passing no
* GL flags at all were all measured on this box, and all four land on
* `SwiftShader Device (Subzero)`. Only `--use-angle=vulkan` and
* `--use-angle=gl-egl` reach the card. Nothing else is required — the
* `--ozone-platform`, `--enable-features=Vulkan` and `--disable-software-rasterizer`
* flags that usually accompany this change measured as no-ops.
*
* What it buys, on the real film loop at 1440x900: 7.56 s/frame to 0.47, and
* about twelve cores pegged to less than one. A 180-frame film goes from
* twenty-two minutes to eighty seconds. The pictures are the same pictures —
* mean absolute difference half a level out of 255, confined to MSAA edges and
* to cloud shadow that drifts between any two runs anyway.
*/
export const GPU_ARGS = [...COMMON_ARGS, "--use-gl=angle", "--use-angle=vulkan"];
/** Where `launch()` goes when there is no usable card. See `launch()`. */
export const SOFTWARE_ARGS = [...COMMON_ARGS, "--use-gl=angle", "--use-angle=swiftshader"];
/** The renderer string a real WebGL context reports, or `null` if it has none. */
async function rendererOf(browser) {
const page = await browser.newPage();
try {
await page.goto("about:blank");
return await page.evaluate(() => {
const gl = document.createElement("canvas").getContext("webgl2");
const ext = gl && gl.getExtension("WEBGL_debug_renderer_info");
return ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : null;
});
} catch {
return null;
} finally {
await page.close();
}
}
/**
* Chrome with the GPU, having checked that it actually got one.
*
* The check is not ceremony. `--use-angle=vulkan` is a demand, not a
* preference: on a box where the driver is missing it does not quietly fall
* back to software, it produces **no WebGL context at all**. The app's boot
* curtain then never lifts, and the caller sits in `waitForFunction` until its
* three-minute timeout before failing with something that looks nothing like
* "there is no GPU here". Asking the context what it is costs half a second
* once per run and turns that into a line of output and a slow, correct render.
*
* `SwiftShader` and `llvmpipe` both count as failure — they are what a silent
* fallback looks like, and a run that thinks it is on the GPU while taking
* twenty minutes a film is the confusion this whole check exists to prevent.
*/
export async function launch({ gpu = true } = {}) {
if (gpu) {
const browser = await chromium.launch({ channel: "chrome", args: GPU_ARGS });
const renderer = await rendererOf(browser);
if (renderer && !/SwiftShader|llvmpipe/i.test(renderer)) {
console.log(` GPU: ${renderer}`);
return browser;
}
console.log(` no GPU (${renderer ?? "no WebGL context"}) — falling back to SwiftShader, which is slow.`);
await browser.close();
}
const browser = await chromium.launch({ channel: "chrome", args: SOFTWARE_ARGS });
console.log(` GPU: ${await rendererOf(browser)}`);
return browser;
}
/**