import { Engine } from './core/engine.js'; import { createConfig } from './core/config.js'; import { RenderSystem } from './render/index.js'; import { MaterialSystem } from './materials/index.js'; import { SkySystem } from './sky/index.js'; import { WorldSystem } from './world/index.js'; import { PhysicsSystem } from './physics/index.js'; import { PlayerSystem } from './player/index.js'; import { WeaponSystem } from './weapons/index.js'; import { FxSystem } from './fx/index.js'; import { AiSystem } from './ai/index.js'; import { UiSystem } from './ui/index.js'; import { AudioSystem } from './audio/index.js'; import { installShotApi } from './dev/shots.js'; import { prewarm } from './core/prewarm.js'; const params = new URLSearchParams(location.search); const capture = params.get('capture') === '1'; // Deterministic shutter for the pixel gate: the engine does not schedule its own // frames, the driver advances exactly N of them through window.__PUMP__. Opt-in, // because tools that measure real frame pacing (tools/perf.mjs) need the loop to // free-run. See the long comment in src/dev/shots.js. const lockstep = capture && params.get('lockstep') === '1'; const config = createConfig({ quality: params.get('q') ?? 'ultra', deterministic: capture, }); const canvas = document.getElementById('game'); const engine = new Engine({ canvas, config }); // Registration order is irrelevant — Registry topo-sorts on static deps. engine .add(RenderSystem) .add(MaterialSystem) .add(SkySystem) .add(WorldSystem) .add(PhysicsSystem) .add(PlayerSystem) .add(WeaponSystem) .add(FxSystem) .add(AiSystem) .add(UiSystem) .add(AudioSystem); try { await engine.init(); } catch (err) { console.error('[boot] init failed', err); document.body.insertAdjacentHTML( 'beforeend', `
BOOT FAILURE\n\n${err.stack ?? err.message}
` ); throw err; } const shotApi = installShotApi(engine, { capture, lockstep }); // Compile every shader permutation before the frame loop starts. Measured: without // this, 86 programs compile lazily during play, up to 30 on one frame, producing // 3.1-3.9 SECOND stalls. See src/core/prewarm.js. // // ON BY DEFAULT since the capture path was made frame-deterministic; opt out with // `?prewarm=0`. It is now PROVEN pixel-neutral: `tools/baseline.mjs` with // `--query=prewarm=0` vs `--query=prewarm=1` reports identical:true on all 11 // shots (0 changed pixels, maxDelta 0). The two things that previously made the // ~1.4 s pre-warm spend look like a visual change were both boot-duration // couplings OUTSIDE the subsystems: (1) the shutter frame index was latency-bound // because the engine kept stepping through the driver's round trips — fixed by // lockstep in src/dev/shots.js; (2) `will-change: transform` on the compass strip // cached a composited-layer raster taken at a wall-clock-dependent moment — fixed // in src/ui/style.js. const warmup = params.get('prewarm') === '0' ? { ok: false, reason: 'disabled by ?prewarm=0' } : await prewarm(engine); console.info('[boot] prewarm', warmup); window.__PREWARM__ = warmup; engine.start(); // Capture harness handshake: only flag ready once a frame has actually landed. // // BOOT_FRAMES is deliberately a frame COUNT, not a rAF race. In lockstep mode the // engine has no loop of its own, so we hand-pump exactly this many frames and only // then raise __READY__; the shot is therefore always applied at engine frame 3, no // matter how long boot (or pre-warm) took in wall-clock terms. const BOOT_FRAMES = 3; if (lockstep) { await shotApi.pump(BOOT_FRAMES); window.__READY__ = true; } else { let warm = 0; const readyProbe = () => { if (++warm >= BOOT_FRAMES) { window.__READY__ = true; return; } requestAnimationFrame(readyProbe); }; requestAnimationFrame(readyProbe); } window.__ENGINE__ = engine; if (import.meta.hot) { import.meta.hot.dispose(() => engine.dispose()); }