# Shared memory and kit modules GripForge runs the logic of every kit as **one WebAssembly module per kit**, next to a core module (`kithost.core`). On the web they all live in **one memory**, and everything that moves every tick or every frame is read **where it was written**: no per-frame copy, no per-frame JSON, no thread waiting for another. This guide shows what that means when you write a kit. ## The model in one picture ``` one WebAssembly.Memory (a SharedArrayBuffer on a cross-origin isolated page) ├─ the core: owns the world, the one allocator (kithost.mem@1), the zone │ zone: INPUT | STATE | EVENTS | WORLD (one row per entity + declared columns) | REGIONS ├─ your kit (relocatable wasm, placed by the loader, linked by pointer) ├─ other kits, the physics module └─ the player and its presentation modules ``` - **One allocator.** Your kit's `#[global_allocator]` is the core's. Bytes handed between modules are owned and freed by whoever receives them; nothing is copied. - **The zone** is a block with a fixed layout, sized by the game schema. Entity rows hold the base pose (`core.position@1`, `core.rotation@1`) and every column that kits or the game declare, with read/write rights and write phases per module. - **Fixed layouts.** Structures shared in memory are `repr(C)`, declared once and versioned; the loader refuses a module built against another major, by name. ## Writing a kit Write your kit against the kit SDK (`gripforge-kit-guest`); `gripforge kit build ` produces both builds: the relocatable module for the common memory and the isolated one (`--mode common|isolated|both`). **Read and write columns in place:** ```rust let mana = kx.column(&id, "game.mana@1")?; // a few loads in the entity row, no call kx.set_column(&id, "game.mana@1", [mana.unwrap_or([0.0; 3])[0] - 1.0, 0.0, 0.0])?; ``` A write the game schema does not allow (wrong module, wrong phase) is an error that names your module and the column. **Publish state others read, instead of answering calls.** A provider that publishes columns lets its clients read them in place: ```rust if kx.published("combat.health", &["combat.health_hp@1", "combat.health_max@1"]) { // read two fields of the row: no service call } ``` **Ask the physics, do not wait for it.** Queries are orders with a ticket, answered about one tick later: ```rust let ticket = kx.physics().ask_move_character(&player, delta); // next ticks: if let Poll::Ready(moved) = kx.physics().poll_moved(ticket) { /* use it */ } ``` **Run heavy systems every N ticks** (`kx.schedule(system, Every::new(...))`), never every frame. Kit logic runs only at the game tick. ## Reading state from a page, a HUD or a tool The page reads the logic in place through `host.shared` and **subscribes** to changes. A subscription carries no data: it tells you something changed, you read again. ```js const shared = host.shared; const off = shared.subscribe(['var:player.hp', 'phase'], () => { render(shared.var('player.hp'), shared.phase); }, { everyTicks: 6, name: 'my.hud' }); shared.eachRow((row) => { /* row.id, row.x, row.y, row.z, row.yaw: views, not copies */ }); ``` `host.snapshot()`, `state.snapshot()` and `entities.all()` return JSON: use them for tools, saves and debugging on demand, never on a timer or per frame. ## Threads and fallbacks - Rendering and physics run on their own Workers by default. Threads never wait for each other: each one publishes its latest complete state (triple buffer, held frames with a sequence number) and orders go through lock-free rings. - Without a `SharedArrayBuffer` (no COOP/COEP headers) there is no copy-based Worker: rendering and physics run on the main thread and the reason is shown in the debug panel (F3: "Render: main (reason)", "Physics: main (reason)"). Navigation is the one exception: it keeps its Worker and exchanges rare queries by `postMessage`. - To compare, `?workers=0`, `?worker.render=0` or `?worker.physics=0` turn a Worker off. ## Allowed exceptions - Assets (models, meshes, textures, sounds) are loaded **once** per thread that needs them; a single copy at load time is fine (for instance reading a mesh once to build a collision box). - JSON is for rare exchanges: configuration, tools, debug reads on demand. - A third-party kit the project marks `isolated` keeps its own memory; each message is copied once each way. That is the price of isolation. ## Do / don't | Do | Don't | |---|---| | Keep kit state in declared columns | Rewrite a whole JSON component every tick | | Publish state as columns, check `kx.published` | Call a service per entity per tick | | Subscribe with `host.shared.subscribe` | Poll `host.snapshot()` on a timer | | Hand another thread an address, a length and a sequence | Post a copy of the frame to a Worker | | Send physics orders and poll tickets | Block a tick waiting for a Worker | ## Checking your kit - Open the debug panel (F3) or read the `gripforge.debug/1` snapshot: render and physics threads with their reason, milliseconds of logic per tick, frame timings. Quote those numbers, before and after, for any performance claim. - Loader counters on the page: `host.cost.wireCopiedBytes` and `host.cost.physicsCopiedBytes` stay at 0 per frame, `calls.memoryChoice` says which memory was chosen, `calls.threadFallbacks` lists modules kept on the main thread. ## Related guides - [Reading the kits' memory in place (structures)](guide:structs): the layouts, the generated accessors, `gfk_zone`. - [Asking the physics without waiting](guide:physics-async): orders, tickets, where the physics runs. - [The core loop of your game](guide:core-loop): the three clocks and the render and physics Workers. - [Debug inspector](guide:debug-inspector): F3 and the `gripforge.debug/1` snapshot.