# v2: simulation, physics and destruction The kernel (tick, determinism, replay, data layout), relevance, physics, destruction and agent tooling. Index: `agents/world-format-v2.md`. Schemas: `agents/v2-formats.md` sections 7, 8, 14. Built: `v2/sim/core/`, `v2/sim/physics/`, `v2/conformance/`; Rust port plan `agents/v2-m-rust-core.md`. ## Tick, determinism and replay - **Fixed simulation tick at 60 Hz** (proposed 2026-09-26 with the rate open; built at 60 Hz: the Heaton slice runs 36,000 ticks for 10 game-minutes) with render interpolation; the render rate is free. - **Every state change goes through a logged input/event stream**, with seeded RNG per system. - **A recording = seed + lockfile + input log.** One artifact serves bug reports, cinematics and photo mode, CI regression and desync bisection: the Source demo. Built: record/replay in the kernel; recordings still lack keyframes (open from goal 3). - Each system is marked **`deterministic | authoritative | cosmetic`**, which decides how it networks and whether it replays exactly. - **As built (kernel format 2, goal 2):** per-tick state hash in ascending entity-id order, layout-independent, cached per entry and proven equal to a full recompute; a 2,700-tick street run 2.25x faster in prod. Phases and manifests: `systems.md`. Content-lock changes need `node v2/conformance/generate.mjs --only heaton`. - **No JS `Math` transcendentals in simulation code:** `Math.sin`, `cos`, `tan`, `exp`, `log` and `pow` give different bits per engine (even Node 22's V8 vs Chromium 153's); only `sqrt` and basic arithmetic agree. Everything uses `v2/sim/clock/dmath.mjs` (a purity test greps for it). In v1, `car.js` uses `Math.tan` and the heli `Math.sin/cos`: port that feel through dmath, never directly. ### Rapier determinism (spike 2026-09-27, `tools/determinism/`, brief d) Scene: heightfield, car.js's ray-cast vehicle, 56 boxes it drives through, a spherical chain, a motorised revolute door, a prismatic slider, seeded impulses; 3,000 ticks at 60 Hz of scripted input; every body's pose, velocities and wheel state hashed each tick, `takeSnapshot()` bytes hashed every 100. All Rapier 0.20.0 (what the game vendors). | build | Node 22 vs Chromium 153 vs Firefox 155 vs WebKit 26.6, all 6 pairs | ms/tick, 67 bodies (Chromium / Firefox / WebKit) | ms/tick, 400-box stress | |---|---|---|---| | `rapier3d-compat` (v1 today) | identical, all 3,000 ticks and snapshots | 0.20 / 0.19 / 0.21 | 1.51 / 1.17 / 1.30 | | `rapier3d-deterministic-compat` | identical, all 3,000 ticks and snapshots | 0.21 / 0.21 / 0.22 | 1.08 / 1.19 / 1.34 | | `rapier3d-simd-compat` | identical, all 3,000 ticks and snapshots | 0.09 / 0.10 / 0.13 | 0.47 / 0.50 / 0.75 | | across builds | game = deterministic bit for bit; SIMD diverges from both at tick 1 | | | - A rerun in the same process is identical; `takeSnapshot` at tick 1500 restored into a fresh world finishes identical to the uninterrupted run to tick 3000 in every engine (the vehicle controller is not in the snapshot; rebuild it on restore). - Why: Wasm floats are fixed IEEE-754 and the browser build has no FMA, threads or relaxed SIMD, so one wasm binary is reproducible; the deterministic build adds the *guarantee* (pinned libm, no platform paths) and matches native. SIMD is 1.7x (WebKit) to 2.4x (Chromium, Firefox) faster but a different binary with different results. - **Verdict: input-only sync is viable for the Rapier layer** if every peer loads the same pinned wasm, same Rapier version, fixed 60 Hz, world built in the same order, no JS `Math` transcendentals, and one ARM run passes first. **Chosen:** `v2/sim/physics/` pins rapier3d-deterministic-compat 0.20.0 (the SIMD speed-up is forgone). - Not tested: ARM (Apple Silicon Safari, iOS/Android: owner check, `node tools/determinism/serve.mjs` then `/?build=det`, expect `a3e6aacdec0f0161`), real Safari rather than Playwright's Linux WebKit, native Rust Rapier (needs `enhanced-determinism`, the same version and a cross-check against wasm before it can referee), other Rapier versions. ## Data-oriented design - **ECS in the core:** entities are ids; components in dense arrays per type (structure of arrays); systems iterate the arrays. Cache friendly, trivially serialisable, exactly what the snapshot, network deltas, saves and mission layers want. The JS kernel uses sparse sets per component with swap-remove; the Rust port plan (goal 11) is **custom sparse sets, hecs as prior art only** (hecs's archetype order would change every golden hash), with dmath ported line for line. No Rust toolchain installed yet (owner's OK needed). - **The scene snapshot is a flat typed buffer** (ids, transforms, state flags) in a SharedArrayBuffer read by the render thread without copying; needs COOP/COEP headers (`rendering-and-audio.md`). Built: `v2/sim/core/snapshot.mjs`. - **Hot and cold data split:** per-frame data packed tight; rare data (names, registry metadata, mission text) looked up by id. - **Streaming by spatial chunk with a RAM budget**, budgeted per system and measured (v1's ~790 MB heap is the thing to beat). - **Data-driven over code-driven:** behaviour lives in catalogue entries, materials, I/O graphs and mission templates, so agents and modders change content without touching engine code. - **Simulation off the main thread:** into a worker first (v1: none, with GC pauses from ~790 MB of heap). ## One relevance and LOD policy Every entity gets a **relevance tier** (near / mid / far / dormant) computed from interest sources (players, cameras, missions). Every system reads it: rendering picks mesh LOD; physics steps dynamic, then kinematic, then asleep, then frozen; AI goes full, then cheap, then statistical; simulation and network rates drop with the tier; heat, water and fire grids only exist in near and mid. A per-system budget table (ms per frame, MB) is enforced by the benchmark harness; it is still to be measured. Prior art: Unreal's Significance Manager, GTA population budgets. Tier budgets per area: `npcs.md`, `networking.md`, `characters.md`. ## Physics - **Rapier stays** (Rust, WASM in the browser and native on the server): one simulation for client prediction and server authority (the `agents/light-engine.md` end state un-parked). - Rapier has experimental voxel colliders (v0.25+, no shape casts, no voxel-vs-heightfield, no auto mass) and no fracture. Destruction is ours: carve voxels, rebuild static colliders, spawn debris as ordinary convex bodies. - Needs: many dynamic props (sleeping, islands), welds/ropes/constraints for building, vehicle deformation later, destructible colliders. Vehicles keep Rapier for the chassis only (`vehicles.md`). - **Moving reference frames (decided want):** standing in a moving van, a Metro carriage, a bus or a boat must just work. Anything inside a vehicle's volume is simulated in the vehicle's local frame (a "grid", as Space Engineers does it: server authority plus client-predicted grids, GDC 2023), so props slide when the van brakes instead of jittering or falling through the floor, and characters inherit the platform's motion. - Entities inside a vehicle replicate in vehicle-local coordinates (no jitter on other screens); entering or leaving the volume re-parents between frames. - Rapier's kinematic character controller can dip through a vertically moving platform (dimforge/rapier#488, open): add the platform's velocity ourselves. - Balance animation reads the frame's acceleration (`characters.md`). ## Destruction Teardown is the reference. Destruction is in the world format from the start, because it changes what a building *is*. - Buildings and props carry a **destructible representation** beside the render mesh: a voxel volume per material (brick, sandstone, glass, timber). Voxels are the lean; pre-fractured chunks remain the alternative (open question in the index). - Destruction state is an overlay on the session/world layer, keyed by registry id, so it can be networked, persisted (T2, `networking.md`) or reset (shared world on a timer; private: your call). - **It must not look like voxels.** Intact buildings render their authored/pipeline mesh; the voxels are only the destruction proxy. Damaged regions are meshed with a smooth surface extractor (surface nets, or dual contouring to keep sharp brick courses and corners) at small voxel sizes, textured triplanar. Rubble is smoothed chunks, not cubes. - Materials carry break behaviour (strength, debris type, sound). - Destruction is server-authoritative; debris is cosmetic-local unless it matters. The navmesh rebuilds dirty tiles identically on every peer (`npcs.md`). - Not started: the destruction prototype on one building. ## Agent tooling (the premise of "humans out of the loop") - **Headless client:** load a world and lockfile, step N ticks, dump snapshots (the scenario runners in `v2/sim/scenarios/` are the first). - **Typed introspection API:** query entities, components, the I/O graph and system budgets; fire inputs. The developer console and cvars are the same API; the forge's external-agent API is this plus `propose_ops` (`missions-and-forge.md`). - **Debug-view render passes:** navmesh, heat, power, ownership, relevance tier, water. - **Golden-snapshot tests** and **visual regression** by screenshot diff on the benchmark camera paths, reusing the splat-compare metric. - **Fail soft:** every system degrades a tier (WebGPU to WebGL2, a mod to disabled) and reports it, never white-screens.