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.12mo agoFor Realengine v5.02mo agoAtelierengine v4.62mo agoSurface Tensionengine v4.53mo agoSolidengine v4.43mo agoGroovyengine v4.33mo agoContinuumengine v4.23mo agoFoundationsengine v4.13mo agoGenesisengine v0.13mo ago
← All posts
← All posts

engine v5.2.32

Engine v5.2.32

August 20, 2026

A patch in the Lume line.

Ten cures in one: meshes reworked behind a hidden tab reappear, wire-spawned objects land for every joiner, custom-cursor games click again on desktop, multi-area games stop losing areas, shared UI library edits hot-reload, phantom version churn ends, momentary server hiccups stop reading as dead models, static-model shadows return on iPhone/iPad, big worlds stream in smoother, and creators can cap the frame rate.

what's new

  • Meshes reworked while your tab was hidden now reappear when you come back — big generated meshes edited behind a hidden tab could return solid but invisible until each one was nudged by hand; the engine now spends no retry budget while nothing is being drawn, re-requests everything parked the moment the tab returns, and tells Savi how many meshes are undelivered on a player's screen if any stay stuck.
  • Objects spawned over the wire show up for everyone, every time. Before, an object created through certain save paths could land in the game's saved data but never appear in the live room — invisible to you and every joiner until someone else happened to edit the world. Now the room builds it within a fraction of a second, no outside edit needed.
  • Fixed: custom cursor sprites no longer swallow mouse clicks. Games that replace the native cursor with an image (draggable=false, pointer-events:none) work again on desktop — clicks pass through to your buttons and your world exactly like they did on 5.2.21.
  • A model that hits a momentary server hiccup no longer reads as dead. The placeholder used to flip to "Couldn't spawn" — and Savi's eye to "a receipted death" — off one failed read, even when the asset was fully imported and serving; creators were told to regenerate or rename assets that were fine. Now the engine keeps probing through transient trouble, tells you exactly what it measured ("N reads have failed so far") and only declares an asset dead when the server itself confirmed it.
  • Shadows from static models come back on iPhone and iPad. On some Apple devices, props and buildings imported as unskinned 3D models silently stopped casting shadows (characters still did). They cast again.
  • Multi-area games stop losing areas: walking back into an area that had emptied out (a prologue street, a lobby, an arena) now reliably brings back everything you built there. Before, a busy room could bring the area back as an empty box until the room restarted.
  • Cap your game's frame rate — engine.graphics.maxFps caps how fast the renderer draws (great for heat, battery, and steady pacing on phones), and players can set their own lower cap in settings. Your game logic runs exactly the same; only the drawing rate changes.
  • Big worlds load in smoother. While a large game streams in, the renderer used to spend a chunk of every frame re-sorting the scene; that cost is ~10× smaller now, so the first seconds of a heavy world hitch less.
  • Editing a shared library file your game's UI uses now reliably updates the screen: before, if the room's update lane hiccuped, a lib-file edit could leave the old UI showing forever — while the diagnostics insisted everything was fine — until the tab reloaded.
›technical notes
  • Geometry-wait parks re-arm on renderer-pause exit (dig 5b3d86d5 — derrf's hidden-tab rework). Three moves on the #713/#983 wait tracker, deleting the "creator returns to invisible walls until someone hand-nudges each one" class: (1) the report clock FREEZES while the renderer is paused (BespokeGeometryWaitTracker.pause()/resume() on the visibility edges + the boot-hidden path) — throttled-flow browsers keep delivering ~1Hz hidden rAF ticks through the mesh handler's collect (which runs above the present gate), so the 10-attempt/~3-minute report budget burned into frames nobody drew while the sim worker was itself throttled too slow to heal anything; hiding pauses the clock, never rewinds it (the frame-budget guard's law). (2) Renderer-pause EXIT (tab return) is a re-arm edge: one unpark-all sweep gives every parked wait — budget-spent and sim-verdict alike — a fresh report budget, because a hidden window is exactly when reworks land unobserved (a plausibly-changed cause, the tracker's own doctrine); a wrongly re-armed unhealable wait re-parks within ~one report cycle (ecs-sync re-classifies per report and re-sends the verdict at-least-once). (3) The parked population gets a getLogs face: a park episode raises one aggregated bespoke-geometry-parked diagnostic ("N generated mesh(es) are parked undelivered on this player's screen", full roster in data) — previously the budget-exhaustion park was console-only on the affected player's machine, reachable solely by per-entity identify_object interrogation, which is why it read as "the stale copy is served from spawn's side".
  • An accepted god-wire spawn's live half is completed at the fold seam (dig 2431 — the #12074 mirror; 7 field specimens plus the multiwriter net's catch). The container room folds an accepted forwarded SpecMutations batch doc-only — foldSpecMutationsIntoWorld advances and persists the doc, deliberately running no applySpec, because the contract assumes the sender's simulating world uploads the created entities via StateDeltas. A thin wire client (anything that writes the wire without a simulating world — the harness gods, and any thin sender) never does, and every applySpec trigger downstream is foreign-gated (kiln skips echo-poking the author room, the self-echo stamp guard skips consecutive self-persists, and every doc-level compare is structurally blind to world↔doc divergence since the fold already advanced the doc) — so a doc-present/world-missing object stayed invisible to every joiner until the next foreign-authored write or a room recycle, unboundedly when neither came. Mirroring reapForwardedDestroyTargets ("the seam that authorized the destroy finishes it", row 2005/#12074): after fold+persist, every accepted spawn still missing a live entity after the same-flush grace (SPEC_MUTATION_SOURCE_GRACE_TICKS — a real sender's create row lands at preUpdate, before the −150 ingest, so real clients no-op) is BUILT by the server via a scheduled real applySpec of the folded head, whose reconcile spawns every doc-present/world-missing row — the exact cascade a foreign write used to deliver by accident. Idempotent both directions: entity exists → the hold resolves silently; server builds first → a late create row lands in the existing create-collision class (owner-idempotent update / host reparent). The invariant restored: a version the room stamps as self-authored is fully represented in its world.
  • draggable="false" no longer reads as an interactive surface (ledger 2527 — Idle Homestead's dead desktop clicks on 5.2.30/.31). INTERACTIVE_SELECTORS matched bare [draggable] by attribute presence, so the ubiquitous anti-drag idiom on sprite/cursor images — an element declaring itself LESS interactive — entered the pointer-events grant walk. Since row 2437's whole-body 500ms rescue cadence (tap-actions INTERACTIVE_GRANT_SCAN_INTERVAL_MS) that walk reaches body-mounted elements, and makeElementInteractive stomped the game's mouse-tracking cursor sprite (<img draggable=false style="pointer-events:none"> on document.body) to pointer-events:auto — an interceptor glued under the pointer hotspot by construction, eating EVERY mouse press in the game (authored buttons and world taps both) while HUD, state, frames, and the control grant all stayed healthy. The selector is now [draggable="true"] — the only drag-positive form of the enumerated attribute, and already the engine's own vocabulary (frame/styles.ts exempts exactly img[draggable="true"] from -webkit-user-drag:none). Drag-interactive elements keep their grant; anti-drag sprites keep their authored pointer-events. The dead-link dim inherits the same narrowing (cursor sprites no longer dim as "controls").
  • A model's death certificate must be earned (ledger 2395, dig abb254af — iz's wardrobe imported clean in 21s and was SERVING while the session read it terminally dead). The model lane minted terminal verdicts off ONE read-side transient: a single 502-without-Retry-After or a lone dead 4xx flipped the placard to "dead" (modelLoadVerdict) and put the asset in the eye's failed bucket, whose caption dressed it as "a receipted death, not a guess" — the confession instrument overclaiming (ledger 2306's verdict-honesty family, inverted face). The retry ladder was already the N-spaced-re-probes machinery; the defect was verdicts minted at attempt 1. Now "dead" requires earned evidence: the server's own fingerprinted terminal cook-kill tombstone (terminalParked), or a dead 4xx that answered EVERY spaced re-probe across the give-up window (deadParked — 10 dead answers over ~5 minutes). Anything short of that stays "waiting": the ladder keeps probing and a serving asset heals it on the next read. The unready atom names its evidence class (failedFact: "server-verdict" | "read-failure", plus failedAttempts — the measured N), and the scene-view note tells only what was measured: read-failure atoms get "could not be READ from the asset host — N reads have failed so far: a measured fetch failure, NOT the server's verdict on the asset … do NOT regenerate or rename", while the receipted-death/rename teaching is reserved for server-verdict atoms. Statuses are unchanged (no settle-gate shifts); the 2306 temporary split (retryInMs) is untouched. The attempt-5 retry-ladder diagnostic rides the same honesty (codex review): at that point no verdict can be earned, so it now teaches a measured read failure — loading placeholder, engine keeps retrying, "do NOT regenerate or rename it off this alone" — instead of claiming a "Couldn't spawn" placard and prescribing regeneration against the scene note's own words. When the read-failure bucket holds more than one asset, each name carries its own measured failed-read count (codex review: one shared maximum was a false sentence for every asset below it).
  • Caster/mask model WGSL compiles on the gpu's latched survivor arm (dig row 2560 — filing d5efc458, app d8a0fb4a on 5.2.31). getModelShadowPipeline keyed models/shadow/static/depth16unorm/spull and declared the storage arm's layouts, but built the WGSL with the builder's DEFAULT vertex arm — which consumes @location(6) lumeSurvivorAttr: u32 with no attribute behind it. The GPU refuses the pipeline (WebKit: "vertex descriptor creation failed !matchesFormat(attribute(6)…)"), three attempts park it, and every static-model caster draw on a convicted device silently skips — shadows from unskinned GLB models missing while everything else renders. Only static-kind model content on the storage arm hits it (convicted WebKit/AGX devices with indirect-first-instance granted); getModelMaskPipeline carried the identical drift (no production caller today; fixed for consistency). Same commit fixes the draw-side twins that invalidated the encoder instead (rigid-child caster binding + identitySurvivorBuffer gaining STORAGE usage). Red-first class pin: survivor-arm.test.ts — for every pipeline the models family registers (3 kinds × color/caster/mask, both arms), every @location the compiled WGSL consumes must be provided by the descriptor's vertex layouts with a matching format class.
  • No-op spec patches never enter the persist pipe (ledger 2551). Every patch-class spec verb already computed a no-op verdict (specDocChanged / the effective-view compare) and used it to fire specPatchNoOpWarning — but called recordMutation unconditionally, so a value-identical patchTerrain (the specimen: a behavior re-patching terrain to its current value, ~1 row/24s for 8 hours, app 3ad02cb1) rode relay-spec-save into a kiln version mint whose only diff was the __appliedSpecSaveRowIds ring: version history churned for hours and Savi's eye tools went EYE-STALE seconds after any read. The verdict now gates the recorder at every site whose verdict baseline IS the durable fold's baseline — applyTerrainSpecWrite (patchTerrain + the material/mark/structure verbs), patchPlayer, patchCamera, patchInputs, patchGodMode, patchUi, patchEngine, patchRouting, updateBuyable, updateJob, updatePlace (including the keyed-map objects branch, which now skips value-identical row upserts exactly like the whole-list branch always did) — each counting through the ledger-1157 noteSuppressedNoopScriptMutation receipt. The warn still fires: the no-op is told, it just isn't persisted. patchAtmosphere stays deliberately UNGATED: its no-op verdict is judged on the session-merged view (base + overrides) while the durable fold applies the patch onto the head's base alone, so an override pre-populated by a behavior would make a tracked bake-in read as a no-op and starve spec.atmosphere forever; behaviors never reach its recorder (no behaviorPersistSourceId), so no tick-loop churn reopens, and the kiln route floors its value-identical mints on the correct baseline. The kiln half (relay spec-save route: mutations mode gets the no-op refusal mirroring doc mode's identicalToHead — an all-no-op batch answers applied: 0 with no ring append and no mint, while one real change in a batch of no-ops still mints once) ships with the next web promote, giving older engine bundles the same floor server-side.
  • The applied-generation record captures its residency/instance keys at expansion time — place re-entry survives interleaved applies (ledger 2145 leg-1 cure defeat, dump a812611d — Alliance of Darkness's black-box prologue). #12599's cure broke the delta-0 binding at unload (place-cleanup drops TomeAppliedGenerationResource), but any later apply re-records it — and the record's top-level residencyKey/instancePlacesKey were recomputed at RECORD-WRITE time while every other capture (the derive-gate inputs, the record's own expansion field) reads expansion time. A hook running inside that apply — onSpawn → api.enterPlace, the prologue session-script idiom routing every joiner — marks the unloaded place resident AFTER expansion skipped it, so the completed apply stamped a key claiming a place it never expanded: the parked placeResident re-apply (the re-entry's only materialization trigger) then hit the delta-0 short-circuit and the place came back permanently empty — residency marked, api.spawn landing without a not-resident warn, authored skeleton/state defaults/onSpawn all absent. The record now stores expansionResidencyKey/expansionInstancePlacesKey: mid-apply residency drift reads as a delta-0 veto, the drain full-passes, and the reconcile's missing-entity spawn materializes exactly what expansion skipped. A re-entry that accepts a traveler materializes the place. Pinned red-first in place-reentry-interleaved-apply.test.ts (the mid-apply defeat shape plus the unload→interleave→re-enter orderings).
  • Render frame-rate cap primitive (creator ask r-8691b70e — Sangre y Vitral on an Adreno 619 at ~82ms/frame). engine.graphics.maxFps (integer 5..240, live on patch, null clears) is a ceiling the renderer paces presents to: a deadline pacer (renderer/frame-rate-cap.ts) gates the frame body exactly like the governor's 120 Hz half-rate pacing — steady spacing by schedule-anchored deadlines, stall re-anchor instead of catch-up bursts, sim tick untouched (determinism untouched). It min-composes with the player's own explicit cap (new resolved player-settings lane perf.maxFps, sanitizer-clamped 5..240; boot seed on the renderer init's playerPerf snapshot, live via set-player-max-fps — players may always go lower, never higher), and the quality governor's judged refresh target folds the same cap (setFrameRateCap, the maxRefreshTargetHz min): a 30-capped session is budgeted against 33.3ms — quality sheds only when the device can't make the CAPPED rate, up-probes stay live, and the capped cadence can never read as the environmental-throttle signature. Spec rail is the renderResolution grammar end-to-end: zod contract in tome-schemas, spec-sync → draw/frame-rate-cap client-plane singleton → renderer handler; the in-game FPS-slider menu itself stays the creator's script — the primitive is the whole engine surface.
  • The synthetic-transform ordered rebuild buckets by depth (#13264 — the streaming-load sort term). getOrderedRecords re-sorted ALL records whenever orderDirty, with recordDepth walking the parent chain inside the sort comparator — O(N log N × depth) per streaming frame (504ms of an 11.2s load capture vs 39ms settled, renderer worker). Depth is bounded by MAX_PARENT_DEPTH, so the rebuild now buckets records by one O(depth) walk each — no comparator at all — and the ingest touched-sort memoizes depth into a scratch map before sorting. Ordering is unchanged: stable within a depth by map iteration order, parents still before children. Bench (scripts/bench-synthetic-order-sort.ts, 10k records in chains 3-5 deep, 120Hz frames): dirty-sort term 5.68 → 0.51 ms/frame; the settled path is untouched (early return unchanged).
  • The HUD stale-compile heal and its receipt key on the whole require() closure, not the entry source alone (ledger 2426 face 3 — the lib-dep hot-reload miss). Row 2186's heal re-keys the game HUD's compile slot on the live doc's ui.render source and recompiles when the serving graph's uiSource lags it — but a save to a scripts/lib/*.js module the entry requires moves NO byte of the entry, so under a stalled apply lane the guard read converged, the compile receipt pinned sinceMs at mount asserting health, and the HUD deduped the old panel forever (Apartment 6B 26d489f5: three saves to scripts/lib/ui-directory.js under an unchanged ui.js, 52k+ dedups on one frozen sig, compile ok). CompiledSpec now stamps uiDependencySignature (the tracked dep closure's per-source signature, from the same dependency sets the compile already records — a Map hit, never a re-walk), the controller compares it beside uiSource and heals through the same content-keyed cache, and the receipt's sourceHash folds the dep signature in so sinceMs re-stamps on lib edits too — the receipt can no longer assert health while a dep-shaped edit serves stale. Graphs minted before the field existed read as "unknown — never heal on deps alone", so no healthy mount confesses a stall that never was. Pinned red-first in worker-controller.test.ts (the lib-dep stall shape plus the pre-field no-spurious-heal control).