engine v5.2.9
Engine v5.2.9
July 29, 2026
A patch in the Lume line.
Saves stop storming, cities stop blinking, Safari plays
what's new
- Saving a script in a world that rebuilds its objects (city generators, wave spawners, destroy-and-respawn builders) no longer makes everything blink out and back over and over — rebuilt objects land once and stay.
- When the room does refuse to create something (flood limits, permission gates), your game's log now says so by name instead of the object just quietly never appearing.
- Models you place while they're still being prepared now show up on their own once they're ready — no more invisible props that stay gone until everyone rejoins the room.
- When Savi checks the scene, a model that actually failed now reads as failed instead of "still generating" forever.
- Firefox players get your world instead of a black screen — a shader Firefox refused to compile is fixed, and every shader the engine generates is now checked against Firefox's rules before it can ship.
- Safari and iPhone players stop losing their session to "graphics device stopped responding" — a Safari bug the engine now works around automatically.
- Big outdoor worlds run roughly twice as fast on phones (one test iPhone went from ~15 to ~27fps), and phones get their own lighter bloom so glow effects stop eating the frame budget.
- Scattered objects (forests, rocks, crowds) now respect every setting you put on their template — most visibly
castShadow: false: turning shadows off on a scatter of 400 trees actually turns them off now, instead of silently ignoring you and paying the full shadow cost. - Savi got sharper at two things this build: kills in multiplayer combat feel instant for the shooter (no wait-for-server hitch in the death reaction), and floating-island worlds get true open sky below the horizon instead of a fake ground haze — she now reaches for both patterns on her own.
- When a texture script draws something but the texture comes out blank, Savi now gets told exactly that — instead of the engine calling it a success and leaving you staring at an untextured object wondering why.
- Fixed a runaway save loop: some games were silently saving new versions every second (one hit 2.6 saves per second, thousands of junk versions) — burning storage and making version history useless. Saves only happen when something actually changed now.
- Savi can now look at your game the way a phone player sees it — checking that menus and HUDs fit a small screen — instead of only ever seeing the desktop view.
- Side-facing walls (
facing: 'x') with off-center doors or windows now collide where the opening is drawn — no more invisible wall in a visible doorway with a secret walk-through spot mirrored across the wall's center. - Savi's 3D-model workshop got a box of professional modeling shortcuts — custom models come back with nicer edges and surface detail in one pass.
- Your game has a real database now — one save file per game, with your build room's data kept physically separate from live players' data. Savi writes ordinary SQL for save systems, load-at-join, save slots, shared world state, and analytics queries — and games that saved before the database keep every old save, carried over automatically.
- Misspell or invent an option on an effect sprite/ribbon/light and Savi now gets told exactly which word the engine doesn't know, with the list of words it does — instead of the effect quietly ignoring it and leaving everyone staring at a fireball that "should" have soft edges.
- Games with always-on regen, timers, or meters no longer flood their version history with a save every half-second — pure player-state ticks batch up quietly (real changes still save instantly, and nothing is lost if the room crashes).
- Ask for crouch in a first-person game and Savi now lowers the camera the way you'd expect, instead of tilting it toward the floor.
- Swapping or deleting sprite art mid-session no longer leaves the engine silently re-requesting the dead art every 10 seconds until someone reloads the room.
- Dragging a file from your computer onto the game view works now — the drop lands in the import overlay instead of vanishing into the game.
- Right after sending a voice message, tapping Tab opens the chat box again instead of doing nothing.
- Cold animation sheets no longer flash giant — the engine holds the whole last frame until the new sheet lands.
- Safari and iPhone players get another big speed-up: the engine stops using a rendering path Safari secretly rebuilds from scratch every frame, and three per-tick busywork loops that hit Safari's JavaScript engine hardest are gone entirely.
›technical notes
- Entity fences stop eating CREATE rows — the city-blink/anti-phase-rebuild class dies (ledger 1795, dig 8b34645c). A forwarded-DELETE entity fence no longer drops CREATE rows (updates/deletes/events stay fenced): a create row is a deliberate re-mint (ENTITY_NEW at the sender's log walker), never a pre-adoption straggler, and the admission path already owns same-id collisions. This deletes the wild loop's only receipted create-killer — prod 5.2.8 "Clog a Toilet" (incidents 119a08e8 → 8b34645c) churned a 123-tick anti-phase rebuild for the room's whole life: fence eats creates → never acked → 3s ghost retire → confirm walk fails → rebuild → next fence. Destroy/respawn builders now converge by construction.
create.nackon every create-drop class: every create row the StateDeltas drain DROPS now answers{kind:"create.nack", ids, reason}beside thecreate.ackpush — create-gate rejects, chain-validation unwinds (displaced-host included), flood-budget prefix drops, and the suspended-session wholesale drop. Client-side a nack fast-forwards the pending entry's grace (ghost retires on the next drain, not 3s later) and the client-ring receipt names the room's reason. No create can die silently — silence now means only "the message never arrived", which is what the grace window remains armed for.- Dead-parked model loads re-probe on a long cooldown — the silent forever-park is eliminated (ledger 1808, the Thunder Bluff latch, dig 22f87a4d). Models placed while their import was still cooking never transitioned to rendered after the asset completed: dead answers collected during the cook window burned the terminal budget into
retryAfter=INFINITY, and both documented re-arm doors are dead mid-session (manifest re-add dedupes at the sim AssetService; room reattach needs a reconnect). Dead-4xx/auth give-ups now park onLONG_RETRY_COOLDOWN_MS(one bounded re-probe per 5 minutes, autonomous via the retry wake) — any asset that becomes servable converges within one cooldown with zero external signal. INFINITY now belongs only to states with their own explicit door (pre-session cap, content-policy rejection). Room attach still re-arms immediately (ledger 1183 semantics preserved). - Eye honesty:
getUnreadySceneAssetsreads a dead 4xx answer as'failed', never'generating'— a cooking asset answers 202, so a dead answer is a verdict, not progress (401/403 stay'generating': auth verdicts the session cookie, not the asset). Savi's live-eye stops promising assets the engine had stopped requesting. - The upload-drop receipt rail's dedupe key drops its per-entity component (dash-named ids made one 80-entity sweep 80 warn buckets in one tick, evicting the asset-load receipts this incident needed): one line per (client, gate) per 30s window, every drop still counted in stats.
- Firefox-valid generated WGSL —
cullSelectWgslgains an explicit unreachable tail return inaggClaimso all five variants validate under naga (r-859dee09). Firefox's WebGPU validates shaders with naga where Chrome/Safari use Tint/Dawn; a Tint-accepts/naga-rejects shader parks the pipeline atcreateShaderModule, the canvas goes black, and telemetry reports only second-order resource-state noise (forensics: 152/153 victims of the affected family were Firefox, one a fresh starter world). The pulled vertex source also threads throughdisplacementWgslcorrectly. - CI gains the missing validation axis: a naga gate over lume's GENERATED WGSL (#11314). Every WGSL gate previously validated against Dawn only, so the whole Tint-accepts/naga-rejects family was structurally invisible. The gate enumerates the generators' output — every lume shader generator × its realistic parameter matrix, couplings taken from the engine's own derivation sites, 1,538 artifacts across culling, terrain, and post — and validates the exact strings prod builds pipelines from. The class can't come back unseen.
- Shipped Safari/WebKit renders again — fragment stub for vertex-only shadow bundles (WebKit bug 319980). Root cause of the Safari/iOS GPU session deaths ("graphics device stopped responding" on desktop, silent WebContent kills on iPhone): every lane shadow bundle pairs a vertex-only caster pipeline with a depth-only bundle encoder, and shipped WebKit's
RenderPipeline::validateRenderBundlerejects that spec-legal shape at setPipeline — the bundle records invalid every frame and the sustained validation storm survives both render-chain rebuilds. Fixed on WebKit main 2026-07-22 but months from shipping, so:probeFragmentlessBundleSupport()runs one throwaway vertex-only bundle at device init inside an error scope (a probe that cannot run never convicts), and when armed,laneShadowWgslgains an empty@fragment fn fs() {}bound with zero color targets — a construction-frozen session latch, same law as the sample-count/color-format contracts. - Phone texture-page mip admission — 15→27fps on iPhone (7.9× draw cut). The mobile texture profile (256² pages) refused every 512² world texture to the oversized direct pools, whose lanes can never activate pulled-attribute batching — phone-tier sessions drew the forest one indirect draw per unique geometry (1,333 draws vs 169 on desktop, ~18ms of empty-indirect Metal encoder overhead in scene.opaque). A 512² chain's mip 1 IS the 256² image, so admission now seats an oversize chain at its first fitting level and the residency copy reads the tail. iPhone receipts: pinned rung 0 1,320→156 draws, scene.opaque 19.6→5.6ms, GPU 38→25.8ms; unpinned natural ladder ~17→~27fps, and the frame-budget guard no longer parks the look (thermal ramp flattened). Tail admission is per-tier policy — desktop keeps native-res pools.
- Offset admission refuses off-block-grid base dims — the zoo 512×285 device-loss storm. Compressed formats require block-aligned dims at createTexture's BASE level; halving a block-aligned NPOT chain can land off-grid (2048×1140 ASTC at offset 2 = 512×285), minting an invalid texture that stormed every lane bundle binding it and killed the session with GPU device loss. Off-grid offsets are refused at admission.
- Bloom shape presets — full/lean/phone on the quality ladder. Bloom's 7-pass half-res 4-level pyramid becomes 'full'; 'lean' (half-res, 3 levels, 5-tap Kawase) and 'phone' (quarter-res, 3 levels) join it as a construction-frozen session contract. The post stack runs at OUTPUT resolution — TAAU internal-res scaling never protected the phone from bloom — so the phone preset is the first bloom cost cut that lands on device. A/B override:
globalThis.spawnBloomShapeOverride+ render-harness--bloom-shape. - Depth-local look fusion — haze rides the composite,
look.presentdies on hazed frames.classifyLookFusionthree-classes the active look (pixel-local / depth-local / offscreen); vocabulary haze (a fixed per-pixel depth tap) now fuses into the composite (zero new bindings on the TAAU-fused kernel), freeing the full-res rgba16float look intermediate on leaving two-hop. Standalone pass stays byte-identical as the offscreen path + A/B control arm. - Transient textures survive usage injection (F1) — zero validation errors at any creation time. WebGPU Inspector ORs COPY_SRC|TEXTURE_BINDING inside wrapped createTexture and Dawn rejects the merged mask (every scene.alpha submit failed under instrumentation). The pool probes once at first transient request (a 1×1 attachment-only create reveals the wrapper), degrades to non-memoryless up front, and keeps per-texture destroy-and-recreate as backstop for late-attaching wrappers.
- Scatter templates stop silently dropping
castShadow/receiveShadow(+14 more keys) — the hoist list is derived, not hand-maintained (incident 7fdfd766).SCATTER_TEMPLATE_PROPERTY_KEYSomitted 16 keys that were valid per the type (ScatterTemplateincludesPartial<ObjectProperties>) and silently vanished innormalizeTemplatePropertieswhen written top-level: attachment, pivot, visibleInFirstPerson, visibleTo, castShadow, receiveShadow, spline, mixer, ik, jiggle, collider2d, sign, fx, interpolation, npc, blueprint, brush. Incident app ca4e9c87: 403 scattered trees all read cast:true against templates declaringcastShadow:false— full sun-cascade cost for shadows the creator turned off. The hoist list is now DERIVED fromSCATTER_TEMPLATE_KEY_ROLES, a role map whosesatisfiesmakes classification exhaustive — a new ObjectSpec/ObjectProperties key is a tsc error until marked hoist/spec/dropped (with a reason comment for every drop). Spelling-parity tests cover every hoisted key. - Skills teaching wave (rides this build's skills.json): combat.md teaches predicted-kill presentation — death reactions project locally, authority reconciles (tide's f3a09a5f §3c, #11288); world-composition.md teaches the open-sky idiom — floating islands set
atmosphere.ground: false(two-creator demand in one week: Lightwake's 14 burned versions + izkimar; the cure itself ships since 5.2.8, the teaching now points at it); weaves.md teaches the structured inter-wisp handoff shape that parses at the seam (#11285); match-a-reference.md's three prompt clauses learn their true targets (#11338); drawn-art.md's cliff step gets a bounded budget — one doubling, then it's a camera-height problem with named levers (#11341). - Texture bakes ship a pixel-coverage receipt — a blank bake is a named outcome, never silent success (incident c2c60fea, ledger 1805). The bake worker reads an exact non-zero pixel census + strided sample hash off the finished canvas before
transferToImageBitmapand ships it with every ok bake; the host raisestexture-script-blank-bakewhen the census reads blank after a draw that ran ops. The blank bake still ships — the receipt is a name, never a gate. texture(ctx)returning a thenable is a runtime fault (texture(ctx) must be synchronous): the canvas is captured the moment texture() returns, so an async draw silently shipped only the ops before its first await.- New browser-gate suite bakes the incident probe through the REAL built worker artifact on chromium+webkit and asserts bitmap content pixel-exactly — the raster class vitest's recording stub structurally cannot see.
- STOP PAIR: the update storm (runaway version-minting) is cured at both ends (dig 535afdad, ledger 1797). Engine side reverts #11111's bidirectional singleplayer template fold: the fold judged live writes against a template mirror its own enqueue never advanced, so a per-tick state write on a promoted key re-enqueued every tick forever, and the coalescing persist sink minted one
game_specsversion per batch — Slime Arsenal (90469935) minted at 2.6 versions/second. Live state writes on template-carried keys are once again live-only (the one-directional promotion atpersist:truewrite time stays). Known trade, taken deliberately: #11111's resurrection cure re-opens — a template-carried key cleared by a plain live write re-seeds its stale template value on rejoin (the goblinjo class, dig 7ed7f11e) — until a storm-proof fold design lands. - Platform backstop (kiln-side, active for every room at kiln deploy regardless of engine pin): a mutations batch that changes nothing can never mint a
game_specsversion — the SDK mutations route serialize-compares the folded doc against the pre-apply head and answers the existing no-op shape (200,updated:false, head version) on equality, inserting nothing (prod receipt: Sky Islands 9bfc70a0 minted byte-identical v4740/v4741). The platform-wide floor for the empty-mint class, whatever the future writer. - Zoo: VAT Lab pen — vertex animation textures from existing primitives, zero engine changes (jure's spike, #11306). Three exhibits + the bake-on-display board (KeyX): soft (80-figure one-draw crowd wave), rigid (60-brick shatter with baked quaternion tumble + staggered time-reverse rebuild), and sim-sourced cloth (Verlet flag, crossfaded seamless loop) — texture scripts bake the data (RGBA8, alpha-255 law), bespoke geometry carries VAT addressing in uv, scripted materials replay it in the vertex stage (~287 KB total). Engine findings for the rendering lane, named: vertex-stage texture sampling works today; scripted materials draw standalone (merged-mesh crowds cap at the 16,384-vert bespoke budget — instancing is the scale-up ask); positionNode does not reach shadows (exhibits set castShadow:false); culling uses rest bounds (envelope markers).
view_player_screenacceptsviewport: { width, height }— the schema catches up to the booth (r-c853e0d1 item 1a). The tool's inputSchema wasz.object({})while the booth underneath already honored viewport end-to-end (resolveViewportclamps 64–4096, warm eyes resize per job) — Savi could not ask for the phone-sized look her own craft clause ("the HUD fits a thumb-wide screen") requires. Viewport rides the booth request verbatim; the caption names the served size from the booth's own receipt; bounds are mirrored at the tool boundary; a provider-level schema pin asserts onlyviewport {width, height}exists (a param the booth ignores would be a new lie). game-ui.md retires "asking the player — they see them; captures don't" (false since the composited eye landed): view_player_screen IS the capture that shows anchored plates.- facing:'x' wall colliders no longer mirror their holes across the wall's center (SHUDDER incident 4bbd28d3; born with the wall feature in #5529, survived all three renderer eras). The mesh (
createWallGeometry) builds the wall shape in XY and rotates it +π/2 about Y, sending an authoredhole.x=+8to local z=−8; the collider grid (generateWallCollider) wrote shape-x straight into +Z, landing the same hole at z=+8. Every facing:'x' wall with an off-center hole got both player-facing faults at once: an invisible wall at the drawn doorway and a walk-through phantom at the mirrored spot (2·hole.x apart). The collider now mirrors hole.x into the mesh's frame — creators authored five months of worlds against what renders, so the invisible side conforms. Centered holes (x=0) were mirror-invariant and are byte-identical before/after; facing:'z' and 'y' were already consistent and are pinned by new tests, including a mesh↔collider projected-occupancy parity sweep across all three facings. - Blender workshop: quickmenu's 10 real algorithms land with their teaching —
qm_applyone-call procedural detail (jure's harvest, #11335). The workshop image gainsqm.py(355 lines of real quickmenu ports) provisioned as a Blender addon, with the blender-game-assets skill teaching the one-callqm_applyshape so wisps reach for proven procedural detail (bevels, greebles, surface breakup) instead of hand-rolling operators. Workshop-image change rides its own image build; the engine build carries the skill teaching. - One save file per game+mode —
api.sqlis Savi's whole storage surface (the storage-rail deslop, #11259). Every game owns one SQLite database per mode, dev and live physically separate, andapi.sqlis the one verb: a tagged template whose interpolations bind as parameters, one call one atomic transaction (semicolons = ordered statements that commit or roll back together), errors throw, budgets fail loud (≤64 statements / ~250ms wall / ~10k rows / 4MB per call; size warn near 8GB, growth refused at 10GB with reads and cleanup DELETEs always working). Behavior-script awaits settle at tick boundaries so a load enters the sim atomically;run_scripthits the dev DB the moment it executes;scripts/db.js migrate(sql)runs under a door-held lease exactly once per (mode DB, spec version) — a throwing migrate never blocks play, it reports and re-runs at the next boot. The old rail is deleted, not wrapped:api.updateStorage, the lifecycle storage lane, the eightstorage:*builtin jobs, createSDKStorage, and the gsdk documents/CAS family are gone, with tombstones on every old name teaching the live verb. Terrain edits and room-state resume are re-planted on_spawn_chunks/_room_staterows (the 10MB doc cliff deleted); pre-database saves surface read-only inlegacy_kvvia an idempotent, crash-resumable boot importer. New data-and-saves skill replaces lifecycle-and-persistence + jobs-and-storage. - fx sink options the engine does not know now talk back instead of silently no-oping (the
soft:lesson —soft: 0.4depth fade was authored in the wild for months against enfeul's fireballs whilesprite()silently dropped the key; B5 landed the fade, this closes the class).sprite()/ribbon()/light()refuse unknown option keys with the builder's full vocabulary in the message (fx: unknown sprite() option "softness" — options are: texture, textures, blend, layerMask, align, stretch, axis, soft), and the compile issues rail (validateFxProgram) holds hand-built sink objects (render: { kind: "sprite", … }) to the same law: unknown sink keys, unknown sinkkinds, and out-of-law hand-builtsoftvalues (non-finite / negative) each report a path-addressed issue instead of a silent drop or a mid-validation crash. Builder throws andFxProgramErrorboth surface through the existing fx.compile fault channel (kept-last-program + one DM per broken version). - Singleplayer per-tick player-state persistence coalesces — the third door on the version-storm class (ledger 1814, dig ef0675cc). A behavior script writing player state every 30Hz tick (enfeul's NANO-WELD regen perk, +1.2 hull/sec) minted one FULL game_specs row per drain at the kiln RTT cap — ~115 rows/min × 775KB for as long as regen ran. The persist funnel now stamps kernel-authored singleplayer batches that are exclusively patchPlayer{state} writes as playerStateOnly; the write-ahead ledger merges stamped appends into the queue tail (latest-wins per key where merge-patch composition is provably exact against the fold, sequential otherwise) and holds a sole stamped head until 30s since the last player-state flush. Any structural batch behind the head lifts the hold immediately — order never bends — and /update freshness, idle save, and container shutdown force the flush. Crash-safe from append (the held fold is DO-durable and replays whole); only kiln VISIBILITY of pure player-state deltas lags ≤30s. Pinned: a 60s regen storm mints ≤2 rows (was ~115); unstamped batches drain byte-identically; god-mode hands, multiplayer worlds, and mixed batches are never stamped.
- Skills teaching additions (ride this build's skills.json): camera-first-person.md teaches crouch as an eye-height change on the FPS feature list — route
eyeHeight, never camera pitch (creator report ma1neac: Savi's first-try crouch tilted the camera; eyeHeight existed all along, invisible from her base context); combat.md's Predicted Kills section gains its premise line naming the current host-event NPC damage shape (an engine change to client-auth NPC intents rewrites the section in the same PR); debugging.md gains the measured-signature-is-a-grep-key line (dig f4510109: five mechanism fixes beaten every tick by a stale clamp the user's own number named twice). - Retired sprite art stops being asked for forever — the sprite-metadata derive queue prunes at consumption (ledger 1813, dump 9766d757). The host's derive queue had success-only exits: entity despawn and spec art-swap never pruned it, so a mid-session art retire kept paying derive fetches for retired art through the claimed-fetch seam and re-offered rail.assetMetadata every 10s into the relay's
unreferencedrefusal — forever (4 retired refs × ≥49min; only a reload cleared it). The sprite2d reference walk moves into ONE shared predicate (tome/sprite-texture-refs.ts) consulted at fetch launch and offer/landing — the queue is scheduling state, never a shadow reference list, so despawn and art-swap retire their asks the same sweep — and the relay'sunreferencedverdict now routes into hydration as a terminal door. Deliberately not #11334's give-up pole: still-referenced refs keep their ordinary retry ladder (pinned), and a retired ref re-queues on a fresh ladder from any new live reference. - Studio input pair (kiln-side — rides kiln deploys, live for every room regardless of engine pin). (1) File drags over the game canvas reach the page again (#11352): the kernel iframe is cross-origin, so a hovering drag routed every drag event into its document and the parent's AssetDropOverlayV4 went silent — drops over the canvas vanished into the game (thread 1785273583, dump e5cab07b). Kernel frames now go pointer-inert only while a
Filesdrag is in flight (DataTransfer.types gate keeps in-game pointer interactions untouched; a depth counter defeats dragenter/dragleave flicker), and every end-of-drag signal restores — drop, dragend, leave-to-outside, window blur, safety timeout, unmount. (2) A quick Tab tap right after a voice send toggles chat again (#11354): a released take now answers hasVoicedAudio() false, so the 0.5–6s settle window stops misreading the tap as TALK and swallowing the chat toggle (cal, dump ea53b5c2). - Cold animation sheets stop flashing giant — the texture-swap hold keeps the WHOLE last-good draw tuple (ledger 1817, #11370). A sheet-per-animation script authors texture+uvRect+size+anchor as one write, but applyComposedSprite's mid-fetch hold kept only the old texture while the new sheet's geometry applied immediately — first play of a cold sheet rendered the old sheet's texels stretched over the new-sized quad (~6× blowup) until the fetch landed. The hold now carries the entire last-good draw value and the existing subscribe re-applies the latest authored value whole when the texture arrives; geometry churn without a texture change never enters the hold (the warm spritesheet-frame path stays immediate, pinned by test).
- WebKit sessions bypass render bundles — the Safari/iOS wall collapse closes (#11337; WebKit RenderBundleEncoder.mm, deep-dive receipts in docs/renderer-gpu-deep-dive.md). Shipped WebKit marks any bundle carrying drawIndirect/drawIndexedIndirect as requiresCommandReplay and re-replays its commands into freshly constructed Metal indirect command buffers on EVERY executeBundles call — the AGX ICB construction pegged the GPU process's WebGPU work-queue thread (93.6% of its samples in replayCommands/makeICB) and the frame's present handshake queued behind it at p50 275ms. This is #8970's iPhone wall collapse, named. Every lume bundle draws indirect (cull args buffers), so on those builds bundles are pure overhead: the UA verdict
reusableIndirectBundles:false(bare-AppleWebKit desktop UAs + every iOS browser; not probeable via error scopes — a perf pathology, not a validation failure) drops the bundle profile and every contributor takes its existing direct-command path. No parallel mechanism; Chrome/Dawn keeps bundles. Riding the same hunt, three JSC per-tick churn eliminations (sim-side −63%): bind-group/layout cache keys become an interned-integer trie and canonicalJson dies (per-frame rope flatten + hash was a dominant JSC cost); ground sampling goes revision-driven with a per-foot IK memo (the per-frame terrain index rebuild could never survive a frame by construction); scripted-primitive ingest settles SILENT on a hook-fed dirty set (~4,800 resolveDeriveWork calls × 30Hz on a settled world → zero, pinned by silence tests).