/** * Which GL backend the CLI's headless Chrome renders the game with. One place, because `verify` * and `forge` want exactly the same answer and got it wrong in opposite directions before. * * The thing to know: **headless Chrome does not use the machine's GPU unless it is told to.** * On Linux the GPU process starts with no display and no ANGLE backend it trusts, so it falls * back to SwiftShader — Chrome's software rasteriser — and every WebGL2 draw runs on the CPU. * Nothing announces this. `WEBGL_debug_renderer_info` reports * `ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero)), SwiftShader driver)` on a machine * whose real GPU is idle two feet away, and a fill-heavy frame that takes 0.2ms on the GPU takes * ~70ms in software. That is the whole reason a verify run of a healthy game feels slow. * * `--use-angle=vulkan` is what fixes it, and the choice of Vulkan over `--use-angle=gl` is * deliberate: the GL backend needs an X display, so it works from a developer's terminal and * silently drops back to SwiftShader from anything without `DISPLAY` (a systemd service, a * detached agent run, an ssh session). The Vulkan backend needs no display at all and reaches the * discrete GPU either way, so the result does not depend on how the CLI was invoked. * * `--disable-gpu-compositing` is load-bearing and must not be dropped as "an optimisation we do * not need". Headless Chrome cannot composite a GPU-backed WebGL canvas: with GPU compositing on, * ANGLE's Vulkan backend LOSES the context about 400ms into the page (measured: `webglcontextlost` * at 373-420ms, in both `launch` and `launchPersistentContext`), and everything after that draws * into nothing. It is nearly invisible — `requestAnimationFrame` keeps firing, the engine logs its * usual start lines, the DOM HUD still paints, and the only symptom is that every screenshot comes * back black. `bitmagic verify` reported PASSED like that, and since a project with no cover art * publishes its verify screenshot as the game's thumbnail, a black frame would have shipped with * nothing to flag it. Turning compositing off makes the compositor read the canvas back on the CPU * — the GPU still does all the rendering, and the fill-rate win is intact (measured 0.29ms/frame * against 97.8ms in software). * * `--enable-unsafe-swiftshader` stays on in BOTH modes. It does not force software rendering * (`--use-angle=swiftshader` does that); it only permits the fallback, which Chrome otherwise * deprecation-warns about and is on a path to refusing outright. Keeping it means a machine with * no usable GPU still renders — slowly, but it renders — instead of failing verify with a dead * WebGL context that looks like a broken game. */ /** `process.platform`, named so callers can pass one in without pulling in the `NodeJS` global. */ export type Platform = typeof process.platform; /** The renderer the game is asked to boot with (`?renderer=` — RendererPreference.ts in the * engine). Distinct from what actually drives the canvas: a WebGPURenderer whose adapter never * came up runs its WebGL2 fallback backend, which is exactly what verify now detects. */ export type RendererChoice = 'webgpu' | 'webgl'; /** * Chrome flags for the GL backend, given the environment and platform. * * Software GL is forced wherever `usesSoftwareGl` holds. Everywhere else the real GPU is * requested. The Vulkan flags are Linux-only because they are only a fix there: macOS headless * already reaches the GPU through ANGLE's Metal backend, and asking it for Vulkan would take that * away. */ export declare function browserGlArgs(env: Record, platform?: Platform): string[]; /** * Chrome flags for booting the game on a specific renderer, given the environment and platform. * * For WebGL this is exactly `browserGlArgs`. For WebGPU it additionally permits the API in * headless Chrome, where `navigator.gpu` otherwise exists but never yields an adapter — the game * then silently runs three's WebGL2 fallback backend, which is precisely the undetected state * verify exists to catch. */ export declare function browserRendererArgs(env: Record, renderer: RendererChoice, platform?: Platform): string[]; /** * How to launch Chrome for a renderer: headless or headed, and with which args. * * Headed exists for exactly one case — **WebGPU on Linux**, where headless Chrome cannot produce * a real-GPU frame that a screenshot can see, in either compositing mode (measured on Chrome 151 * against an NVIDIA GPU with the Vulkan ICD installed): * * - With `--disable-gpu-compositing` (the WebGL fix above), WebGPU reports `enabled_readback`, * Dawn reaches the native adapter and the game renders — but the presented frames never reach * the compositor OR the canvas: `page.screenshot()` and an in-page `drawImage(canvas)` both * come back black. Verify's black-frame detection then fails a healthy game. * - Without it, GPU compositing comes up and WebGPU reports `enabled` — but Dawn's adapter * request dies in the GPU process (`A valid external Instance reference no longer exists`), * `requestAdapter()` resolves null, and the game silently runs its WebGL2 fallback, which the * renderer probe rightly fails. * * Headed Chrome has neither problem: GPU compositing initialises against the display, Dawn gets * the native adapter, and screenshots carry the real frame. So when webgpu is requested on Linux * and a display exists, the plan is headed — with the window parked far off-screen so nothing * flashes over the creator's desktop, and `--disable-backgrounding-occluded-windows` so Chrome * does not throttle rendering in a window nobody can see. `--disable-gpu-compositing` is dropped * in this mode: compositing is the point, and keeping the flag puts the run back in the black * readback mode above. * * Everywhere else — webgl, macOS, CI, software GL, or a Linux box with no display (ssh, systemd, * detached agents) — the plan is headless with the exact args `browserRendererArgs` always chose. */ export interface BrowserLaunchPlan { headless: boolean; args: string[]; } export declare function browserLaunchPlan(env: Record, renderer: RendererChoice, platform?: Platform): BrowserLaunchPlan; /** * The plan for the automatic software-GL re-run: `browserLaunchPlan` as if * `BITMAGIC_BROWSER_SOFTWARE_GL=true` had been set. * * Both verify paths (one-shot and `--watch`) relaunch through this after `shouldRetrySoftwareGl` * says the machine's GPU stack is the likely culprit, so neither has to know which env var forces * the fallback. */ export declare function softwareGlLaunchPlan(env: Record, renderer: RendererChoice, platform?: Platform): BrowserLaunchPlan; /** * Whether a failed run is worth one automatic re-run on software GL. * * A lost WebGL context is a property of the machine's GPU/driver/compositor stack, not of the * game (see the file header) — the software path sidesteps that stack entirely. Pointless when * software GL was already in effect, which is what `usesSoftwareGl` establishes. */ export declare function shouldRetrySoftwareGl(observations: { webglContextLost: boolean; }, env: Record): boolean;