Spawn
playmakespawnjam
⌘K
visitorclaim namesign in
sign up
playmake
spawn / aboutwhat we're building

pinned

start herewhat spawn isfaqfrequently asked questionsthe betthe spawn bet

updates

engine v5.2LumeJul 10, 2026engine v5.1ConnectionJun 18, 2026engine v5.0For RealJun 4, 2026engine v4.6AtelierJun 1, 2026engine v4.5Surface TensionMay 22, 2026engine v4.4SolidMay 15, 2026engine v4.3GroovyMay 13, 2026engine v4.2ContinuumMay 9, 2026engine v4.1FoundationsMay 4, 2026engine v0.1GenesisApr 29, 2026

pinned

what spawn isstart herefrequently asked questionsfaqthe spawn betthe bet

updates

Lumeengine v5.21mo agoConnectionengine v5.11mo agoFor Realengine v5.02mo agoAtelierengine v4.62mo agoSurface Tensionengine v4.52mo agoSolidengine v4.43mo agoGroovyengine v4.33mo agoContinuumengine v4.23mo agoFoundationsengine v4.13mo agoGenesisengine v0.13mo ago
← All posts
← All posts

engine v5.2.25

Engine v5.2.25

August 14, 2026

A patch in the Lume line.

what's new

  • Big worlds with lots of detailed scenery run noticeably smoother while you walk around — the engine now remembers collision shapes it has already built instead of rebuilding them from scratch every time objects stream in or toggle their physics.
  • Behind-the-scenes: when something goes wrong in a game, the team can now see exactly which engine version it happened on — fixes land faster.
  • Fixed a bug where things your scripts built could come back invisible after a server hiccup — and stay invisible no matter what. The world now heals them on its own, and re-running the script that built them fixes them too.
  • Savi now hears loudly when a script's changes applied live but nothing reached the saved game — instead of discovering it after a reload wipes the work.
›technical notes
  • Content-keyed collider shape cache — the trimesh BVH construction churn is gone (dig df45b868). 81% of rapier's wasm time in a dense-place walk was Bvh::rebuild_range_binned under createCollider (160–185ms/s sustained): spec-level physics none↔static toggling and AOI streaming re-create mesh colliders continuously, and rapier.js rebuilds the full trimesh BVH inside every createCollider call (desc.shape.intoRaw()), then throws it away. The new cache (engine/physics/rapier/shape-cache.ts) keys BUILT wasm shapes by the engine's stable content identity — bespoke/cook/terrain signature + exact realized scale + trimesh/hull lane — and shares one wasm SharedShape across every collider realized from the same geometry (an Arc refcount bump per collider, standard native-rapier shared-shape usage recovered through the JS glue). Colliders stay fully independent (pose/friction/groups/sensor live on the collider); eviction is LRU under byte+entry caps and restores stock rebuild-per-use behavior on the shared JS shape, so a freed entry can never invalidate a standing collider. Bench on the shipped package: 2,048-tri trimesh createCollider 1.21ms → 0.12ms (10.3×), 8,192-tri 3.97ms → 0.43ms (9.3×); the dig-shaped sustained churn frame drops 4.3×.
  • Every kernel log row now carries the booted engine identity (engineVersion + engineHash) — the DD engine-version facet, kernel/container half. The container-tier env seed (SPAWN_ENGINE_SEMVER/SPAWN_ENGINE_HASH → logger base context, ledger #421/#584) already existed; this closes the realms it never reached: the workerd engine isolate (a fresh realm with empty process.env) now seeds the pair from CONFIG.engine at EngineHostCore construction, RoomShell stamps its engine identity (x-spawn-engine-* headers, durable) onto every shell record instead of only the failure-line identity block, and the container host wires shell records through the kernel logger — which also stops hookServerConsole re-wrapping warn/error shell records into lines whose message is the first line's raw JSON. Logging only; no engine behavior change.
  • Journal-replay husks are no longer permanent (ledger 2241). A runtime-spawned entity restored from the relay journal materializes sim-complete but drawable-free (Draw* components derive only where the spawn path executes, then replicate) and stays invisible until its manager's onSpawn re-mints it. Two seams made a sick host lane (2239 residency family) freeze that state forever, and both are closed: (1) the deferred-onSpawn drain (tome/lifecycle.ts) DISCARDED a parked hook when its entity read remotely-simulated — but a zombie host row (named in the table, not proving) reads exactly like a live one, so the drain threw away the very re-mint it exists to deliver; the entry now WAITS and fires when authority lands on this seat (re-election, heal, migration). Deterministic-id spawns adopt live children in place and heal missing visuals, so a re-run after a genuine remote run converges instead of duplicating. (2) api.spawn()'s adopt-under-reclaim path returned with no writes for an unchanged child, so a script re-run — the one lever a creator reaches for — was a no-op on the very husks it should have healed; adopt-under-reclaim now heals visuals like any adopt, husk-gated (the spawn call declares a visual, the entity wears none) so live drift on drawing children stays untouched.
  • The nothing-persisted verdict — a persist:true run_script whose durable batch comes back empty now says so instead of silence (ledger 2244, dig 2ad10ad8). The chain had no honest failure path for persist-requested + world-mutated + empty-durable-batch: kiln's exec route skips its persist leg on an empty batch, studio-chat's fallback skips on empty mutations, and instance-place partition drops never reached droppedMutations — a relay-hosted exec once applied 18 live writes, persisted nothing, and told nobody (the creator burned a full session filing the "revert"). Three legs, one contract: (1) the exec engine's instance-place partition now reports through droppedMutations with a reason field ("instance-place" — the write landed on a session TomePlaceInstance row that dies on room recycle; "persistence-filter" — the existing dig-9b58cad7 class), wired kernel→wire→kiln→chat; (2) kiln's exec route stamps persistOutcome: "nothing-persisted" on persist + kernel-proved worldMutated + zero durable mutations; (3) run_script composes the same verdict from the underlying facts (old-kiln compat) into a structured field plus a note Savi cannot read as success, with per-reason copy naming where the writes actually live and what the durable fix is.
  • The behind-truth verdict field — a catchup-starved client now says so where Savi's instruments read (dig 439a5e6f, ledger 2242 fix 3). A relay client that knows it is behind the room's journal head (its welcome named checkpoint seq + head; its drain tracks the applied cursor) now stamps that one fact on both diagnostics read surfaces: the engine-bug client dump (behind block) and the authored-UI read receipts (look target:ui → receipts.behind, riding beside hud). The field carries behindSeqs (a stated LOWER bound — the head kept moving), behindSinceMs (the welcome instant; the join-time spec fetch delivered everything saved before it), behindForMs, and one honest sentence: edits saved after <t> have not arrived here — a delivery backlog, not a script or UI failure. Presentation floor so healthy joins stay silent: gap > 0 AND (≥100k seqs — ~50× the relay's own 2,048-row journal bound, ~86× under the 8.66M field specimen, so a fossil room reports on the FIRST look — or ≥30s of drain — a healthy join drains in seconds). Pull-only provider (runtime worker → wire session): costs nothing until an instrument reads. The field the fossil-room incident burned a surgical three-save Savi investigation for the lack of.
  • World reset re-mints behavior module instances — update-wired ears hear again after reset (r-7b0ec903). An explicit world reset drops every subscription on the rationale "producers run fresh", but a producer behind a module-scope once-guard (var wired = {} + api.on in update()) was exactly the one that did NOT run fresh: its module instance survived the content-keyed behaviorCache across reset, the latch stayed set, and the ear never re-armed — update-wired games went deaf after reset (red-pinned by the #12599 adversarial review, pins in #12602). Design call: reset = FRESH BOOT — module scope resets with the world. resetTomeWorld now sweeps the instance layer only (remintBehaviorModuleInstances): behaviorCache entries holding a compiled instance for the world's scripts are dropped, so the reset apply re-executes each factory into a fresh module. Every compile artifact stays warm by design — module artifacts, the function-factory memo (re-mint is factory re-execution, zero codegen, sealed workerd isolates untouched), source/dependency caches, and cached compile failures (no instance ⇒ no module state). The it.fails pin flipped green.
  • lume particle slab-bank refusals speak, and the FIFO stops starving later effects (#12401, the mechablade whole-effect vanish). A slab-bank allocation refusal now emits one console line + one fx-arena-refused diagnostics event per deferred stretch (placement re-arms; announce-once lifecycle), instead of silently dropping the effect. The pending-FIFO skip-scans: the head keeps first claim on its slab class but no longer blocks later effects that could place now — early greedy effects can no longer starve whole later effects out of existence. Under-pressure eviction ("make room") is deliberately out of scope — design fork in the PR body for jure's ruling.
  • worldPosition / worldFeetPosition — the world-frame position vocabulary (r-99ba97f9, BT dig fcdbe249 §2). feetPosition on a parented child is the documented LOCAL seat/part offset, so a script that computed a WORLD point and wrote it onto a parented rider was silently reinterpreted as local — BT's driver sat 17.5m from his own car in car-local space and the "invisible car" was one write. Both frames arrive as a bare {x,y,z}, so the bare write can't discriminate intent and flipping its meaning would break every authored assembly; the convert gets its own word instead: worldPosition means the WORLD point on every entity, parented or not. Writes convert world→local under the parent's current transform at write time (pivot fold and socket/part anchors included) and re-dispatch as a plain feetPosition write — mutation stream, spec mirror, and persistence only ever see the canonical local value. Reads compose parent ⊗ local on the fly. The two frames the conversion cannot honestly serve refuse with both options named (bone attachments — only the renderer resolves the animated bone frame; camera-attached subtrees — per-viewer pose), and a patch that re-parents and writes worldPosition in one bag refuses rather than guessing which parent was meant. Purely additive — no existing write changes meaning, so no engine-version gate.
  • api.raycast verdicts carry a terrain lane (BT dig fcdbe249 §3, sibling). { verdict: true } results gain verdict.terrain: boolean | null — the data-model answer the overlap probes already speak (does solid terrain intersect the ray's segment per the terrain samplers; null = unresident voxel data, "cannot know"). A null hit over real ground now says so: hit: null + terrain: true is "the ground exists and this cast could not see it" (terrain collider not resident in this runtime, or a blind context) — the fork between "collider absent" and "probe unusable" that cost Savi's repair session real time, made structural. Verdict-lane only; plain calls pay nothing.
  • Reset converges every realm to the document (r-16ed9c68, #12819). A plain reset's bump previously merge-applied an identical doc on non-authority clients — an empty diff — so client-side presentation ghosts (journal restates, orphaned children) stood through every reset. Reset bumps now ride replace: true, and on that transition non-authority realms sweep membership-carrying world content the incoming doc does not author (world-content-sweep.ts). Doc-authored entities are preserved by the replace apply's snapshot-restore branch. After a reset, the world IS the document.
  • Delete propagation: destroyed spec rows leave the doc. destroy() of a spec-declared id with no live entity early-returned without touching the document — the row survived every reset and boot, silently. Under a persistence context the row and its parent-linked subtree now leave the doc exactly like the live path. Bonus root: buildNextSpecWithRemovedObject missed the placeId:objectId qualified runtime form, so a qualified-form destroy edited nothing — fixed, live path included.
  • api.destroyObjects(ids[] | queryFilter) — bulk destroy with honest per-id verdicts (destroyed | not_found | refused+reason; live players refuse rather than filter silently). Each id runs the full single-destroy machinery — no parallel delete path. Kills the graveyard-purge chore class: hundreds of orphans is one call, and a no-op is a spoken verdict instead of silence.