arcade is a Bun workspace monorepo where one source package, @arcade/kit (packages/kit/, ~65k lines of TypeScript), and a vendored control-panel library, @arcade/knobs (packages/knobs/, ~17.5k lines), power four live WebGPU multiplayer games (getaway, blackout, outbreak, carrier) and the shared authoring workbench (lab). Every workspace package exports .ts source directly with no intermediate build step; Vite and Wrangler bundle only the modules each game imports. Game rules, procedural maps, meshes, HUDs, and art direction stay inside games/<name>/. landing/ and redirects/ remain independent projects with their own lockfiles.
flowchart TD
Games["games/{getaway,blackout,outbreak,carrier}"]
LabApp["games/lab (/arcade/lab)"]
Shared["games/<name>/shared (deterministic world, physics, protocol)"]
Server["games/<name>/server (Worker router, Lobby DO, Room DO)"]
Client["games/<name>/src (WebGPU scene, HUD, audio, Lab adapters)"]
Kit["@arcade/kit (sim, net, gpu, fx, rig, vehicle, client, audio, weather, lab, dev-control)"]
Knobs["@arcade/knobs (vendored control panels, presets, seeded dice)"]
Games --> Shared & Server & Client
Server --> Shared & Kit
Client --> Shared & Kit
LabApp --> Kit & Client
Kit --> Knobs
Each workspace game splits its code into three directories (shared/, server/, src/) over @arcade/kit and @arcade/knobs:
| Layer | Path | Runtime & constraints | What belongs here |
|---|---|---|---|
| Shared game logic | games/<name>/shared/ |
Pure ESNext (window, document, WebSocket, DurableObject, and GPUDevice are forbidden) |
Fixed-tick world state (stepWorld), game rules, collision and combat math, procedural map/chunk generation, and wire message schemas. Runs identically in the browser client, the Cloudflare Durable Object, and bun test. |
| Authoritative server | games/<name>/server/ |
Cloudflare Workers & Durable Objects (@cloudflare/workers-types) |
Worker URL prefix stripping (/arcade/<name>), Lobby Durable Object (backed by @arcade/kit/sim/matchmaking and ctx.storage), and Room Durable Object (fixed-tick loop, seat tokens, snapshot broadcast, and SQLite checkpoints in Outbreak). |
| Browser client | games/<name>/src/ |
Browser DOM, WebGPU, Web Audio | Entry loop (main.ts), game-specific scene and mesh builders, local/network session wiring, HUD, sound cues, and lazy Lab adapters (src/lab/*). |
| Shared engine kit | packages/kit/ (@arcade/kit/*) |
Subpath-scoped (sim and net are runtime-pure; gpu, fx, client, audio, weather, lab, and dev-control target the browser/dev server) |
Reusable simulation, networking, WebGPU rendering, effects, character rigs, vehicle presentation, controls, audio, weather, the Lab workbench, and the arcade dev-control bridge. |
| Control panels | packages/knobs/ (@arcade/knobs) |
Browser DOM & optional WebGPU die | Vendored from Kit's knobs repository (bun packages/knobs/scripts/sync.ts; never edited in place). Draws every game's Backquote overlay, the Lab Details column, and station controls cards through @arcade/kit/lab-knobs. |
Instead of splitting shared code across separate npm packages with cross-package version drift, @arcade/kit exposes subpath exports (@arcade/kit/sim/rng, @arcade/kit/gpu/renderer, @arcade/kit/net/prediction) from a single packages/kit/ workspace folder checked by bun run check:packages:
Lot adds gpu/gltf and gpu/textured-mesh: static GLB node transforms, meshopt decoding, base/normal/metallic-roughness/occlusion maps and a ScenePass on the existing HDR, polar-shadow and temporal path. The optional UV stream is 32 bytes; existing compact vertices remain 24 bytes. Alpha-mask shaders are separate so opaque imported surfaces keep the discard-free fsSolid path. Skins, animation, morphs, clearcoat, emissive maps and texture-transform extensions are outside this adapter's scope. gpu/night-atmosphere provides shared height-fog integration, a light-pollution sky and analytical lamp scattering. client/first-person supplies pointer lock, grounded collision movement and keyboard ownership for this first-person consumer. gpu/post/camcorder is the opt-in Camera filter final pass (iPhone, VHS, CRT TV, body cam) on Night Shift, the Lot and Outbreak; its body cam drives FirstPerson.mount through client/bodycam-rig. gpu/screen puts a CRT or LCD in the world: ScreenDisplay runs the signal stage (fit, decode, snow, afterglow, mips, VHS through the camera filter) and the light readback; ScreenQuadPass draws its glass in a scene, ScreenFinalPass fills the screen, and screenSceneLight / screenBounceSource turn the picture into a room light and a lightmap scale. Other renderers can evaluate the tube with screenBindingsWgsl + screenTubeWgsl; Outbreak's nearest television uses it behind a Details toggle. gpu/forest draws the Lot's 51k Ashwood trees instanced (LOD bands with dithered cross-fades, far thinning), and sim/debris + gpu/scatter-mesh place and bake its ground dressing.
sim/(rng,geom,obb,ai,ledger,matchmaking): Deterministic simulation building blocks.rng.tsprovidesRng,mulberry32,seedOf,hash2, andhash3.geom.tsprovides the CSR static rectangle grid (RectGrid), zero-allocation DDAraycast/clearLine, circle-rectpushOut, andMinHeap.obb.tsruns the 2D SAT two-point contact manifold and Box2D 2×2 block impulse solver in both(x, z)and(x, y)coordinates.ai.tsholds shared steering and sight helpers.ledger.tsimplements the deterministic item store, container rules, and crafting engine used by Outbreak progression.matchmaking.tsprovidesSharedMatchmaker, room codes, and name sanitization across all multiplayer lobbies.net/(bytes,seats,jitter-buffer,interest,rewind,interpolate,prediction): Multiplayer transport, interest/delta management, and reconciliation primitives.bytes.tspacks binary frames, varints, angles, nibble pairs, bitsets, and shared prefixes.seats.tsseparates public player IDs from private reconnect tokens.jitter-buffer.tsadapts playout delay while preserving one-shot input edges and bridging small packet-loss gaps.interest.tsprovides per-recipient spatial interest bubbles, priority accumulation,AckHistoryfor delta compression againstackSnapTick, andBandwidthPacer.rewind.ts(RewindHistory) provides server-side lag-compensated hitscan rewind.interpolate.ts(SnapshotBuffer) interpolates remote entities between bracketed server snapshots, andprediction.ts(Predictor,Smoother,OptimisticEvents) replays unacknowledged local inputs on server ack, decays position error exponentially, and tracks press-frame optimistic actions with confirm/rollback/reapply hooks.gpu/(mesh,renderer,shaders,look-shaders,level,limits,motion,motion-blur,sprite-motion,surface-materials,foliage),look.ts,textures/,gpu-perf.ts: WebGPU rendering infrastructure.mesh.tspacks vertices into 24 bytes (float32x3position,snorm8x4normal,unorm8x4colour,float16x2material/aux).renderer.ts,shaders.ts, andlevel.tsrun the shared polar-shadow compute pass, cutaway chunk router, 4× MSAA HDR target, and 5-level Jimenez bloom chain.motion-blur.tsandmotion.tsprovide the depth-aware 8-tap shutter gather and transform velocity history.surface-materials.tsandtextures/supply procedural surface recipes and the 16-layer256×256WebP texture array.foliage.tsgenerates procedural trees, shrubs, hedges, and grass with actor trample bending.look.tsandlook-shaders.tsdefineLookSettingspresets and WGSL grading hooks.gpu-perf.ts(PerfSampler) formats rolling p50/p95/FPS and whole-frame GPU timestamp intervals.fx/(particles,particle-renderer,volume*,smoke-trail,tyre-renderer,explosion,fire-water*,splatter*,skids,skid-renderer,frame-timer): Fixed typed-array particle pools, rate and distanceEmitters, lit billboard/spark/debris rendering, four-connected BFS flood-fill volumetric smoke (SmokeVolumes), continuous ribbon tyre trails, persistent multiplied skid-mark decals, multi-stage explosions, fire and water jets, and surface blood decals.rig.ts,human-locomotion.ts,human-pose.ts,human-profile.ts,shamble-crouch.ts,pose-diff.ts: The shared procedural human rig (human-rig.md and animation.md), including planted-foot walk/run/sprint gait, zombie shamble and crouch, two-bone IK, semantic limb and prop detachment, and mocap pose-diff measurement.vehicle/(chassis,body-pose,lights,emissions,slip,driving-fx,fx,smoke-trail,smoke-renderer,sprites,audio/*,lab): Presentation vehicle chassis spring/damper pose, wheel rotation, headlamp/taillamp mounts, per-wheel slip estimation, tyre heat, skid and smoke emissions by surface, the shared Driving FX Lab group, and procedural drivetrain and nitro audio synthesis shared by Getaway, Blackout and Outbreak.client/(controls,key-cap,input,camera) andorbit-gestures.ts: PhysicalKeyboardEvent.codebindings, QWERTY prompt labels (keyLabel) with opt-in layout labels, SVG/HTML key caps with hold progress rings (keyCapHtml,keyPromptHtml), filled action glyphs for prompts (action-glyphs), top-downInputandCamera, and the unified 3D viewport gesture grammar (orbit-gestures.ts) shared across Lab, Catalogue, and Characters, and the lo-fi phosphorboot-screen(real per-stage and per-byte load progress, painted from index.html by thescripts/boot-screen.tsVite plugin before the bundle arrives; Night Shift).audio/(runtime,mix,offline,synth,cues,emitters,library,manifest,lab,footsteps,ambience): Shared Web Audio graph (default or game-definedmix: named buses, muffle, ducks, sends),OfflineAudioContextrenderer for deterministic node-graph and sample proofs, procedural sound synthesis (quiet seeded footsteps and night ambience beds), and the semantic sound library (@arcade/kit/audio/library, sfx-library.md).weather/(settings,state,wgsl,renderer,lightning,lightning-pass): Continuous-phase weather simulation (Clear,Drizzle,Rain,Storm,Fog,Snow), wind gusts, multi-stroke lightning (a flash envelope instate; a seeded branching channel, stroke timing, the WCAG 2.3.1FlashLimiterand its HDR ribbon pass inlightning/lightning-pass), progressive surface wetness, wet-surface WGSL hooks (weatherSurfaceDeclarationsWgsl), and GPU-instanced precipitation and splash rendering.lab/,lab-panel.ts,lab-quickswitch.ts,lab-navigation.ts,lab-knobs.ts,lab-tuning.ts,dev-control.ts,lab-commands.ts,dev-hub*.ts: The standalone three-column Lab workbench (Outliner,Details,Librarydrawer,⌘Kpalette,Watchpins, 8-sector radial menu, transport, frozen-frame scrub, A/B split wipe, variation grid, and station workspaces), the floating game debug overlay (LabPanel, with its one-key return to play and the[]quick switch), the sharedLabCommandRegistry, the loopback/__arcade/controlWebSocket bridge for thearcadeCLI, single-tab Studio Tab Strip tours, and the local Dev Hub.
The table below shows which shared modules each game and the Lab import. scripts/architecture-map.ts generates this table from source imports, and packages/kit/architecture-map.test.ts checks it during bun run check:packages so it cannot drift. Run bun scripts/architecture-map.ts --write after adding or removing @arcade/kit imports in a game.
Shared module (@arcade/kit / @arcade/knobs) |
Responsibility | Getaway | Blackout | Outbreak | Carrier | Lab | Brawl | Archers |
|---|---|---|---|---|---|---|---|---|
proof/* |
Rendered-frame quality gates and deterministic warm-sweep capture hooks | — | — | — | — | Yes | — | — |
sim/replay |
Seeded input recordings, per-tick state hashes and first-divergence golden checks | — | — | Yes | — | — | — | — |
sim/rng |
Seeded PRNG (Rng, mulberry32, seedOf, hash2, hash3) |
Yes | Yes | Yes | Yes | Yes | Yes | Yes |
sim/geom |
RectGrid, DDA raycast/clearLine, pushOut, MinHeap |
— | Yes | Yes | — | Yes | Yes | — |
sim/obb |
2D SAT manifold and Box2D 2-point block impulse solver | Yes | Yes | Yes | — | — | — | — |
sim/ai |
Shared steering, sight checks, and tactical/zombie AI helpers | — | Yes | Yes | — | — | — | — |
sim/flow-field |
Shared per-goal flow fields (FlowFieldCache) over GridNav flat grids or chunk tiles |
— | — | Yes | — | — | — | — |
sim/ledger |
Deterministic item store, containers, and crafting rule engine | — | — | Yes | — | — | — | — |
sim/matchmaking |
SharedMatchmaker, room codes, and player name sanitization |
Yes | Yes | Yes | Yes | — | Yes | Yes |
sim/move |
Top-down circle body step with eased velocity and substepped push-out | — | Yes | — | — | — | Yes | — |
sim/{abilities,props,boids} |
Data-driven, server-authoritative abilities (casts, projectiles, swarms, zones, dashes, grabs), shoveable OBB props, and a presentation boids flock | — | — | — | Yes | — | Yes | — |
sim/{thrown,flare-burn} |
Deterministic thrown body (arc, bounce, roll, stops against trunks, rocks and walls, rests) and a road flare's pure burn and sputter | — | — | — | — | Yes | — | — |
server/* |
Room and Lobby Durable Objects (RoomObject, LobbyObject) and the room game Worker router |
Yes | Yes | Yes | Yes | — | Yes | Yes |
net/* |
Room seats/idle/tick core and client socket, binary framing, seat tokens, JitterBuffer, SnapshotBuffer, Predictor, Smoother |
Yes | Yes | Yes | Yes | — | Yes | Yes |
gpu/wgsl |
Typed WGSL structs (wgslStruct), slot specialization (wgslSlot), and snippet composition |
Yes | Yes | Yes | Yes | Yes | Yes | — |
gpu/mesh |
24-byte packed vertex builder (Mesh, packVerts) |
— | Yes | Yes | Yes | Yes | Yes | Yes |
gpu/{cull,targets,pass-graph,reactive} |
Frustum/shadow-sweep culling, contiguous draw merging, ScenePass slots, MSAA/HDR targets, 5-level bloom, and the TAA reactive-mask contract |
Yes | Yes | Yes | Yes | Yes | Yes | Yes |
gpu/{renderer,shaders,level} & limits |
Polar shadows, cutaway chunks, 4x MSAA HDR, and 5-level bloom | — | Yes | Yes | — | Yes | Yes | Yes |
gpu/{fog-scatter,volume-pass,fog-blur} |
Analytic height fog, fog-sky skylight, equiangular lamp scattering and wet streaks, drifting fog bank, lit cloud deck and fog-depth blur; volume slots with depth, history, transmittance and depth-aware upsample | — | Yes | Yes | — | Yes | Yes | — |
gpu/{boot,scene-frame} |
WebGPU canvas boot, and frame inputs for small arenas: placed lights, box occluders, flat floor lighting | — | Yes | — | — | Yes | Yes | Yes |
gpu/lights/{physical,beam,flicker,street} |
Opt-in physical lights and surfaces (GGX, 3D spot cones, torch beam profile, sky reflections, specular AA), street lamps as lens emitters with roadway photometry, and stateless lamp flicker and failure | — | — | Yes | — | Yes | — | — |
gpu/shadows/spot-shadows |
Perspective shadow maps for the few most important spot lights, re-rendered only when a slot changes | — | — | — | — | Yes | — | — |
gpu/gi/{plan-bounce,lightmap} |
One bounce of the frame's lights in plan space, and Cycles per-light baked lightmap arrays (rgb9e5ufloat) + 3D L1 SH probe volumes for rooms |
— | — | Yes | — | Yes | — | — |
gpu/shadows/capsule-shadows |
Analytic soft character shadows from rig bone capsules: contact occlusion and a contact-hardening shadow per light and the sun, grid-culled | — | — | Yes | — | — | — | — |
gpu/contact-shadows |
Screen-space contact shadows volume slot: a short depth march toward the brightest lights at each pixel, with history | — | — | Yes | — | — | — | — |
gpu/reflections/{ssr,surface,wet} |
Screen-space reflections volume slot (Ashwood's march, streak blur and history), HDR-alpha roughness tag, and the shared wet/puddle surface term | — | — | — | — | — | — | — |
gpu/{motion,motion-blur,sprite-motion} |
Depth-aware 8-tap shutter gather and actor/sprite velocity history | Yes | Yes | Yes | — | Yes | Yes | — |
gpu/{surface-materials,foliage} & textures |
Procedural surface recipes, 16-layer WebP array, foliage, and trample | — | Yes | Yes | — | Yes | Yes | — |
gpu/finish & gpu/materials/wear |
Finishing pass: chamfers, baked vertex AO/curvature/exposure, wear looks, CC0 scans, albedo validator | — | — | — | — | Yes | — | — |
props/* |
Generated furniture library: catalogue, .prop meshes, instanced batches, seeded room placement |
— | — | Yes | — | Yes | — | — |
look, gpu/look-shaders & gpu/post-uniform |
LookSettings presets, WGSL scene/post grading hooks and the game post uniform after them |
— | Yes | Yes | — | Yes | Yes | Yes |
sim/creature-motion |
Lamplight's creature root: deterministic planar momentum, brake/plant/pivot phases, SecondOrder springs, Froude scaling |
— | — | — | — | Yes | — | — |
creature/* |
Arachnid legs (world-space feet, footholds, swing clearance, IK, body springs, contacts), matte tube view and Lab settings | — | — | — | — | Yes | — | — |
gpu/post/camcorder |
Opt-in Camera filter final pass (iPhone, VHS, CRT TV, body cam) on Renderer.setFinalPass |
— | — | Yes | — | — | — | — |
gpu/screen & gpu/lights/{panel,practicals} |
Opt-in CRT/LCD displays on a 3D quad or full screen whose picture lights the room (async mip readback, upright panel light), and practical fixture presets (failing tube, caged bulb, LEDs) | — | — | Yes | — | Yes | — | — |
forest/{view,settings} & sim/{forest-world,forest-demo} |
Photoreal night forest renderer (scanned trees, mist bank, beams, exposure, quality tiers) over a plain ForestWorld |
— | — | — | — | Yes | — | — |
client/walk-camera |
First-person walking camera: stride bob, settle and reduce-motion over client/walk |
— | — | — | — | Yes | — | — |
gpu-perf, fx/frame-timer & quality |
Rolling p50/p95/FPS PerfSampler, GPU pass timestamp telemetry and adaptive quality tiers (GameQuality) |
Yes | Yes | Yes | Yes | Yes | Yes | Yes |
fx/{particles,particle-renderer} |
Fixed typed-array particle pool, Emitter, and billboard/spark/debris pass |
Yes | Yes | Yes | Yes | Yes | Yes | Yes |
fx/flare |
Road flare (fusee): strontium flame, sputter, sparks, slag and a lit smoke column, plus its scene and fog lights | — | — | — | — | Yes | — | — |
fx/{volume*,smoke-trail,tyre-renderer} |
Flood-fill volumetric smoke and continuous ribbon tyre trails | Yes | Yes | Yes | — | Yes | — | — |
fx/{explosion,projectile-trail,fire-water,splatter*} |
Multi-stage explosions with trailing fragments and water splashes, fire/water jets, and surface blood decals | Yes | Yes | Yes | Yes | Yes | Yes | — |
rig & human-* |
Shared human skeleton, planted-foot gait, shamble, IK, detachment, pose-diff | — | Yes | Yes | — | Yes | Yes | Yes |
avatar/* |
Top-down human avatar adapter: soldier, wizard, zombie and skeleton meshes, weapon clips, ragdolls (soldier and any 15-bone figure), hit-reaction springs | — | Yes | Yes | — | — | Yes | Yes |
vehicle/* |
Presentation chassis pose, lamps, tyre emissions, and drivetrain/nitro synth | Yes | Yes | Yes | — | Yes | — | — |
client/{controls,key-cap,action-glyphs,chat.css} |
Physical KeyboardEvent.code bindings, QWERTY labels, key-cap UI, prompt action glyphs, and chat styling |
Yes | Yes | Yes | Yes | Yes | Yes | Yes |
client/{input,camera,side-camera,smooth} & orbit-gestures |
Top-down Input, Camera (trauma² shake, directional kick, HitStop), rig-agnostic TraumaShake, side-view SideCamera, and orbit/pan/zoom gestures |
— | Yes | Yes | Yes | Yes | Yes | Yes |
audio/* |
Web Audio runtime, offline renderer, procedural synth, and sfx-library |
Yes | Yes | Yes | Yes | Yes | Yes | Yes |
sky/* |
Lazy catalogue stars, HDR night sky, textured Moon, astronomical clock and lunar illuminance | — | — | Yes | — | Yes | — | — |
water/* |
Opt-in continuous water normals, optics and typed controls; Carrier ocean and BARBARA lake adapters | — | — | — | Yes | — | — | — |
weather/* |
Continuous-phase weather sim, wet-surface WGSL, and rain/snow/splash pass | — | — | Yes | — | Yes | — | — |
shell |
The player-facing game shell: settings (volume, mute, quality, reduce motion, key labels), title and lobby, Escape menu, leave warning and loading (game-shell.md) | Yes | Yes | Yes | Yes | — | Yes | Yes |
lab-panel & @arcade/knobs |
Backquote debug overlay and Lab Inspector drawn by packages/knobs |
Yes | Yes | Yes | Yes | Yes | Yes | Yes |
ramps & ramp-table |
Gradient and curve settings kept as JSON text, baked into lookup tables for per-frame reads | — | — | Yes | — | Yes | — | — |
lab/* & compare |
Lab workbench, sections, adapters, A/B compare, variation grid, and stations | Yes | Yes | Yes | — | Yes | — | — |
dev-control, lab-commands & dev-hub |
LabCommandRegistry, /__arcade/control bridge, arcade CLI, tours, and Dev Hub |
Yes | Yes | Yes | Yes | Yes | Yes | Yes |
Workstream A (engine-render) converges Arcade's three WebGPU renderers into one unified rendering foundation in @arcade/kit/gpu that retains the strongest capabilities of each game's renderer:
- From the Kit Renderer (
@arcade/kit/gpu/renderer, Outbreak & Blackout): 24-byteVERT_BYTESvertex layout (float32x3pos,snorm8x4normal,unorm8x4color+aOpenTone,float16x2mat/aux), GPU polar 1D ray-cast local shadows (512rays ×64lights) with partial-height furniture blockers, temporal compute visibility field (VisibilityFieldShader), baked 2D indirect bounce (bounceTex) and floor contact AO (floorTex), occupancy-gated roof cutaway and 4 m Z-banded Bayer-dithered wall cutaway (fsSolidvslevelCut), footprint-filtered (fwidth) procedural surface recipes + 16-layer256²WebP texture array (sampleMaterialFastinpostLightMaterial), two-stage vertex wind + 8-actor foliage trample (foliage.ts), corner-safe volumetric smoke + half-res scissor-bounded flashlight shafts, and the opt-inLookSettingsgrading pipeline (Noir,Painterly,Dusk,Sodium 70s,Golden Hour) with lazy per-variant WGSL specialization. - From Getaway (
games/getaway/src/render/renderer.ts): Frustum-swept2048²texel-snapped orthographic directional sun/moon shadow pass (DirectionalShadowPass+texelSnappedOrthoLightMatrix+isShadowCasterVisible, usable as aScenePass), stencil-masked occluded vehicle/actor ghost silhouettes (depthCompare: "greater",stencil != 1), half-resolution per-instance/per-sprite velocity buffer + 8-tap depth-aware shutter motion blur (MotionBlur,PreviousTransforms,+0.23 msat 1440p) with nitro radial burst and lazily compiledmotionPipes, alpha-weighted decal atlas mips (uploadAlphaWeightedMipChain), and world-space additive spotlight beams. - From Carrier (
games/carrier/src/render/): ComposableScenePassplugin architecture (prepare?(enc)+draw(pass)in ordered"sky" → "opaque" → "alpha"slots viaPassListwith@group(0)Frame bound), configurablePassContextdepth convention including reversed-Zdepth32float(clearValue: 0,greater-equal) for 40 km horizons with zero Z-fighting, pre-allocatedcolorAttachment/depthAttachment/mainPassdescriptors mutated in place on resize/render (zero per-frame descriptor allocations), analytical Gerstner wave + seabed refraction/foam shading, and physical day-night atmosphere helpers (skyColor,hazeColor,applyFog).
| Capability | Kit Renderer (Outbreak / Blackout / Lab) |
Getaway Renderer | Carrier Renderer | Best-in-Class Source | Target Home in @arcade/kit/gpu |
|---|---|---|---|---|---|
| Pass extensibility & zero-alloc descriptors | PassList slots + pre-allocated SceneTargets.mainPass |
PassList slots + pre-allocated SceneTargets.mainPass |
ScenePass plugin slots (sky → opaque → alpha) + in-place mainPass descriptors |
Carrier | @arcade/kit/gpu/pass-graph (ScenePass, PassList, PassContext) & @arcade/kit/gpu/targets (SceneTargets.configureMainPass) |
| Depth buffer modes | depth24plus (less-equal, clear 1) |
depth24plus-stencil8 (less, stencil ref 1) |
Reversed-Z depth32float (greater-equal, clear 0, 40 km far) |
Carrier (far horizon) & Getaway (stencil) | @arcade/kit/gpu/pass-graph (DepthMode: "standard" | "stencil" | "reversed-z") |
| Compact vertex format | 24 B VERT_BYTES (f32x3 pos, snorm8x4 nrm, unorm8x4 col+AO, f16x2 mat/aux) |
48 B (12 × f32) |
24 B VERT_BYTES (packVerts) |
Kit Renderer & Carrier | @arcade/kit/gpu/mesh (VERT_BYTES = 24, packVerts) |
| Local practical shadows | GPU polar 1D ray-cast (512 rays × 64 lights, partial-height blockers, 5/9-tap PCF) |
Unshadowed point/cone loop | Unshadowed deck/navigation lights | Kit Renderer | @arcade/kit/gpu/shaders (polarShader, shadowAt, partialShadow) |
| Directional sun/moon shadows | Baked static R8 moonTex mask + opt-in DirectionalShadowPass |
Texel-snapped 2048² depth32float DirectionalShadowPass + 9-tap PCF + shadow-sweep culling |
Analytical sun diffuse + horizon self-shadow | Getaway | @arcade/kit/gpu/pass-graph (DirectionalShadowPass, texelSnappedOrthoLightMatrix) + @arcade/kit/gpu/cull (isShadowCasterVisible) |
| Indirect GI & contact AO | Dijkstra 2D bounceTex + floorTex.r AO + Jimenez multi-bounce polynomial |
Vertex room tints + wheel-weighted car contact decal (kind == 9) |
Hemisphere sky/sea bounce | Kit Renderer & Getaway | @arcade/kit/gpu/shaders (bounceAt, shadeScene) + analytical contact decals |
| Surface materials & textures | 6 fwidth procedural recipes + 16-layer 256² WebP array (sampleMaterialFast after lightSum) |
CPU alpha-weighted decal atlas (uploadAlphaWeightedMipChain) + window material |
Packed 24-byte MAT (lit, glow, foliage) |
Kit Renderer & Getaway | @arcade/kit/gpu/surface-materials + @arcade/kit/textures + @arcade/kit/gpu/targets |
| Vegetation & interactive wind | Curved 3D grass, dome-normal leaf mounds/rosettes, 2-stage wind, 8-actor trample | Static city trees | Swaying island foliage stream (splitFoliage) |
Kit Renderer | @arcade/kit/gpu/foliage (FoliageWindConfigStruct, FoliageActorsStruct) |
| Ocean, sky & long-range fog | Plan-distance height fog | Exponential distance fog | Gerstner swell/chop, seabed refraction, skyColor/hazeColor/applyFog, clouds/stars |
Carrier | Carrier OceanPass / SkyPass / CloudsPass (ScenePass plugins) + shared atmosphere WGSL |
| Cutaway & occluded readability | Occupancy roof cutaway + 4 m Z-banded Bayer-dithered levelCut walls + exploredTex memory |
Stencil-masked occluded car ghost silhouette (fsGhost, STENCIL_GHOST_FACE) |
Unoccluded open sea/air | Kit Renderer (cutaway) & Getaway (stencil ghost) | @arcade/kit/gpu/renderer (levelCut routing) + @arcade/kit/gpu/pass-graph (STENCIL_OCCLUDER_FACE, STENCIL_GHOST_FACE) |
| Volumetrics & combat FX | Flood-fill SmokeVolumes, half-res scissor drawVolume flashlight shaft, SplatterRenderer, ParticleRenderer, WeatherRenderer |
SmokeVolumes, ParticleRenderer, ribbon SmokeRenderer, heli setBeams |
ParticleRenderer, multi-stage ship/jet explosions, RainPass |
Kit Renderer & @arcade/kit/fx |
@arcade/kit/fx/* + @arcade/kit/weather + @arcade/kit/gpu/renderer |
| Motion blur | Shared MotionBlur (camera + burst) & lazy ensureMotionPipes() |
Half-res Velocity pass (+0.23 ms) + PreviousTransforms + 8-tap depth-aware shutter gather + nitro radial boost |
None | Getaway & @arcade/kit/gpu/motion-blur |
@arcade/kit/gpu/motion-blur + @arcade/kit/gpu/motion |
| Post-processing & Look grading | 5-level BloomPyramid + 6 LookSettings presets (4 tonemappers, Kuwahara, outlines, halation, split-wipe) + per-variant WGSL |
5-level BloomPyramid + ACES + chromatic fringe |
5-level BloomPyramid + Hill ACES (1.6× exposure) + sRGB + grain |
Kit Renderer | @arcade/kit/gpu/targets (BloomPyramid) + @arcade/kit/gpu/look-shaders |
| Chunk streaming & culling | 9-slot STREAM_SLOT_BYTES world ring + 2D rect culling + mode-grouped contiguous draw merging |
6-plane 3D frustum + shadow-sweep culling + contiguous draw merging | 2D ground-rect frustum culling + contiguous draw merging | All three (converged) | @arcade/kit/gpu/cull (extractFrustumPlanes, aabbInFrustum, boxIntersectsRect2D, drawContiguousChunks*) |
| GPU frame pacing & telemetry | Single-in-flight installFramePacer + FrameTimer whole-frame GPU timestamps + PerfSampler p50/p95 |
FrameTimer + GpuPerf |
Single-in-flight installFramePacer |
Kit Renderer | @arcade/kit/gpu-perf + @arcade/kit/fx/frame-timer |
Every frame executes a deterministic 7-stage pass schedule over @arcade/kit/gpu/targets (SceneTargets + BloomPyramid) and @arcade/kit/gpu/pass-graph (PassList). Stages with zero active work are skipped with zero GPU pass overhead:
flowchart TD
Prep["1. Pre-Pass Prepare\npasses.prepareAll(enc) · particle/splatter/weather uploads"]
Shadow["2. Shadow & Visibility Passes\n• Polar 1D ray-cast compute (512 rays × N lights) + VisibilityField compute\n• Optional 2048² depth32float directional shadow pass (isShadowCasterVisible + drawContiguousChunks)"]
Main["3. Main 4× MSAA HDR Scene Pass (SceneTargets: rgba16float + DepthMode)\n① sky slot passes (SkyPass, StarsPass, CloudsPass)\n② Static world: fsSolid chunks (drawContiguousChunks*) → levelCut cutaway chunks\n③ Opaque instanced models (stencil=1 when DepthMode='stencil') + opaque slot passes (Ocean, Ship, Vehicles)\n④ Translucent glass chunks + instanced alpha + alpha sprites + tyre smoke + additive sprites/beams\n⑤ Stencil-masked occluded ghost silhouettes (depth > scene, stencil != 1)\n⑥ SplatterRenderer → ParticleRenderer → WeatherRenderer → alpha slot passes"]
Vol["4. Half-Res Depth-Aware Volumetrics (loadOp: 'load' on hdrView)\n• SmokeVolumes 24-step ray-march + 4-tap bilateral upsample\n• Scissor-bounded half-res volumetric flashlight/beam shaft (drawVolume)"]
Blur["5. Velocity & Depth-Aware 8-Tap Shutter Motion Blur\n• Camera + per-instance/sprite velocity pass → 8-tap depth-gated gather + feel/nitro burst"]
Bloom["6. 5-Level Jimenez HDR Bloom Pyramid (BloomPyramid)\n• 5× 13-tap downsample passes (soft-knee threshold on mip 0)\n• 4× additive 3×3 tent upsample passes"]
Comp["7. Per-Variant Look & Display Composite\n• Lazy specialized variant ('noir' | 'painterly' | 'generic' | baseline)\n• Tonemap (ACES / Filmic / AgX / Hue-Preserving) + split-tone + grain + 8-bit IGN dither"]
Prep --> Shadow --> Main --> Vol --> Blur --> Bloom --> Comp
- Strict
fsSolidvsfs(discard) separation: Opaque world chunks, grass, trees, furniture, and instanced actors bindfsSolid(zerodiscardstatements) so Apple Silicon hardware Hidden-Surface Removal (HSR / early-Z) stays active. Only chunks intersecting the live player/peek cutaway box switch tolevelCut(fs) viadrawContiguousChunksByMode. - Transient MSAA & conditional depth store:
SceneTargetsallocatesmsaawithGPUTextureUsage.RENDER_ATTACHMENTonly (loadOp: "clear",storeOp: "discard", resolved in tile memory tohdrView).depthStoreOpis"discard"unless stage 4 (SmokeVolumes) or stage 5 (MotionBlur) is active on that frame, avoiding a104.7 MB4× MSAA depth flush at 4K. - Post-light material evaluation (
postLightMaterial):lightSumruns first using base geometry normals;sampleMaterialFast(texture_2d_arraywith compile-time constant layer indices) runs after the light loop so texture coordinates and material locals never overlap the 36-register polar shadow loop.
- Typed WGSL structs (
@arcade/kit/gpu/wgsl):wgslStruct(name, fields)computes std140/storage alignment (vec3f16-byte alignment,mat4x4f64-byte alignment,array<vec4f, N>stride,@align(16)on scalar-only uniform structs, and@size(...)tail padding with zero synthetic_pad_tailconstructor fields). DrivesLookUniformsStruct(64 floats),MaterialRecipeStruct,SurfaceTexUniformsStruct,FoliageWindConfigStruct,FoliageActorsStruct,ParticleFrameStruct,VolumeFrameStruct,GetawayFrameStruct,GetawayLightStruct,GetawayShadowFrameStruct,CarrierFrameStruct, andCarrierGradeStruct. - Slot & snippet specialization & lazy pipeline creation:
wgslSlot,wgslSnippet,resolveWgsl, andSceneHooks(diffuseTerm,combineLighting,fragmentReturn,lookSurface) assemble shaders structurally with zero.replace()mutations.Renderer.ensureLookPipes(variant)lazily compiles and caches per-variant pipeline sets ("noir"stripskuwaharaPostand Painterly gouache, compiling in1.6 ms;"painterly"strips Noir selective-colour;"generic"serves split-wipe and custom hybrid looks), andRenderer.ensureMotionPipes()defers the 8 MRT motion-velocity pipelines untilf.motionis actually enabled (cutting coldnew Renderer()pipeline creation from 20 to 8 in Outbreak and 12 in Blackout). - Shared targets, bloom, shadow pass & instance buffers (
@arcade/kit/gpu/{targets,pass-graph}):SceneTargetsmanagesmsaa,depth, andhdrtextures across allDepthModes;DirectionalShadowPassmanages the2048²depth32floatorthographic shadow map, comparison sampler, and shadow-sweep culled static/instanced pass encoding;STENCIL_OCCLUDER_FACEandSTENCIL_GHOST_FACEconfigure stencil-masked occluded silhouettes;BloomPyramidmanages the 5-level HDR mip chain, texel-size uniform buffers (upTexelSource: "target" | "source"), bind groups, and pass encoding;GrowableInstanceBuffergrows by1.5×and coalesces contiguousArrayBufferbatch slices into a singlequeue.writeBuffercall;uploadSplitSpritespacks alpha and additive sprite halves into one shared vertex buffer;uploadAlphaWeightedMipChaingenerates alpha-weighted decal atlas mip pyramids. - Spatial culling, contiguous draw merging & GPU-driven crowd scaling (
@arcade/kit/gpu/cull):extractFrustumPlanes,aabbInFrustum,boxIntersectsRect2D, andisShadowCasterVisiblecull chunks in 2D plan space or 3D clip/shadow-sweep space, whiledrawContiguousChunksanddrawContiguousChunksByModemerge adjacent visible vertex ranges (first + count === next.first) into singlepass.drawcalls. For dense crowds (ZombieCrowdRenderer), uploading one compact32 B(8-float) struct per actor (x, y, aim, phase, speed, state, hp, flags=64 KB/frameat2,000zombies) into a storage buffer and expanding the 8 body parts in the WGSL vertex shader (zombieIdx = instance_index >> 3u,partIdx = instance_index & 7u) cuts CPUrenderPack7.4×(137.8 µs → 18.7 µsat2,000zombies,3.5 µsat500) and cutsqueue.writeBufferupload12×(768 KB → 64 KB). Direct instancedpass.drawis optimal up to~1,500zombies (12,000parts, where direct0.302 msand compute-culleddrawIndirect0.279 mstie at500zombies); above1,500zombies (>12,000instances),@compute @workgroup_size(64)frustum culling intopass.drawIndirectwins1.47×at2,000zombies (0.744 ms → 0.507 ms) and2.49×at10,000zombies (2.602 ms → 1.044 msonmetal-3). - Off-thread chunk/tile bakes via
Transferableping-pong: Main-thread WebGPU command encoding takes0.3–0.6 msand stays on the main thread for minimum input-to-photon latency, while multi-millisecond static mesh and plan-texture bakes (compileSingleWorldChunk,buildCityMesh,floorTile/moonTile/bounceTile,bakeSeabed) hand off via double-bufferedTransferableArrayBufferping-pong (postMessage(buf, [buf])), transferring768 KBin15 µsmedian /25 µsp95 (11×faster median and53.8×faster p95 thanstructuredClone165 µs/1,345 µs, with zeroCOOP/COEPheaders required). - Frame pacing & GPU timing (
@arcade/kit/gpu-perf,@arcade/kit/fx/frame-timer):installFramePacercaps in-flight submissions to 1 frame and collapses multiple within-taskrender()calls to the latest state;FrameTimermeasures one complete dependent GPU interval from the first shadow/compute pass to the final composite pass (PerfSamplerreports rolling p50/p95/FPS).
| Stage | Status | Scope & Deliverables | Verification Gate |
|---|---|---|---|
Stage 1: Typed WGSL structs (@arcade/kit/gpu/wgsl) |
Shipped (PR #64) | Promote wgslStruct, wgslSlot, wgslSnippet, resolveWgsl; convert LookUniforms (eliminating 27 u0..u15 swizzles), SurfaceTex, MaterialRecipe, Foliage*, ParticleFrame, VolumeFrame, and Getaway* structs. |
diffPixels = 0 across Outbreak, Blackout, Getaway, Lab; unit tests in wgsl.test.ts. |
| Stage 2: Explicit WGSL slots & per-variant Look pipelines | Shipped (PR #69) | Replace all .replace() and regex WGSL mutations in look-shaders.ts, shaders.ts, surface-materials.ts, and Outbreak ground.ts with SceneHooks, asphaltConcreteSlot, and lookVariantSlot; cache lazy "noir", "painterly", and "generic" pipeline sets. |
diffPixels = 0 (byteIdentical: true) across all 6 Outbreak Look scenes; measured variant byte sizes & compile times. |
| Stage 3: Shared culling, targets, bloom & instance buffers | Shipped (PR #77) | Extract @arcade/kit/gpu/cull and @arcade/kit/gpu/targets; migrate @arcade/kit/gpu/renderer, Getaway Renderer, and Carrier Renderer/Post/Frame (CarrierFrameStruct, CarrierGradeStruct). |
diffPixels = 0 (byteIdentical: true) on Apple Metal across Outbreak, Blackout, Getaway, and Carrier. |
Stage 4: Shared PassList, DepthMode & shadow-sweep culling |
Shipped (PR #82) | Extract @arcade/kit/gpu/pass-graph (ScenePass, PassList, PassContext, DepthMode, compileShaderModuleWithLog) and isShadowCasterVisible; wire PassList and DepthMode across @arcade/kit/gpu/renderer, Getaway, and Carrier; document unified target architecture and migration plan. |
diffPixels = 0 across all 4 games; unit tests in cull.test.ts; clean git archive + wrangler deploy --dry-run. |
Stage 5: DirectionalShadowPass, zero-alloc SceneTargets.mainPass & lazy motionPipes |
Shipped (PR #87, PR #91) | Promote Getaway's 2048² texel-snapped orthographic directional shadow pass (DirectionalShadowPass, texelSnappedOrthoLightMatrix), stencil silhouette descriptors (STENCIL_OCCLUDER_FACE, STENCIL_GHOST_FACE), pre-allocated SceneTargets.configureMainPass descriptors, and uploadAlphaWeightedMipChain into @arcade/kit/gpu/{pass-graph,targets}; make Renderer.ensureMotionPipes() lazy on first f.motion use (cutting cold startup pipeline compilation by 60%). |
diffPixels = 0 on Getaway, Outbreak, Blackout, and Carrier; unit tests in cull.test.ts; clean git archive + wrangler deploy --dry-run. |
Stage 6: Carrier wholesale migration onto @arcade/kit/gpu/renderer |
Planned (awaiting Kit's go-ahead) | Migrate games/carrier/src/render/renderer.ts onto @arcade/kit/gpu/renderer configured with depthMode: "reversed-z":1. Frame uniform alignment: Keep Carrier's 48-float CarrierFrameStruct (viewProj, eye, sunDir, sunColor, zenith, horizon, viewport, forward, extra) bound at @group(0) @binding(0) via Renderer's PassContext so all 14 Carrier ScenePass modules (SkyPass, CloudsPass, StarsPass, OceanPass, ShipPass, BoatPass, PlanePass, TurretPass, LightsPass, WeaponFx, PlaneFx, ExplosionFx, RainPass, ZonePass) run unmodified through renderer.addPass(slot, pass).2. Static island streams: Route setStatic(chunks) (solid + foliage streams split by splitFoliage) through @arcade/kit/gpu/renderer's contiguous chunk drawer with frustumGroundRect view bounds.3. Post & Look convergence: Route Carrier's Grade (CarrierGradeStruct: Hill ACES 1.6× exposure + sRGB transfer + warmth split) through @arcade/kit/gpu's BloomPyramid composite while unlocking opt-in @arcade/kit/look presets and FrameTimer GPU telemetry in Carrier's debug menu. |
Requires Kit's explicit go-ahead before touching Carrier's CONTRACTS.md scaffold files wholesale; verified with diffPixels = 0 on seed=1&t=120&tod=14 and all 439 Carrier tests green. |
Authoritative gameplay runs at a fixed tick rate (TICK_HZ in shared/) using pure state transitions (stepWorld(world, inputs, dt)). Randomness inside shared/ always draws from a seeded Rng (@arcade/kit/sim/rng), never Math.random(). Because shared/ has zero browser or Worker dependencies, a solo/practice session in the browser and an authoritative multiplayer room in a Cloudflare Durable Object execute the exact same simulation code.
Outbreak's whole world is also a value: CoopSim.toSnapshot() returns a plain JSON SimState (lag-compensation
history included) and stepSim(state, inputs, dt) is a pure transition over it, for forks and rollouts. Chained
calls cost about as much as stepping CoopSim in place (0.12–0.27 ms/tick vs 0.5–0.7 ms when every tick rebuilt the
town and nav): stepSim keeps the sim that produced each returned state and steps it again when that exact, unedited
state comes back. Forks, older states and edited states restore into a fresh sim. scripts/stepsim-bench.ts checks
both paths hash for hash against the mutable sim; hashSimState leaves rewind out so digests stay comparable.
Each multiplayer game deploys a Cloudflare Worker (arcade-<name>) with two Durable Object classes:
Lobby: Persists active room summaries inctx.storageand routes quick-join requests throughSharedMatchmaker(@arcade/kit/sim/matchmaking).Room: Owns the authoritativeWorldstate, WebSocket connections, reconnect grace windows, idle kick timers, and fixed-tick catchup (MAX_CATCHUP).
On the client (@arcade/kit/net/*, documented in outbreak-coop.md):
- Inbound client inputs pass through
JitterBufferso network jitter does not drop one-shot button edges (interact, attack, reload). - The local player advances immediately via
Predictorand reconciles on each server snapshot ack;Smootherdecays residual positional snap over time without affecting authoritative coordinates. - Remote actors and hordes render through
SnapshotBufferatserverNow - interpolationDelay, lerping between the two bracketing snapshots while reusing pooledZombieStatepages indecodeSharedand preallocated lookup tables inNetSessionso 60 Hz frame interpolation and 20 Hz snapshot decoding allocate zero per-entity garbage.
Cross-runtime benchmarks across V8 (workerd / Chromium) and JavaScriptCore (bun) on 300–2,000 zombie scenes (scripts/spike-net-rust/, scripts/spike-ecs-vs-soa/, scripts/spike-ecs-shootout/) establish five architectural rules:
- Dense swap-remove
ZombiePoolSoA (0..count) beats general-purpose ECS frameworks (bitECS,Becsy,Miniplex,Koota):- In the 8-way 500 / 2,000-zombie full-frame shootout (
AI + collision + delta snapshot + 8-part render pack), after wave spawn/death churn,bitECSsparse-set entity IDs (bitPosX[queryEids[i]]) suffer non-monotonic L1/L2 cache gathers (176.1 µsmedian /466.4 µsp95 at 2,000 zombies),Becsyflyweight cursors (cursor.__index = i,169.5 µsmedian /325.2 µsp95) prevent V8/JSC from hoisting typed-array base pointers into registers, andMiniplexarchetype buckets chase nested JS object pointers (175.0 µs,703 KB). - A 60-line dense
0..countswap-removeZombiePoolSoA + GPU vertex-pulled gait (instance_index >> 3u/instance_index & 7u) runs the full 2,000-zombie tick in103.8 µs(25.0 µsat 500 zombies) with0 KBGC and66 KBRAM (14.2×smaller than AoS938 KB) — even beatingBevy-style#![no_std]Rust/WASM (+simd128) doing CPU 8-part matrix packing (127.4 µs).
- In the 8-way 500 / 2,000-zombie full-frame shootout (
- Keep
SurvivorandCaras monomorphic structs stripped of presentation fields:- Keep low-cardinality heterogeneous actors (
1..4Survivors,1..16Cars,4Ships) as monomorphic TypeScript structs rather than forcing them into ECS tables. - Strip presentation-only fields (
lurch,asymmetry,drag,reach,hitFlash,detachedLimbs: string[]→detachedMask: u16,recoil,shoveAnim,flickerPhase, andlootMessage) out ofshared/simulation state into client presentation, and route per-tick simulation events through a preallocatedEventRing<T>stamped by(tick, seq).
- Keep low-cardinality heterogeneous actors (
- Worker handoff rule (
TransferableArrayBufferping-pong, neverstructuredClone):- Background Web Worker bakes (
compileSingleWorldChunk,buildCityMesh,floorTile/moonTile/bounceTile,bakeSeabed) must always hand off geometry and textures via double-bufferedTransferableArrayBuffers (postMessage(buf, [buf]),15 µsmedian /25 µsp95 for768 KBwith zeroCOOP/COEPheaders required) rather thanstructuredClone(165 µsmedian /1,345 µsp95).
- Background Web Worker bakes (
- No standalone Rust/WASM wire codecs over JS objects:
- Calling a Rust
bincodeorpostcardserializer across the JS↔WASM boundary over JS entity objects is slower in V8 (3.2–4.0 µsincluding boundary marshaling) than packing aDataView/ByteWriterdirectly in TypeScript (0.80 µsin V8,5.34 µsin JSC). CombiningQuantizedHistoryRing(60 m interest bubble + priority-scheduled acked delta at778 B, fitting inside one1,460 BTCP MSS segment) withFlowFieldNavcuts 60 Hz Durable Object tick time 48.5× (741 µs → 15.3 µs) and 4-player outbound bandwidth 7.3×.
- Calling a Rust
- When to use Rust/WASM (
#![no_std]wasm32-unknown-unknown):- Reserve Rust/WASM strictly for future zero-copy leaf modules where simulation state and binary snapshot encoding share the same WASM linear memory without per-entity JS marshaling—specifically 5,000-zombie mega-hordes or
Q16.16fixed-point deterministic lockstep/replay modes that must avoid V8 vs. JSCMath.sin/cos/atan2transcendental ULP drift.
- Reserve Rust/WASM strictly for future zero-copy leaf modules where simulation state and binary snapshot encoding share the same WASM linear memory without per-entity JS marshaling—specifically 5,000-zombie mega-hordes or
In Outbreak, adding or modifying a feature (map, weather, pacing, progression, vehicles, melee, hordes) does not require editing central simulation, wire-protocol, Lab control, command, harness, feel-event, or co-op client files:
- Shared simulation & wire hooks (
games/outbreak/shared/feature-registry.ts,outbreakSharedFeatures): Server/Worker-safe registry (zero DOM imports) ordered deterministically by(order, id).shared/features.tsis the one list of shared features (CoopSimimports it); the registry and every feature see the sim asOutbreakSim(shared/sim-types.ts, which also holdsCommand,Survivor,SurvivorSlotand session tuning) and the wire asshared/net-types.ts, so nothing in a feature importscoop-sim.tsornet-protocol.ts.bun scripts/check-cycles.ts(in Outbreak'scheck) fails on any import cycle inshared/,src/orserver/, type imports included. Features register per-CoopSimstate allocation (createState) and deterministic cloning (cloneState,CoopSim.cloneFeatureStatesTo) so simulations remain pure and forkable for headless bot rollouts, alongside simulation step hooks (registerInteractions,onChunkSync,onTopologyChanged,stepSurvivorMovement,pushOutSurvivor,stepSurvivorActions,stepWorld,transformAISurvivor,modifySurvivorDamage,refreshPrompt; every survivor damage source, crowd or environment, goes throughsurvivorDamageso seat armor and escape immunity apply), tuning namespaces (resolveTuningNamespaces), and binary snapshot section codecs (OutbreakNetSectionCodec). - Discovery and progression (
games/outbreak/shared/{discovery,progression}/):shared/progression/holds the ledger catalog, tuning and rule table, loot and bench sites, runtime types and theOutbreakProgressioninventory core. D-19 discovery lives inshared/discovery/: pure site and rule modules (recipes,found,verbs,vignettes,barricade) and the runtime partsRecipeBoard(the bench's recipe board, corkboard),FoundKnowledge(pages, practice, study, held-out buildings) andFieldVerbs(closets, tripwires, cars, bottles, glass, repairs, vignette traces), which progression composes throughProgressionHost. Their state is ledger stores plus themastery/study/heldOutkeys of the progression snapshot, soSimStatekeeps its shape. Cross-feature shapes (Notice,ChannelView,PickMode,PistolAmmo) live in the pure types seamshared/types.ts. - Client presentation, Lab & harness hooks (
games/outbreak/src/feature-registry.ts,outbreakClientFeatures): Browser/client registry where each feature folder (src/{discovery,hordes,map,melee,pacing,progression,vehicles,weather}/index.ts, listed once insrc/features.ts) registers itssettingsGroup,labControls,onSettingChanged,gameCommands,harnessMethods,harnessState,feel(reset,processFrame), andnetSession(onApplyWorld,onReconcileLocal,onSyncFrame) hooks. A feature'sindex.tsonly registers; its scenes, controllers and Lab hosts live in domain files beside it (stage.ts,lab.ts,controller.ts,map.ts) that other modules import. The central modules never import the registry: each declares the hooks it runs (OutbreakLabFeatures,OutbreakCommandFeatures,OutbreakHarnessFeatures,OutbreakFeelFeatures,NetSessionFeatures), the registry implements them, andmain.ts(or a test, throughsrc/features.ts) passes it in asfeatures. - Client loop, world, and scene modules (
games/outbreak/src/{loop,input,hud,world,scene}/):src/main.ts(391lines) is a thin composition root that wires WebGPU boot, session creation, Lab controls, commands, and the automation harness over six focused subsystem modules:src/loop/frame-pacer.ts(OutbreakFramePacer:FixedClock, GPUFrameTimer,PerfSampler, adaptive quality ticks, single-in-flightinstallFramePacer),src/input/controller.ts(OutbreakInputController: keyboard/mouse/wheel/gesture bindings, aim ray-to-ground projection, fixed-tick command building),src/hud/frame-ui.ts(OutbreakFrameUi: DOM status/prompt/minimap/inventory HUD and key-layout updates),src/world/scene-level.ts(OutbreakSceneLevel: authored origin build vs.WorldClientStreamer,Vision/SceneryMemory/CombinedFogMemory, cached light occluders/walls, instanced door and window sash batches),src/scene/frame-lighting.ts(OutbreakFrameLighting: per-frame light slots, frustum/roof lamp culling, polar vs. unshadowed lamp partitioning, sconces, table lamps,packOutbreakLightingUniforms, foliage actor bending,FRAME_FLOATSpacking), andsrc/scene/frame-renderer.ts(OutbreakFrameRenderer: camera/peek/lean follow, avatar/crowd/combat-FX/clutter/prop/weather/vehicle batch assembly, andrenderer.render).
Every tunable visual, lighting, AI, physics, weather, or gameplay feel parameter lives in a typed settings object grouped by subsystem (settings.lighting, settings.ai, settings.stamina, settings.flashlight, settings.ground, settings.zombies) and bound to LabPanel (@arcade/kit/lab-panel, drawn by @arcade/knobs).
- No look or feature query parameters: URL query parameters (
?room=,?seed=,?peaceful=1,?bench=1,?station=) are reserved for routing and automation. Visual and gameplay tunables belong inLabPanel. - Single write path and update boundaries: Pointer drags, keyboard scrubs, exact numeric entry, group resets, seeded dice rolls (
tuningSeed), and preset imports all route throughLabNavigation.setand controlchangecallbacks so live uniforms, spawn-time retriggers, and structural rebuilds fire consistently (lab.md). - Versioned preset migration: Exported presets and saved localStorage/URL recipes carry an explicit schema version and migrate legacy flat keys into subsystem namespaces before validation.
The standalone Lab (games/lab, served at /arcade/lab/) mounts the three-column workbench shell (Outliner, Details, Library drawer, top-bar transport, ⌘K palette, and Watch list) over a single WebGPU device (packages/kit/lab/).
- Game adapters (
games/lab/src/games/registry.ts): Each game exports lazy chunked adapters for its effects, materials, spawnable characters/vehicles/props, andLabAudioAdapter(games/<name>/src/lab/*). Adapters invoke the game's production simulation, mesh builders, shaders, and audio graph rather than duplicating a Lab-only approximation. - Stations (
games/lab/src/stations/*.ts): Saved demos that Kit returns to are defined withdefineStation({...})(lab.md). Opening a station via1–9/0,⌘K, orbun run arcade station <id>loads the target section, runs its setup commands, and populatesleft,right, anddockwidget slots (status,controlsknobs cards,variants,compare,perf,metrics,changes,notes,voice). PressingNrecords a Scrawl-style voice and drawing note that uploads keyframes, preset state, and Whisper transcription to the team board.
Every interactive page registers its actions in a LabCommandRegistry (@arcade/kit/lab-commands) and connects to /__arcade/control during bun run dev (@arcade/kit/dev-control, dev-control.md). UI buttons, keyboard shortcuts, the 8-sector radial menu, ⌘K search, single-tab Studio Tours (arcade tour), pinned notes (arcade note), and bun run arcade … CLI commands all invoke the same registered command handlers.
Every change is verified across four layers:
- Package and game checks:
bun run check:packagesandbun run --cwd games/<name> check(typecheck, unit tests, and production bundle smoke test). - Deterministic simulation & wire proofs: Seeded multi-tick simulation hashes (
prove.ts,coop.test.ts,bench-multiplayer.ts) and mocap pose-diff scripts (games/blackout/scripts/animation/). - Headless Metal browser proofs: Playwright scripts executed via
hivemind run --kind browser -- <cmd>verifyadapterInfo.vendor === "apple", zero console/page/WebGPU validation errors, offline audio graph equivalence (prove-game-audio.ts), station readiness (prove-stations.ts), and paired A/B GPU frame timestamps. - Clean export & Worker dry-run:
git archiveexport withbun install --frozen-lockfile, all checks, andbunx --no-install wrangler deploy --dry-runper game.
| Stage | Workstream | Problem | Target improvement |
|---|---|---|---|
| Stage 1 (PR #59) | Workstream B (engine-structure) |
docs/architecture.md described an unimplemented 2026-10-12 seven-package plan and 332-line packages/kit. |
Rewrote docs/architecture.md as the current engine map with an automated shared-module usage table (scripts/architecture-map.ts), and updated docs/README.md and docs/shared-code.md for Outbreak and Carrier. |
| Stage 2 (PR #62) | Workstream B (engine-structure) |
Flat merged settings bags in Outbreak (src/main.ts merged ~8 default objects via Object.assign, requiring walkSpeed vs aiWalkSpeed collision workarounds in shared/session.ts) and sibling games. |
Namespaced settings by subsystem (settings.lighting, settings.ai, settings.stamina, settings.flashlight, settings.ground, settings.zombies) with typed defaults, transparent LabPanel/knobs bindings, versioned preset migration, and zero collision workarounds. |
| Stage 3 (PR #70, PR #79, PR #96, PR #97, PR #98, PR #101) | Workstream B (engine-structure) |
Multi-thousand-line files where parallel agents collided (packages/kit/lab/workbench.ts, packages/kit/dev-control.ts, games/outbreak/src/main.ts, games/blackout/src/game/game.ts) plus per-frame/per-snapshot GC churn in NetSession and decodeShared. |
Split each god-file along existing seams into cohesive modules (workbench-{types,catalog,presets,grid,commands}.ts, dev-control/{types,commands,tours,screenshot,studio-ui}.ts, games/blackout/src/game/{sandbox-session,combat-fx}.ts, games/outbreak/src/{lab-controls,game-commands,feel-events,harness}.ts), pooled NetSession/decodeShared zombie lookup and snapshot pages, and documented the 8-way ECS vs. dense SoA shootout. |
| Stage 4 (PR #111, PR #112, PR #113, PR #114, PR #115) | Workstream B (engine-structure) |
Parallel Outbreak feature branches (melee, vehicles, pacing, progression, weather, map) repeatedly collided in shared/{coop-sim,net-protocol,session}.ts and src/{feel-events,game-commands,harness,lab-controls,net-session}.ts. |
Added OutbreakSharedFeatureRegistry (shared/feature-registry.ts, pure forkable per-CoopSim state + OutbreakNetSectionCodec) and OutbreakClientFeatureRegistry (src/feature-registry.ts), and migrated map, weather, pacing, progression, vehicles, and melee into self-registering feature folders (shared/<feature>/index.ts, src/<feature>/index.ts). |
| Stage 5 (PR #236, PR #237) | Workstream B (engine-structure) |
games/outbreak/src/main.ts grew back to 1,908 lines as the #1 merge-conflict hotspot (95 commits and 98 conflict events in 36 hours). |
Split games/outbreak/src/main.ts (1,908 → 391 lines, -79.5%) into deep subsystem modules (src/loop/frame-pacer.ts, src/input/controller.ts, src/hud/frame-ui.ts, src/world/scene-level.ts, src/scene/frame-lighting.ts, src/scene/frame-renderer.ts) while keeping 0 import cycles and identical harness.state() and visual output. |
| Render (PR #64, PR #69, PR #77, PR #82, PR #87, PR #91, PR #94) | Workstream A (engine-render) |
Duplicated WGSL snippets, uniform packing offsets, and effect renderers across Getaway, Carrier, Blackout, and Outbreak. | Converged wgslStruct/wgslSlot, per-variant Look specialization, @arcade/kit/gpu/{cull,targets,pass-graph}, DirectionalShadowPass, and documented the unified renderer roadmap in Section 4. |
| Follow-up | Shared server & net | Room Durable Object lifecycle boilerplate (throttled lobby reports, idle kick countdown, BUILD handshake) is still per-game; Getaway and Blackout have not yet migrated their client netcode to @arcade/kit/net/*. |
Extract shared Room DO lifecycle helpers and adopt @arcade/kit/net/* in Getaway and Blackout when their netcode is next touched. |