engine v5.2.1
Engine v5.2.1
July 14, 2026
A patch in the Lume line.
what's new
- Growing black patches are gone. On some machines, dark blotches could appear on screen (worst at night) and spread until they swallowed the whole view — reloading didn't help. Fixed at the source, for good.
- The realistic night sky no longer shows irregular black patches after dark — stars and the Milky Way shine everywhere they should.
- Walls, floors, and objects with realistic (PBR) textures stopped showing up after the 5.2 update — they render again, textures and all.
- Scripts that do heavy work in bursts (every few ticks) used to slip past the safety net and could grind your game to a halt until the tab crashed. The engine now catches them within seconds and pauses them with advice that actually fixes the problem.
- If the game picture ever freezes while the game itself keeps running underneath, you now get a clear "reload" prompt within seconds — no more silently stuck screens you have to diagnose yourself.
- The volume slider works every time now — it applies immediately to all game sound, including sounds already playing and music or videos inside game menus.
- Two looks that quietly changed in 5.2 are back to how you authored them: glowing objects (suns, halos) can shine through fog again when you told them to, and see-through cutout surfaces once again hide what's behind them.
- Models no longer get stuck as placeholder rings after a room hiccup — when the connection comes back, everything finishes loading on its own.
- Ask Savi for a "free cursor plus hold right-click to look around" camera and she gets it right now.
- Right-click selection works on every object again, whatever material it wears — objects with custom scripted looks had stopped being selectable in build mode.
- Savi can now convert a world you've already built to a different starter kind (like 2D ↔ 3D) — you see exactly what would change before agreeing, your creations survive, and any starter scripts you'd edited are parked for reference instead of deleted.
- "Fires once" effects — fire-pit coal beds, one-shot bursts — no longer vanish permanently for players who joined while the world was still warming up.
- Built-in rain, sparks, and fireworks read as real motion streaks now instead of a wall of tilted cards.
- Converting a 2D tile place back to heightmap terrain no longer flattens the ground to zero wherever tiles had been painted.
- Pressing interact no longer triggers an action twice when a key and a controller or touch gesture share the same binding.
- Cliffs and steep terrain show their real rock strata again instead of vertically stretched streaks.
- The blur effect works again — death flashes, dash juice, and scripted blur looks that went quietly sharp in 5.2 are back.
- Big lakes no longer draw over nearby bushes and shore objects at certain camera angles.
- Scripted looks on model parts and decoration layers now survive save and reload instead of silently reverting.
- Slash and shockwave effects no longer flash a black wedge from certain angles.
- Objects anchored to terrain stop visibly bouncing on other players' screens while terrain is edited live.
- Name tags and floating text stay crisp while whatever they're attached to runs around — no more smeared, doubled-up labels on moving players.
- Savi no longer gets falsely told a texture script "was not found" after you switch objects off it — retired paint scripts step aside quietly, keeping whatever they last drew on screen.
- Switching between games in the same tab no longer wedges the world when the previous session was a live/play session — the engine cleanly resets to the new game instead of refusing its updates.
- Camera scripts that set their own yaw/pitch every tick — locking the camera on your killer during a death animation, cinematic snaps, forced framing — now actually control the screen again in mouse-look games. The engine used to keep following the mouse and silently ignore what the script wrote; now the script wins the moment it disagrees, and normal mouse look stays as smooth as before.
- When your game's renderer dies mid-session — including the "GPU error storm" way, where the screen silently freezes while everything else looks alive — you now get an honest reload prompt within about a minute instead of never, and Savi is told your screen is frozen so she stops narrating changes you can't see and suggests a refresh instead.
- Worlds heavy on foliage, fences, grates, and other see-through-cutout surfaces render faster — especially on phones and tablets — with zero visual change.
- Fixed a family of silent failures when addressing objects by the wrong name: deleting or editing an object that doesn't exist now says so (and if you used a list position like "5", the message names the object actually sitting there); reading the spec by list position tells you the object's real id; and objects attached to the camera (HUD elements, music emitters) now show up when you list objects by tag.
- Fixed a bug where a player whose tab sat hidden through host changes (or who was mid-session when the room restarted) could get permanently out of sync — ghost objects only they could see, builds half-applied — until they manually reloaded. The server now detects the stuck session and quietly re-syncs it within seconds; nobody has to leave and rejoin.
- Rewriting painted fields through spec edits (or cancelling a brush stroke) now updates everything that reads them at once: the terrain mesh, NPC pathing, terrain-anchored props, scatter beds and their footprint ribbon, instanced scatter, and painted foliage density all pick up the new paint immediately instead of keeping the old state until something else changed.
- Some iPads and phones were being drawn blurry and sparse for no reason: when the browser itself limits how fast frames arrive, the engine kept cutting visual quality trying to fix a slowdown the graphics weren't causing. It now recognizes that situation, gives the quality back, and keeps the game exactly as smooth as the browser allows.
- When a device limits its own frame rate (Low Power Mode, battery saver, browser throttling), the player now sees one small note saying so, and Savi gets told it's the device's environment — not the game — so neither of you burns an evening debugging content that was never the problem.
- Fixed a shimmer where nested see-through objects (like layered translucent shells around one center) rapidly flickered over each other once they split into multiple materials — the draw order between such layers is now steady frame to frame.
- Fixed a bug where a mod's on-screen HUD (buttons, sliders) stopped working after installing the mod into another game, even though it worked fine in the mod's own dev game — HUD button presses now reach the mod's scripts in fresh installs.
- NPCs now see the hills you sculpt: units path over (and get blocked by) god-mode-raised terrain instead of walking through it at ground level, and re-route as soon as you keep sculpting.
- Fixed a bug where a look pushed by a script (fog, murk, color grades via
pushLook) could permanently fail to appear — with a wrong "script not found" error — even though the script file existed. Look edits now show up, and the error only appears when the script really doesn't exist. - Games whose scripts persist changes every tick — spawning crops, saving building edits, updating persistent object properties — no longer freeze for seconds when their world re-syncs: a re-apply of content the game already has now takes ~2ms instead of re-processing every object, and the save-echo that used to push a whole new world revision to every player is recognized and suppressed.
- Pixel-art sprites no longer shimmer or crawl at fractional camera zooms — texels stay crisp and stable while the camera pans, at every zoom level instead of only exact integer scales.
- A leftover broken terrain-mark entry in your game's data can no longer crash the whole server over and over (it used to force restarts and spam error DMs). The engine now skips the broken entry, keeps your rivers, ponds, and roads working, and logs which entry to delete — and the way it used to sneak into your game (deleting a terrain mark that didn't exist) is fixed at the source.
- Savi now finds out when one of your game's textures fails to load or draw on players' screens — before, those failures only appeared in the player's browser console, so she could confidently ship a broken visual without knowing. She also stops promising that a script interrupted by a hidden game tab will run by itself later: she'll tell you plainly the change hasn't happened and ask you to keep the game on screen so she can retry.
- Tiles your game's scripts dig, sow, or paint now stick in multiplayer: they reach the server and every other player, survive rejoins and host changes, and save with the world — no more garden beds wiped back to grass mid-session.
- 2D tilemaps no longer show faint vertical or horizontal seam lines at certain zoom levels — tile edges stay clean at every camera zoom instead of occasionally sampling a sliver of the neighboring tile in the tileset.
- Fixed a bug where leaving a testing/monitoring client alone in a multiplayer room could freeze the game world and make Savi's script runs time out — such clients are no longer put in charge of simulating the world, and rooms recover the moment a real player walks in.
- Fixed props with custom-shaped colliders sometimes losing their real collision shape for a whole session (players could clip into trees, rocks, and vehicles that looked solid). The engine now recovers the real shape instead of getting stuck retrying a dead-end fetch.
- Physics objects that end up turned exactly halfway around (180°) no longer read back facing the wrong way.
Known issues (being worked on staging right now — this version stays in testing until the first one is fixed):
- Houses and other multi-part builds can visibly wobble between two positions. This one blocks the release: the version won't go out while it's happening.
- Fast-moving flat sprites (2.5D characters and props) can look blurry or ghosted in motion — a top-priority fix is underway in its own thread.
- Some floating text can still smear while moving (name tags over players are already fixed in this version; the rest is in progress).
- On flat worlds you can sometimes see where the ground stops being drawn at the far horizon — a known design limit being redesigned.
- Worlds with lots of sounds recover automatically instead of going silent.
- Grass and ground cover now reach the horizon on desktop.
- Distant grass fills the horizon better on flat worlds: blades near the edge of the grass field stand a little taller, so the field covers the bare ground beyond it instead of stopping in a visible line.
- World labels stay sharper and steadier while the camera or their parent moves, and tiny distant text fades away cleanly instead of turning into shimmer.
- In 2D worlds, text can use
sortingLayerandsortingOrderto sit between sprite layers, while ordinary signs and unlayered labels still obey walls and other foreground geometry. - Reverted the camera divergence-adoption gate from this release — it caused a stutter/jump-back while rotating in games with custom camera scripts. Custom camera scripts that write yaw/pitch every tick return in a later release with an explicit authority signal.
- Layered flat effects (cracks, decals, panels) now stack in the right order from every camera angle.
- Fixed brand-new worlds crashing on their first load when dense ground decoration was enabled.
›technical notes
- Killed the TAAU history NaN-poisoning class (ledger 1195): one non-finite scene texel entered the temporal resolve, was written to history, and the Catmull-Rom resample + neighborhood taps spread it ~2 texels/frame until it consumed the canvas — reload-immune, since it re-seeded deterministically. The resolve (
lume/post/taau.ts) now scrubs non-finite values at all three feedback boundaries: non-finite scene taps carry zero weight in the 3×3 filter and the moments, a non-finite history resample collapses to the current frame's filtered color, and nothing non-finite is ever written to history (branchless self-equality selects + a 65504 ceiling for ∞ in rgba16float). - Hardened the known seed sites: all three wave-derived
normalize()calls inwater-wgsl.tsroute through a safe-normalize with an up-vector fallback, and the bloom prefilter (bloom.ts) stops relying on WGSLmax()NaN semantics — the vendor lottery where spec-faithful adapters propagate NaN and the "scrub" was a no-op. - Clamped five fBM remaps in the realistic-sky night stack to the [0, 1] domain the surrounding math assumes (3 in the night bake
sky-night.ts, 2 in the moon layersky-radiance.ts): 4–5-octave fractal noise swings past ±1, and feeding the remap into WGSLpow()is indeterminate for negative bases — NaN texels baked into the night sky texture blacked out everything at that view direction, stars included (ledger 1185). Day and twilight rendering unchanged; the out-of-domain texels become deterministic and finite on every adapter. pbr: truetextured primitives draw on lume again (ledger 1191):normalizeMaterialSpeccanonicalizespbr: trueinto a spentoverrides.pbrmarker, and the batch gate (SOLID_BATCH_OVERRIDE_KEYSinlume/primitives/store.ts) rejected the marker — recipe null, entity silently skipped, zero fetches. The marker is now accepted at the gate. The skip's diagnosticprimitives-material-unsupportedis allowlisted (it was emitted since the lume port but server-dropped), and apbr: truecube exhibit joined the zoo's primitives zone.- The behavior park watchdog gains a rolling-window trigger (ledger 1197): it still parks on 3 consecutive over-budget ticks, and now also parks when total over-budget excess across the last 3 seconds of ticks passes 20 budgets on ≥2 ticks that each overflowed their full tick interval. Duty-cycled heavy lanes (
if (tick % 3 === 0)around a 1.5s step) used to run unthrottled until Chrome killed the tab; they now park on their second heavy beat. A single spike of any magnitude never parks (one-time init stays safe), and sustained just-under-budget work bills nothing. - The watchdog's nudge text teaches total-budget pacing ("spreading the work out doesn't reduce the total — shrink it") instead of chunk-scheduling, which was coaching scripts into exactly the evasion the old consecutive-only trigger couldn't see. The
tome.behavior.budget_parkedtelemetry line now carries the trigger and window numbers. - Present-heartbeat watchdog (ledger 1186): a visible canvas whose frame loop stops posting its ~1Hz heartbeat for 30s while other worker messages still flow now fires the loading-state reload prompt, a
renderer-present-stallengine diagnostic for Savi, and a DD-countablerender_present_stallconsole error — the frozen-canvas-while-worker-alive class was previously invisible to every watchdog (they all ticked inside the dead render loop) and unlogged in prod. Fires at most once per session; hidden/occluded pages and never-stamped old bundles are never judged. - Frame-prep errors name their profiler step: the frame profiler keeps an always-on active-step stack, and the render-loop diagnostic's catch now carries
frameStep(stream/read-ops,handler/*,assets/tick, …) in the console line, creator-facing message, and data — the next caught frame-prep throw names its subsystem outright instead of needing a fleet dig. - User volume settings apply unconditionally (ledger 1187, standing since 5.1): bus gains were applied only by rewriting
buscommands inside liveaudio.updatelists, so when the prep loop swapped empty lists or stopped swapping, no volume change could land while already-playing WebAudio kept ringing at old gains.WebAudioRendererHandlegrowssetBusGain, and the renderer host subscribes the six bus gains directly to the user sound settings store — main-thread, worker-independent, applied at attach and on every change. The old command path stays; both compute gains from the samecomputeUserBusGains(). - Tome-UI DOM
<audio>/<video>elements now obey master volume (authored volume × master, via a MutationObserver indom-host.ts) — they previously played at element volume entirely outside the bus graph. Kiln's default master aligned to the engine's deliberate 0.35 (the slider showed 50% while sessions actually played at 0.35). - Restored two silent 5.1→5.2 scripted-material parity breaks (dig ac9a3f41), both "author's instruction silently ignored" class:
material.fog = falseis honored again:fogis realShadeMaterialrender state (the assignment used to land on a dead expando while assembly fogged every material unconditionally). The assembler compiles the fog mix out whenfog = false, and the bit rides the pipeline key — an additive sun in dense fog can no longer be swallowed to a silhouette.- Authored
depthWrite: trueon blended materials is honored whenalphaTest > 0(the cutout discard-then-depth-write idiom, e.g. dome shells occluding interiors) — exact 5.1 semantics for the cutout class. A blended material withalphaTest === 0still gets the alpha-bucket contract's depth-write override; the policy is documented at the site. - Room reattach re-arms terminally parked model retries (ledger 1183):
RendererAssetServicelatchesretryAfter = Infinityafter the dead-4xx budget or pre-session park cap — reachable purely from outage artifacts during a room container restart, after which the models never re-requested for the life of the process. The authenticated attach now relays through the host chain toassets.service.notifyRoomAttached(), which clears exactly the infinite parks: one re-probe per re-armed id per successful attach, the ladder's own backoff governs from there. Finite clocks and server-stated windows (quota deadlines, cook-kill tombstones) are untouched. - Camera skills teach pointer-lock truthfully (r-224, skill sources only):
camera-first-person.mdno longer claims mouse look needspointerLock: true(mouse-delta axes feed whenever a button is held), teaches the first-class mixed mode — cursor game + hold-to-look via edge-triggeredhideCursor()/showCursor()— and warns that cursor verbs are last-write-wins within a frame (a per-frameshowCursor()silently stomps an edge-triggered hide). - God-mode pick ray sees scripted-material meshes again (ledger 1201, 5.2.0/lume regression): a scripted-material binding swapped the entity to the standalone scripted lane (
lume/primitives/scripted.ts), which kept GPU-side geometry only — the CPU renderer raycast walked only the batched lanes + oversized pools, so the ray passed straight through and selected terrain behind. The scripted lane now retains a CPU pick source (zero-copy position + index references, local AABB) with a lazily built BVH, andraycastPrimitives/primitiveWorldBoundstraverse it alongside the lanes. The pick source is minted from geometry alone, independent of the material record — a still-compiling, parked, or failed binding picks identically to a healthy one; bespoke meshes ride the same lane. The stale "falls through to the sim pick" fiction is removed from the docs. replaceSpecWithStartergrows a convert mode for built worlds (plan p-7d8c9abb):replaceSpecWithStarter(id, { convert: true, dryRun: true })returns a manifest of exactly what would change with zero writes;{ convert: true, consent: {...} }executes the consented conversion. The merge is a pure function of (birth seed × target seed × current spec) with one law throughout — a facet is register-bound iff the seeds disagree on it: register-bound authored config replaces with a migration record, edited seeded files park underscripts/parked/<birthId>/(never deleted), register-agnostic and creator work survives, absent seeded pieces never resurrect. Lands mode/gravity/camera/inputs/ground/embodiment explicitly and regenerates lineage plus a freshauthoring.architecturesnapshot carrying the migration records.- One-shot fx volleys no longer burn during the pipeline compile window (ledger 1200): while any fx pipeline was still building, the pass group skipped but
stepEffectkept consuming spawn state — persistent beds (spawn.count) andburstvolleys latched their fired flags CPU-side with no spawn pass ever running, so the work was overwritten unconsumed. Silent, never throws, permanent — and live-shaped: every effect in a joining client is created during exactly this window (the join snapshot arrives while the arena's pipelines compile). The fx spawn timeline now freezes untilpasses.ready()— elapsed, rate accumulators, burst timers, and persistent volleys don't advance while the pass group can't run; anchors keep refreshing so the first live frame spawns at the authored transform. Pinned by a permanentverify-fx-gpu-couplingscenario that runs a five-role brazier program verbatim through the compile window. - The engine's own god-mode effect decks shipped the wall-of-cards bug the fx skill teaches against: pre-streaked textures on
align: "velocity"sprites (a velocity-aligned quad keeps its painted streak a constant ~67° off the motion axis on every particle — nothing decorrelates). All 12 velocity-aligned deck sites swap to round paints (rain spatter →effect-droplet-round-soft; sparks/magic/portal/embers/fireworks flares →effect-ember-glow) — motion supplies the streak. The kiln round-subject gate also recognizes explicit round tokens mid-name, soeffect-droplet-round-soft-style mints hit the round gate instead of skipping it on head-noun matching. - The heightmap edit applier skips journal commands whose payload isn't a height edit: terrain-edit journals are kind-blind, shared per place, and survive terrain-kind flips by design — so after a tilemap → heightmap flip, a
tile-setcommand's[[x,0,y],[x,0,y]]bbox was applied as a height clamp, pinning heightmap columns to height 0 (the corruption flowed into the chunk mesh, the heightfield colliders, and the horizon ring, which all sample through the sameapplyEditscomposition). The tilemap and voxel appliers already filtered foreign kinds; the heightmap applier was the one hole of the three. - Pressing interact no longer fires an action's toast twice on setups where a key and a controller/touch gesture share a binding (ledger 1205). Each input source is edge-triggered per its own press edge, so one physical press could land the same logical interact in the interaction queue twice in one tick — and
onInteractran once per entry, doubling everything the hook emits (toast + announce on the staging zoo's exhibits). Interaction dispatch now coalesces duplicate queue entries per (source entity, target entity) within a tick. Legitimate distinct interacts are untouched: two different players pressing the same tick both land; only one player's duplicate sources collapse. - Cliff and steep terrain textures no longer smear on 5.2 (a porting bug inverted the side-projection axis): the biplanar side projection now samples the coordinate running across the face again — three-era parity — instead of the one along its normal, so carved cliffs, craters, and pond walls show their strata instead of vertically stretched streaks.
effect("blur")works again — the blur juice beat in death flashes and dashes was lost in the 5.2 renderer swap. The look resolver kept resolving blur, but the new renderer had nothing that consumed it, so the screen just stayed sharp (55 worlds call it). Blur now rides the look pass as a gaussian gather with the 5.1.x kernel's gaussian core (σ 9.5 texels per axis at full intensity; intensity 0..1 scales the radius exactly like before, and fading it never hitches) and a slightly tighter tail — support stops at 2σ instead of 5.1.x's 2.84σ, the disclosed tradeoff for a well-sampled single-pass gather. Scripted looks getblur(ctx.scene, amount)back in builtin/postfx (it re-samples the scene, like pixelate), and the skill teaches the verb again. Known boundary: blur composes with grades/vignette/grain, but stacking it with a discarding resampler (pixelate / chromatic aberration — the vhs, glitch, chromatic, speedLines, pixelate recipes carry one) silently drops the blur.- Large water sheets no longer draw over nearby transparent objects at certain camera angles: a transparent water surface used to sort by the signed view depth of its transform origin, so a big lake whose center sat behind the camera sorted as the nearest draw in the scene and stamped over bushes and foliage in front of it. Water now sorts by the farthest forward extent of its bounds (never negative), so shore objects composite on top of the water behind them at every angle.
- Scripted materials reach models + decoration layers (the #8783/#8784 persist-gate parity fix): the shared zod schema now mirrors the engine's material types exactly.
model.materials/partsacceptScriptedMaterialSpec(previously every taught per-part scripted write died at the kiln persist gate withproperties.model: Invalid input), andDecorationLayerSchemagains itsmaterialfield (zod stripped it silently on save — live-looks-right, reload-loses-it). A parity test pins both fields against the engine types. - Slash and shockwave effects no longer flash a black wedge from certain angles (ledger 1210, the staging-zoo juice aisle video). Every slash/shockwave spawned a vestigial invisible 1×1 plane alongside the real effect — a three-era leftover whose only remaining job was carrying a size the effect already reads from its transform. If that plane ever reached the renderer without its effect material tag, it drew as a plain untextured plane: black from its unlit side, the wedge in the video. The vestigial plane is gone; the effect draws from its own component and transform alone, so the wedge class is unrepresentable.
- TAAU scrub fallback epsilon → f16 min normal (jure's find, ledger 1195 rider): the resolve's division guards used
1e-5, which sits below the f16 min normal (2^-14 ≈ 6.104e-5) — under the shader-f16 aliases the literal materializes as an f16 subnormal, WGSL permits flushing subnormals to zero, and an all-invalid 3×3 then computed 0/0 = NaN, re-seeding exactly the NaN class the scrub exists to contain. One untyped WGSL const (EPS_MIN_NORMAL_F16 = 6.103515625e-5, exactly 2^-14 — exact in f16 AND f32) replaces all three epsilon literals; a test pins the constant and sweeps every kernel axis for surviving flushable guards. - Fixed static objects visibly bouncing for some players while terrain was being edited live (ledger 1204). Terrain-anchored objects (
feetPosition.y: { terrain: offset }) were re-resolved locally by EVERY client whenever the composed terrain height under them changed, while the place host's re-anchor writes also replicated in — two writers for one row. When two clients' terrain state briefly disagreed (live wisp edits, or plain join-time replication ordering), the writers fought and "static" props ping-ponged between the two grounds, differently on each player's screen. Re-anchor writes now belong to the entity's simulator only (owner/lease/place host); other clients receive positions over the normal replication lane. Cost: non-host players see anchored props settle one round-trip after a terrain edit — the standard latency class for host-simulated state. Singleplayer and server behavior unchanged. Toasts and announcements raised by behaviors no longer show twice for you in your own world. When a behavior fired a toast, the copy your game showed instantly and the copy the server reflected back were supposed to be recognized as the same event — but a toast with no icon/color set was recorded in one shape and came back over the wire in another, so the reflection slipped past the duplicate check and rendered again. The echo check now compares events the way the wire actually carries them, so behavior toasts, announcements, and every other behavior-fired effect land exactly once for the person whose game fired them (everyone else always saw them once).run_scripttoasts were never affected. Captures and Savi's live view no longer drop text and sprite objects that use motion/MRT rendering. On sessions whose scene pass carries the TAAU motion-vector target, some lanes' pipelines carry a second color attachment — and the capture pass declared only one, so any such pipeline handed to a capture faulted the whole submit with a GPU validation error ("[pipeline] is not compatible with [RenderPassEncoder capture/scene-view]") and the object went missing from screenshots, thumbnails, andview_live_scene. The capture pass now mirrors the live scene pass's attachment state (the motion vectors land in a scratch attachment that is discarded), so every live pipeline — text, sprites, and any future MRT lane — draws into captures unmodified. First captures also get slightly more complete: the capture reuses the live pipelines directly instead of waiting on capture-only twins to compile. When a script keeps using the wrong entity ids, Savi now gets told once clearly instead of the problem hiding in logs. The engine already taught the fix per call ("entity not found — call ignored. Use entity IDs, not array indices"), but a behavior looping over bad ids — sequential array indices, generated name suffixes — repeated that lesson thousands of times into the runtime log and nowhere else, so the game silently did nothing for hours while nobody noticed. Now a loop of misses from the same call site counts as one problem: after the tenth ignored call, Savi gets a single direct notification naming the object, the script, the ids it tried, and the fix, and the per-call warnings drop to a compact summary every 500 ignored calls. One-shot typos keep the full per-call teaching, exactly as before. A model told to play a clip that contains no motion now says so instead of silently freezing in a T-pose. Some GLB exports bake broken clips as a single static keyframe per track (a Blender/Mixamo action that never made it into the export as real animation) — the clip exists, binds, and "plays" its one bind-pose sample, so the model just stands frozen with zero errors anywhere, and the only way to find out was debugging clip-by-clip. Now the first time a script plays such a clip, Savi gets one clear diagnostic naming the clip and the entity and pointing the fix where it lives: the source file's export, not the script. Deliberate one-frame pose clips that are never played stay silent, auto-play behavior is unchanged, and the horde path reports the same truth instead of a misleading "clip not found". - World text stops shimmering/ghosting against its moving parent under TAAU (ledger 1222 — the "nameplate blurs when I run" pair). Text never wrote the velocity MRT (the alpha lanes' writeMask-0 default) and never writes depth, so the TAAU resolve's closest-depth dilation reprojected nameplate pixels with the OPAQUE surface behind them — a runner's nameplate rode the streaming terrain's motion while the body reprojected on its own, and the thin-feature lock preserved the stale glyph copies as a per-frame double image. The text lane is now the alpha bucket's cutout exception: instances carry the previous packed block pose (position + resolved billboard rotation, the primitive lanes' prevTransforms discipline), the MSDF fragment writes true per-glyph object motion via
lume_velocity_pair, and the store runs one settle rebuild after motion stops so a resting text parks with zero velocity. First-time packs seed prev := current (zero object velocity on the spawn frame); hidden/refonted/dimension-flipped texts reseed instead of differencing across the gap. - Renderer scripted-texture library replaces now split changed-source refs from RETIRED refs (dropped from the library because nothing references them). Retired families keep their last-good texels and atlas grammar, leave the re-bake sweep, and never fire the "was not found" diagnostic — that teach is now reserved for refs never present in any received library (true typos). Previously a projection reset re-derived the library without retired scripts and the invalidation sweep re-baked every cached family variant into simultaneous false not-found reports (dig 20b7b07c: ~129 doomed bakes, one wasted Savi turn chasing a phantom).
- The TextureScripts derivation dropped its carryover rule: the library is now a pure function of (spec + live draw state + live compiled fx programs) on every derivation path, instead of depending on session history (live tabs kept retired entries forever; post-reset re-derivations starved them). FX sink refs are now collected by walking FxCompiledProgram.
- Client-reported
engine.diagnosticrows are stamped with the reporting client's held spec revision + dbVersion (data.specRevision/data.specDbVersion), relay-stamped in the runtime worker — stale-client-vs-propagation questions become one read. - The variant-change room recycle in
GameRoomRuntime.setSdkConfignow recycles the WHOLE room scope — host, SDK identity, and the room binding itself — and runs BEFOREbindRoomId(pc-42951396). The old order half-reset the scope: a different variant claiming the runtime tore the world down and reset identity but keptthis.roomId, and the bind ran first — so an app switch that also moves the room id (local dev runs every app through one kernel container, and a live-mode session leaves the singleton runtime scoped tolive-room-1while the next game's dev room isroom-1) died at the cross-room protection ([room-runtime] room mismatch: runtime is scoped to live-room-1, received room-1) instead of recycling. Same-room variant switches (the June#6757behavior prod rides on kiln variant reassignment) are unchanged; the recycle no longer requires a live simulation host (a variant change with no host previously fell through tomergeSdkIdentity, which never clears fields — staleupdateSlug/keys from the old game could leak into the new scope). The misroute protection is untouched: within ONE variant, a mismatched room id still throws — pinned inper-room-sdk-identity.test.ts, whose "stray rooms are refused" contract is now scoped to same-variant strays (the old pin's cross-variant stray was the app switch itself). On a cross-room recycle the new binding lands beforeonRoomResetfires (in-process/NO_WORKER relays the reset synchronously; an unbound runtime would be re-claimed by the old room's re-activation, first-bind-wins), so the old room parks "failed" and heals on its next real join, which carries SDK identity and lands back in the recycle. - The steady-state renderer-authoritative camera path now enforces the all-cameras-are-fully-authored ruling (ledger 1265, the 06-05 camera law): a mouse-reading custom camera classified onto the display-rate orientation path used to have its per-tick authored yaw/pitch writes structurally discarded —
RendererCameraOrientation.updateConfigtook "parameters only, never yaw/pitch" in steady state, so death-lock grips (the prod specimen: scripts write forceYaw/forcePitch, camera script snaps its yaw/pitch to the grip every tick) held rock-steady in sim state while the screen free-looked. Broken since the 5.0 cut; #8173 fixed only the non-lookAt sibling. The fix is the ruling's own named shape, a divergence-adoption gate at the config-ingest seam: config.yaw/pitch that is explainable as an ECHO of angles the orientation itself published (camera-sync mirrors them back through the f32 CameraAngles SAB, one-plus ticks stale, optionally advanced by the script's own look-axis integration of the same mouse motion) keeps renderer authority and display-rate mouse feel; anything else is the script authoring a different pose and it wins that same ingest — seed the divergent axes, publish so movement axes match the screen, drain pending deltas on full-pose snaps (zoom-handback semantics). The echo test keeps a per-ingest-interval envelope of published angles (tested per interval, so adoption discontinuities aren't convex-hulled over) extended directionally by the window's signed net mouse motion with 2× sensitivity headroom — a static grip behind a sweep exits immediately while a taught self-integrating orbit script never false-adopts. Per-axis adoption keeps display-rate look on the free axis under single-axis grips (horizon locks). The zoomTo sim-authority window and its handback seam are untouched. - The renderer→sim camera-angle mirror is now universal across live-path kinds (
mirrorLiveCameraAngles, extracted from the custom-kind-onlywriteCustomCameraAnglesBack): built-in first/third-person and god sims learn the renderer's published yaw/pitch every tick, exactly like custom scripts. This is what makes the gate's invariant hold for channel-only look sources — gamepad sticks (writeGamepadLook) and touch drag-look feed ONLY the camera-orientation SAB with no lookX/lookY trace in the sim's input ring, so an unmirrored kind froze behind the renderer and the gate read its frozen state as a script override, re-adopting every ingest (measured pre-fix: intended 64.4° stick sweep → net 2.15°, view pinned). The mirror also dissolves two sibling symptoms on built-ins: lookX integrated during a zoomTo window no longer snaps at handback (the mirror re-bases through the zoom), and the pitch-convention rederive's flipped publish can no longer be adopted back against the correction. TomeCameraState is client-plane; no replication contact. - Known bounded residuals, pinned in tests and filed for the required sim-side authored-window latch follow-up: a player actively fighting a held grip sees wiggle-amplitude wobble (±0.83° moderate / ±4.1° hard at default sensitivity) or a one-ingest sawtooth under steady sweep (~1.7–5°), and a yank after wiggling around the grip gets up to the echo window (≈2 ingests, ~25° at hard-yank rates) of free runaway before re-adoption recovers the grip exactly. Mouse-still grips hold exactly.
- The unattributed validation-storm death class (ledger 1232 — the 07-12 destroyed-scripted-texture storm: one validation error per submit, ~an hour of frozen screen with ZERO signal to the player or Savi) is now judged. Validation errors that match no rail (not the dead-device regex, not the #187 burst classes, no scripted-material label to park) used to fall through
handleDeviceErroras a per-frame silent no-op; they now feed a sustained-storm window (≥24 errors spanning ≥15s inside 30s — reachable in both delivery regimes, including Dawn-muzzled sessions where errors only arrive via the #365 sentinel probe) that rides the SAME ladder the known burst classes use: bounded render-chain rebuilds first, then the honest device-lost declaration. No new walls, no new transports — the trip lands on the existing #6710 sticky reload wall for the player and the existingrenderer-device-lostengine-diagnostic (getLogs + one deduped DM) for Savi. - The
renderer-device-lostdiagnostic message now carries the client truth instead of a mechanism note: the player's screen is frozen on its last frame (or black) and shows nothing until they reload, suggest refreshing if they mention a stuck screen, this is a device/renderer failure — not a game bug — do not rewrite or optimize content over it, andview_live_scenemay not reflect what that player actually sees. Informational, never a work order (the savi-perf-notifs-pull-not-push law). Once per death, content-deduped server-side, zero cost when healthy. - Discard-carrying opaque draws now render on their own cutout channel AFTER plain opaque (#9055). Punch-through pipelines (fragment
discard) disable early-Z and TBDR hidden-surface culling for whatever is still undrawn behind them, so mixing them through the opaque bucket in registration order wasted the depth buffer's rejection power — worst on mobile tile GPUs.MainPassDrawsgains acutoutchannel andcollectMainDrawsfolds it in after every contributor's plain opaque (registration order within each class; return shape unchanged — chain and capture consumers untouched). Routed emitters: model MASK slots (static/horde/character), decoration sprite + scripted layers, the opaque voxel bucket, rooms submeshes, built-in tilemap draws, and all four scripted paths (tilemaps, models, character primitives, standalone primitives route byshade.alphaTest > 0). Both channels depth-test and depth-write, so the split is pure ordering, never correctness. Shadow passes untouched (every lume shadow caster is vertex-only depth-only — no discard exists there, already optimal). - Made the destroy/name-resolution funnel honest (ledger 1247, dump a693ca36 — Savi aimed every kill at "5", a list POSITION, while the target was the object named "garden-music"; every wrong kill read as success). Three seams, no new vocabulary: (1)
destroy(id)on an id that matches nothing in the live world AND nothing in the spec now reports on the missing-target rail (warnMissingMutationTarget, the ledger-1215 escalation funnel the other mutation verbs already ride) — visible ingetLogsand in-band inrun_scriptresults; spec-declared ids stay quiet (client-realm local spawns and unmaterialized places are delivered by the clientDestroy fan-out, and double-destroy of a real object stays idempotent-silent). A bare-number miss that lands inside the caller's place objects array names the object sitting at that position ("the object at places.main.objects[5] is "garden-music"") — the error text is the teaching. (2)getSpecnumeric path segments that land on an id-carrying object still resolve (positional reads keep working — nothing in-tree relies on them, but shipped worlds might) and now say which id the position landed on, once per digit-folded path shape per cooldown. Prefer ids in spec paths; the positional form on id-carrying arrays is deprecated in spirit and a candidate for removal at the next breaking window. (3)api.querygains "enumerate" visibility: with no radius, camera-attached objects match like any others (the enumerate-everything fallback was structurally blind to them); with a radius they stay excluded (their WorldFeetPosition is a bookkeeping anchor, not a place) but the exclusion is reported, naming the hidden ids.nearest()and every engine caller keep pre-1247 "sensor" visibility — byte-identical behavior. - The stale host-epoch wedge (ledger 1257 — jacob's Waystead carried epoch 48 into a place re-epoched to 2 for an entire session; jo's Sunhollow Farm dropped 1,238 host writes on a room restart and half-built a farmhouse) now self-heals server-side. The epoch gate itself is untouched — host writes stamped with a dead epoch still drop exactly as before — but the state-delta drain now tracks continuous stale-epoch drop windows per sender, and when the skipping is provably a wedge (≥30 dropped-write messages spanning ≥10s, uninterrupted by a table-corroborated stamp) it flips the sender's projection to NeedsReset: the next egress tick ships the production reset snapshot (current TomePlaceHosts included) and the client rebases through the rail every join/migration/resume already uses. The thresholds sit an order of magnitude past the legitimate migration straggler window (~1 RTT of in-flight messages), a ≥30s quiet gap restarts the window so separate migration races never accumulate, a valid host stamp clears it, and a 60s per-client cooldown bounds re-forcing when a client is reset-proof wedged.
- Telemetry: each force emits one DD-countable
netcode.state_delta.reseat_forcedwarn (client, place, epochs, skip counts, forcedCount), one script-visible runtime-log line for Savi ("the server re-seats the client automatically — no rejoin or tab-close needed", so she stops prescribing the close-and-rejoin remedy that triggers a host migration and drops persist:false entities), and areseatsForcedcounter on the upload stats (F3net uploadsline). - Field-chunk change tokens are content-aware — the version-ABA at the shared invalidation seam is dead (ledger 1250).
applySerializedFieldToWorldpins every restored chunk to version 1 (and a recreated chunk restarts at 1), so a spec-level rewrite of an already-painted chunk (updatePlace/definePlace field patches, brush gesture-cancel, hydration of changed fields) landed v1→v1 with DIFFERENT cells and every version-keyed consumer kept stale state until an unrelated input moved.commitFieldChunk— the single seam every local chunk write funnels through — now stampscellsHash(FNV-1a over the int16 cells mixed with the quant/cellSize they decode under, ~4.6µs per default 4096-cell chunk, on a path already O(cells)), and every change-token consumer mixes it. All seven, by name: the terrain mesh inputs-hash + horizon + spline-redrape (viagetComposedFieldChunkVersionsInBounds/composedChunkChangeToken), nav'scomposedFieldPlaceSignature(#9019), terrain-anchor re-resolution (composedFieldVersionAt), the scatter footprint memo (bed-field layer signatures now readchunkChangeTokenoff live chunk components AND the terrain:height walk), the scatter-instances supertile signatures, and the decoration-density layer snapshots. Versions keep their ordering role (resim reset, predecessor+1 bumps) untouched — materialized per-layerFieldData.chunkVersionsdeliberately stay RAW versions because translate/write deriveversion + 1from them; per-layer signature consumers readchunkChangeTokenoff the chunk components instead. - Chosen over never-restarting version seeding deliberately: the hash is a pure function of the replicated chunk value — identical on client and server for the same write, stable across resim replays (a counter/timestamp scheme would diverge between realms and churn under resim, and the rollback fast path applies chunk values without set/add events, so hook-fed counters can't be trusted either — the reason field-feed-derive's destroy-epoch shield had to exist; that shield stays and keeps covering the feed + GPU field-texture path). Bonus: identical restores are byte-quiet — a spec re-apply of unchanged fields rebuilds nothing.
- The composed token also closes a max-masking hole:
max(authored, runtime)hid a write to the lower-versioned layer under the higher one; the token now mixes both layers independently. Mixing quant/cellSize into the stamp closes a second pre-existing blindness: identical raw cells restored under a different grid decode to different world values, invisible to version equality. TomeFieldChunkValue.cellsHashrides the existing JSON encode/decode transparently; values from pre-hash builds read as 0 and re-stamp on their next commit.- The quality governor recognizes the interval-capped regime and stops paying quality for it (ledger 1223 — the 60Hz sibling of the L19 pacing-first class). When a browser environmentally caps rAF delivery at a mixed cadence (the iPad CriOS specimen: ~35Hz arrivals on cpu 2.4ms + gpu 7.1ms of measured work), the fused wall signal read sustained overload, the ladder walked to the survival floor in ~40s, and five rungs of cuts moved the interval 32.1→28.4ms (~12% — the cuts bought nothing), then held the floor forever because calm was unreachable. The governor now keeps a relief ledger per overload down-step (predicted relief = the same live/applicable/relieving costHints the probe gate charges) and classifies the regime INTERVAL-CAPPED when consecutive steps verifiably under-deliver while the measured work explains neither the budget nor the interval — then restores the pre-descent rung in one shift (
interval-cap-restore, on the transition ring and telemetry) and holds the same wall-interval gate the clean-2× environmental signature gets (the capped cadence stops entering the fused signal; a genuinely degraded ≥40ms interval still does), releasing when arrivals return to the judged display rate. The exact-2× environmental signature is untouched — this covers the mixed-cadence space it deliberately misses. Real overload is unaffected: measured work fails leg (a), and a genuinely GPU-pinned device's interval shrinks when pixel work is cut, so relief verification passes and the descent proceeds. - The interval-capped latch (ledger 1223, #8971) finally gets SAID instead of silently absorbing the blame (two specimens in two days: the iPad CriOS ~35Hz session and a MacBook at 12fps under Low Power Mode all session, found only by luck). On latch engage: (1) PLAYER — one toast per session on the existing juice toast surface, watched at the renderer→host perf-sample relay (no new transport), mechanism-honest wording ("Your device is limiting frame rate — Low Power Mode or battery saver can cause this"; the engine detects the CAP, never claims LPM itself), never repeated on re-latch, transient by construction; (2) SAVI — one
render-interval-cappeddiagnostic per session riding the existing engine-diagnostic rail into the perf-notify pointer DM (newrender-interval-capcategory: environment-not-content blame, consent-framed, on the same global 1/hr budget — any perf DM silences it for the hour), with the full verified-cap body in the runtime log for the pull side. The client-health snapshot now carriesintervalCapped(strict-boolean validated) and thegetClientHealthsummary names the cap at any rung — including restored-to-full, where the rung-0 fast path used to hide it. The governor's classification logic is untouched; this consumes its latch state. - Fixed the transparent-lane sort flicker class (ledger 1242, Space Craft protocell): the alpha bucket sorts translucent primitive lanes as whole batches by a centroid-anchored
sortDepth, so nested translucent lanes sharing ~one world center (base shell lane + emissive-split lane + inner cubes at the same point) tie within noise — per-frame centroid drift (membership rebuilds re-snapshot displaced instance transforms; camera motion moves every distance) flipped the sign of the tie and the lane draw order flipped frame to frame, z-fighting whole translucent layers. Regressed 5.1.13 → 5.2.0 with the lume whole-batch alpha sort. Fix is a deterministic near-tie order, never a new depth model:PrimitiveLanemints a creation ordinal (alphaSortTiebreak), the lane and oversized-pool alpha draws carry it asSortableDraw.sortTiebreak, the alpha sort itself stays a consistent pure-depth comparator (an epsilon comparator is intransitive — spec-level implementation-defined sorting), and a post-pass (stabilizeAlphaTieRuns, sort-order.ts) then reorders each maximal run of consecutive carriers whose neighbor gaps are withinALPHA_SORT_TIE_EPSILON(0.25 m: comfortably above cm-scale centroid drift, far below deliberate authored separations) by ordinal — ascending, later-created lanes composite on top; deterministic and input-order-independent, and a coincident pair's mutual order is pair-local (no third lane can split their run). Sub-epsilon ladders deliberately chain-merge into one ordinal-ordered run (documented trade against already-approximate whole-batch centroid depths); gaps beyond the epsilon never form runs, so genuine separations keep pure depth order. Draws without a tiebreak (2D sprite sort bands, per-entity scripted/model depths, particles, water) never move and break runs — bit-for-bit pure back-to-front — and a lane's emitted back/front two-pass pair order rides the run sort's spec-guaranteed stability (equal depth, equal ordinal). - Fixed dead mod HUDs after install (ledger 1271): mod install namespaces the mod's inputs (
edgeX→rts-controls:edgeX) and the input proxy resolves the mod SCRIPT's bare reads to the namespaced form, but a mod HUD wired atui.rendersent BAREsendAction('select')/sendAxis('edgeX')— undeclared, dropped, dead buttons. The{modName}:send-prefix rewrite existed only for mod-installed creator tabs. The compiler now stampsuiModNameon the compiled spec whenui.renderpoints at an installed mod's script (amods/{name}/…ref with{name}present inspec.mods), and the game UI surface carries it asactionPrefix/axisPrefixthrough the existing dom-hostmaybePrefixInputName— the same mechanism creator tabs use, no new spec state.input.actionDatareads get the same mod-scope resolution the action/axis proxies already had, so payloads sent under the namespaced action land on the mod script's bareinput.actionData.select. Creator-authoredui.rendernever matches (nomods/prefix or no manifest entry) and stays byte-unchanged; uninstall deletes thespec.modsentry, so the prefix comes off with the mod — the fix writes nothing into the spec, leaving operation-precise uninstall untouched. - The NPC nav floor oracle now reads sculpted terrain (ledger 1244 — jesse's Field Orders: units passed THROUGH god-mode-sculpted hills at y=0). NavGrid's heightmap column sampler composed only
heightFromDefinition(generator + marks) and never consulted the replicatedterrain:heightfield overlay, sonavAtanswered floorY 0 / blocked:false on field-raised ground and movers kept agents at the generator height. The sampler now composes the same ground the terrain mesh/collider build bakes — generator + marks + composed field overlay — through the same pipeline (getComposedFieldInBoundsmaterialized once per nav chunk,readFieldValuebilinear per column), gated to heightmap generators exactly like the mesh (fields sculpt heightmap terrain only); every other generator derives byte-identical columns. - Field writes now invalidate the nav grid: the per-place input compare gains a
terrain:heightplace signature (newcomposedFieldPlaceSignaturein field-store — an order-independent fingerprint over painted chunk coords/layers/versions, both layers). It is a query-time walk refreshed at tick boundaries, never a component subscription, because the prediction rollback fast path applies field-chunk values without firing set/add subscribers (the field-feed-derive rule) — so sculpt, erase (chunk despawn), and rollback-applied field state all rebuild nav on every realm. Unpainted fields cost two index probes per place per tick. - Perf (36-chunk / 9216-column region rebuild, shared dev box): generator-only 5.2ms before → 5.6–6.8ms after (within the 4.2–5.2ms noise band measured at HEAD); field-painted 4.2ms (overlay ignored) →
8.0ms (+105µs per lazily-rebuilt chunk); steady-state refresh with a 64-chunk painted field 3.7µs → ~9–13µs per tick across 20 getNavGrid calls. - Fixed the unanchored-pushLook permanent library miss (ledger 1241): a look script referenced ONLY by
api.pushLook(scriptRef)— never anchored to any place'satmosphere.look— has exactly one lane into the renderer's LookScripts library:ensureLookScriptEntryat push time (syncLookScriptsWithSpeccollects refs only from atmospheres, live overrides, and existing library keys — pushed layers are invisible to it). That ensure's component write can be lost while the layer itself survives (behaviors resim in prediction overlays that stage-and-discard component writes but leak the in-placeLookLayersResourceMap mutation; projection resets re-derive the library without pushed refs) — after which nothing ever repairs the miss: the renderer parks the look onscripted-look-compile-failedclaiming the script "was not found in this game's scripts" with the file present in the spec, under either ref shape (scripts/look-x.jsorlook-x.js). The specimen (Primordia, three sessions) only escaped by anchoring the script to a place's atmosphere.look (v1761), which routes the ref through the spec-sync collector — an undocumented requirement pushLook was designed not to have. Two repair lanes now converge it, mirroring the scripted-material twin: the look resolver (client renderPrep, base world — writes stick) re-ensures a resolved ref whenever the library misses it, healing in the same frame the resolved look ships (healthy frames cost one component read + four map probes); and spec sync now collects live pushed-layer refs exactly likecollectLiveMaterialRefs, so every spec application repairs lost entries. Unanchored pushLook now just works.LookLayersResource(+ActiveLookLayer/LookLayerState) moved fromsystems/look-resolver.tstoresources.tsto keep look-scripts ↔ look-resolver acyclic. The compile-failed DM now only fires for scripts genuinely absent from the spec, and its fix-text names both look entry points (atmosphere.look.scriptand theapi.pushLookarg). - Shade compat: unknown vocabulary reads through TSL no longer throw — they return
undefinedwith a one-time teaching warn (shader-vocabulary-unknown-symbolvia getLogs), so a script written against newer vocabulary degrades instead of killing the material. Addsvaryingandfwidthto the shade vocabulary. - The delta-0 short-circuit's completed-apply binding now survives coherent ObjectAPI persist mirrors (ledger 1268, PR-C #7919 follow-through). The binding veto (
canShortCircuitIdenticalApply: generation record'sspecRefmust be the exact liveGameSpecResourcedoc) existed to prove "the world already IS this spec" — but every persist-mirrored write (persistent spawn/destroy,setProperty/updateObjectSpecpersist) moved the doc head and permanently disqualified the game from the 2ms identical-apply lane, even though those mirrors write the live ECS effect and the doc in one synchronous op (the instant contract). Prod receipt: a zero-delta apply measured 5,233ms (shortCircuit false, objectsVisited 0, objectsSkipped 2050, expandPasses 2) on a live game whose applies cost 2.2ms whenever the gate binds. NowupdateSpecResourceadvancesspecRefalongside the head for writers that declareworldCoherent— the persist mirrors, gated per write byisLiveCoherentPropertyKey: every persisted key must have a live runtime writer (the NO-WRITER schema keysroom/collider2dare spec-valid but APPLY-DERIVED — expandRoomObjects / physics-primitive derivation materialize them only in a full pass — so their mirrors keep breaking the binding) and must not be a top-level key (behavior/parent/tagsdefer wiring to a requested reconcile). Spawn mirrors break the binding when the def carries place-level expansion content (room/spline). Writers that request anapplySpecreconcile (updateSpecAndRequestApply: setScript, patchTerrain/Player/Camera, add/removeBehavior, definePlace/updatePlace) keep breaking the binding, so a drain of their content can never short-circuit past its own recompile/rebind — op-count-pinned in both directions inspec-apply-changed-set.test.ts, including the no-writer-key negative pin and a mixed-order burst pin (once broken, later coherent writes cannot resurrect the binding until a full apply rebinds). Note:patchState/replaceStatewrite liveTomeStateonly (no spec mirror), so pure state writers never touched this binding in either direction — the storm lane this fixes is the property/spawn persist mirrors. - room-runtime's
applyTomeSpecnow recognizes the kiln poke-back echo: a fetch at a strictly-newer dbVersion whose content the delta-0 gate certifies as the fully-applied live generation (the room's own persist echoing back through a version the consecutive-onlynoteAuthoredDbVersionstamp couldn't absorb — interleaved persists) still runsapplySpec(its internal short-circuit costs ~2ms and keeps the rails a real short-circuited apply owes the world: the spline-orphan sweep and thetome.apply.episodetelemetry), but suppresses the tail — the revision-bumping upsert and its replicated push fan-out to every client — stamping the fetched dbVersion so the staleness guard sees the room at the echoed version. The gate iswouldShortCircuitIdenticalApply— the apply's own predicate, run conservatively on the raw doc. Replace pokes never dedupe (replace re-asserts live state, ledger #296); retry states (degraded lowering, compile errors, scatter recovery, residency growth) fail the predicate and take the normal tail; a failed apply falls through to the raw-reference upsert retry rail. - Deliberately NOT in this change, named for the ledger: (a) the O(spec) skeleton phases (expandPlaces ×2, normalize/namespace ×2, signature rebuilds) still run whole-doc when a real delta arrives — making them O(delta) needs identity-threading through sanitize/namespace/normalize plus expansion caching with world-state (terrain/residency) validity, a separate careful change; (b) the multiplayer-relay fold lane (
foldSpecMutationsIntoWorld) still breaks the binding on every forwarded batch; (c) mid-apply mirror writes (self-rebuilding creator behaviors, the vil-home-builder bench residual) still land the generation record behind the head — the binding re-forms at the next quiescent apply instead of never. - 2D pixel-sprite aliasing/shimmer fixed at the sampler math (jacob's batch-2 felt report, 2026-07-12 — sprites in his 2D sessions crawl/shimmer under camera motion; same session family as the #8988 tilemap seams, whose screenshot pinned the zoom at ~3.65 screen px per texel). Root cause: pixel-grammar sprites sample NEAREST with no mips, and at fractional magnification the art's texel boundaries land at fractional screen positions — texel columns alternate ⌊3.65⌋=3 and 4 px wide as a function of the quad's subpixel phase, and every camera pan (or sprite move) rephases the pattern, so texel edges flip full-contrast frame to frame. Camera
pixelSnapcannot cure this class: it quantizes the presented camera to the SCREEN-pixel grid, but the texel:pixel ratio stays fractional. Fix: pixel-art texel AA in the lume sprite fragment (material.texelSnap, per-batch) — the batch binds a LINEAR clamp sampler in place of the lease's nearest one, and the shader remaps each sample to its texel center (bilinear weight 1.0 — bit-identical texels) except inside a one-screen-pixel band at texel seams, where the linear tap interpolates (seam + clamp((t−seam)/fwidth(t), ±0.5)). Texel interiors stay bit-crisp, seams render at a stable subpixel position instead of re-picking texels, and the mapping is bit-identical to nearest at aligned integer zoom (pinned by f32-emulated test). The flag mirrors the asset service's sampler policy exactly viaspriteTexelSnapin sprite-logic — precedence: repeatX strips never snap (they lease wrapS repeat for the −1..2 U span; the clamp AA sampler would stomp tiling — mechanical wrap need, doctrine-aligned with the plate behaviors), then authoredsprite.filterboth ways, then the atlas meta's manifest-authored filter both ways (explicitfilter: "linear"on a grammar-pixel id is never snapped), then the texture-id grammar (plates/tiling surfaces stay linear). Linear-filtered sprites, placeholders, outline taps, and the tilemap lane are untouched. - A null entry in
terrain.marksno longer kills the server (DD sweep 01b440e6 — 396 rows in one hour, all app a6af7bb9,TypeError: null is not an object (evaluating 'mark.kind')inisHeightmapTerrainMark, emitted as bothtome.terrain.anchor_refresh_failedandtome.reconcile.spawn_update_failed, cascading to 3×process.uncaughtExceptionand[FATAL] System "tome/physics-dispatch" permanently failed after 5 retries — server restart required). Two halves, per the savi-failure-contract (a failing spec degrades exactly its own object, never a server restart):- Write side (root):
mergeSpecPatch's replacement branch now recurses plain-object patches with an empty base, so merge-patch null-means-delete holds at EVERY depth. Before,updatePlace("main", { terrain: { marks: { old: null } } })on a place whose terrain had nomarkskey stored{ old: null }verbatim into the spec — a persistent poison that crashed every subsequent apply and tick. A null inside a fresh subtree deletes nothing, and nothing is what it stores now. Uniform across every merge-patch surface (updatePlace, patchPlayer/Camera/Engine, buyables, terrain materials/structures), not a marks special case. - Read side (seatbelt, covers already-poisoned persisted specs): the terrain family guards (
isHeightmapTerrainMarkMap/isVoxelTerrainMarkMap) skip null/non-object mark entries instead of crashing inside their.every(), with one deduped[terrain/marks]diagnostic per map instance naming the offending keys — matching the skip every downstream mark consumer already does.normalizeVoxelMarks(the one other unguardedmark.kinditeration) gets the same skip;reportInvalidTerrainMarksalready names the entries on terrain-content change. - Cascade pin in
physics-dispatch.test.ts: a marks map containing null → system tick completes, diagnostic emitted, valid marks keep dispatching liquid hooks, no throw. Family-guard + resolve pins inmarks-null-entries.test.ts; write-side merge-semantics pins inspec-write-contract.test.ts.
- Write side (root):
- Texture load failures reach Savi again (ledger 1264, savi-failure-contract gap — prod specimen
[LumeTextureService] Failed to load texture scripts/tex-ocean.js: ctx.canvas(64, 2048) exceeds the 1024×1024 cap, fired 3× client-side while Savi shipped the broken visual). The ledger-895 retry-exhaustion emitter fortexture-load-faileddied in the lume port: LumeTextureService's generic retry ladder ate every non-policy failure (bake transport rejections, GPU upload throws, transient-looking fetch errors) with a client-consoleconsole.errorper attempt and then looped on a long cooldown forever — no runtime-log entry, no DM, nothing Savi could see.recordRetryFailurenow firesreportEngineDiagnostic('texture-load-failed')exactly once per failure episode — guarded by areportedflag on RetryState (theterminalpattern), not an exact attempts-crossing check, because the policy rail increments the same counter and a mixed budget/generic interleave could otherwise skip or re-open the crossing (review 9054); success and source edits reset the episode — carrying the underlying error text and, forscripts/*.jsrefs, the edit-re-bakes recovery. Degradation is unchanged — entities keep their last good image or loading placeholder and the ladder keeps retrying; this only ADDS the feedback path. The bake-fault policy taxonomy (texture-script-compile-failed/-runtime-error/-budget) already reported on park and is untouched. Contract row + harness added: the cap specimen drives the real service to exhaustion and asserts one framed getLogs entry + one DM across three reporting clients (service.test.ts), per the savi-failure-contract harness pattern. run_scriptforward-timeout copy stops teaching a false trigger model (look 1b53cc7e, same creator session): the error text said "Run it again once the game is back in view" (Savi cannot observe tab visibility and cannot run without a turn — she repeated it to the creator as a promise) and "with the script still queued" (no queue exists; the script is discarded). Both timeout reasons inscript-forward.tsnow state the truth: nothing is queued, the change has NOT been made, and if the creator asked for the change, tell them plainly it hasn't happened yet. Retry guidance is branch-honest: in singleplayer, retry is only possible in a turn while the creator's game tab is visible (ask them to keep the game on screen and ping); in the left-SP-mid-flight branch the room is multiplayer at timeout, so a retry runs on the server directly and the copy says so — no spurious visibility precondition (review 9054 truth-tighten). Error text is API surface; pins extended inscript-forward.test.ts.- Tile edits gain the client→server lane they never had (ledger 1248 — Jacob's Waystead garden, dig 4287dc11). In client-auth rooms,
api.setTile/clearTile/resetTilecalledapplyTilemapTileOverridebare while voxel edits rodetome/voxel-edit-rail, so gameplay-script tile edits on a client lived in local chunk state only, forever — every server-driven chunk reconcile (host-epoch NeedsReset rebase, rejoin, AOI chunk rows) wiped them. The rail now carries atilekind alongsideset|fill|stamp: the ObjectAPI tile verbs route throughapplyTilemapTileOverrideFromScript, which applies locally first (responsive feel, byte-identical outside client-auth) and forwards overrail.voxelEditunder the exact voxel gates (active script ref, owned/lease/host/interaction-grant source, no transactional overlays, no singleplayer). The server half validates shape + tile id range, charges the same shared per-tick voxel-rail work budget (one cell per edit), and re-applies through the same funnel — so terrain-edit persistence and AOI fan-out ride the existing rails and the reconcile RETURNS the edits instead of wiping them. - Provenance parity with the voxel kinds:
applyTilemapTileOverridegains an optionalauthorIdstamped on the stored edit command (script paths stamp the executing source entity; the god-mode tile brush's replicated authoring-input path stays unstamped and on its own lane — it never touches the rail, so no double-apply). - Version-skew semantics (one kernel manifest ships client + server atomically, so a 5.1.x/5.2.x pin sees neither half and behaves byte-identically to today): a newer client against an older server gets
cmd.err bad_payloadfor the unknown kind — the local apply already happened, degrading exactly to today's local-only behavior; an older client against a newer server sends no tile forwards and nothing changes. - 2D tilemap fractional-zoom seam lines fixed at the sampler math (jacob's dirt field, #inventors 2026-07-12: 1px grass-green vertical columns every 175 screen px through a dirt tilemap — measured in the screenshot as neighbor-tile-colored, exactly every 3rd tile boundary at a 175/3 px on-screen tile). Root cause: the built-in tilemap shader's corner UVs land EXACTLY on the atlas cell boundary (
tilemapTileUvRectrects + corner uv 0/1, no gutter), so an edge pixel whose interpolatedtileUvrounds onto — or, under MSAA center-eval on partially covered edge pixels, past — that boundary makes the nearest-sampledfloor(u·W)read the ADJACENT atlas cell's first texel column; the quad-edge/pixel-grid subphase repeats every few tile columns at rational fractional zooms, which is why the wrong-texel column recurs periodically and only "at exactly the right zoom". Fix: the fragment clampstileUvto the per-instance cell rect inset by half a texel (rect rides a new@interpolate(flat)varying; half-texel fromtextureDimensions) before the V-flip and both asset taps (tileset + lit normal companion). The inset is identity for every in-cell pixel under nearest sampling (the clamp bound is the edge texel's center — no art rescale or shift, pinned by test) and is the standard gutterless-atlas containment under linear. Square and diamond variants both carry it; the cooking wash keeps rawtileUv. Same-class exposure existed in the three-era material (uv().mul(uvRect.zw).add(uvRect.xy), no inset) — long-standing, not a lume regression. - Fixed the zombie-host class in client-auth rooms (ledger 1237): place-host assignment elected ANY Ready client, so a non-simulating instrument client (the thin-client stress harness — it ignores
host.assigncontrols and simulates nothing) alone in a room became the place host forever: healthy input cadence kept it "alive", nothing ever simulated, andrun_script/server-write forwards died in fence timeouts — strictly worse than an empty room. Fix is a host-eligibility opt-out riding the existing join payload, never a new authority path: a client appends?hostEligible=falseto the gameplay socket URL, the network worker carries it on the existingconnection.attachingress message, andisEligibleConnection(shared by place-host assignment AND the sim-lease reconcile sweep) requireshostEligible !== false. Absent field = eligible, so old clients and pinned engines are byte-for-byte unaffected; every attach adopts the NEW socket's declaration, so a later session on the same clientId never inherits a predecessor's opt-out. A room whose only occupants are ineligible behaves exactly like an empty room today: unhosted (frozen, not server-simulated), server-originated writes final at the canonical copy (no forwards, no fence timeouts), re-hosted at a bumped epoch by the next eligible entrant. The thin-client stress harness now declares the opt-out on every socket URL. Migration/epoch machinery untouched. - Static mesh colliders map again for schemeless
cdn/…asset paths (the 5.2.1-only zoo-statics regression: playground trees/rock/swap-car collided as placeholders for the whole session with[mantle] static collider failed to mapspam, A/B-isolated against 5.2.0). Two halves in the collider-asset lane (physics/systems/collider-assets.ts):- URL half (root): the lightweight extractor lane now roots schemeless relative CDN paths (
cdn/model-x.glb→/cdn/model-x.glb, the asset service's ownnormalizeAssetUrlsemantics) before fetching.isMagicCdnUrlaccepts the schemeless form (it URL-parses against a base), but the fetch used the string verbatim — in the browser runtime worker that resolves against the client bundle directory (/v4/client/<hash>/cdn/…), a deterministic 404 for every such asset. The lane had been silently rescued by the THREE.js fallback since it shipped. - Classifier half (what 5.2.1 broke): #8936's verdict classification skipped the THREE.js fallback for EVERY
MagicCdnFetchError, but only a classified verdict — 202 still-generating, or a server-stated window (429 quota / tombstoned generation,retryAfterMs) — is an answer about the ASSET that the fallback would just re-receive. The skip is now scoped to exactly those classes; an unclassified failure (bare 404/5xx, no verdict window) falls back to the asset-service lane again, which resolves URLs independently and holds its own cache + retry ladder. The ledger-1025 park contracts (still-generating ladder, verdict windows) are untouched and pinned.
- URL half (root): the lightweight extractor lane now roots schemeless relative CDN paths (
- Rapier 3D rotation readback preserves quaternion w = 0 (#9096):
readBodyRotationcoerced w withtoFiniteNumber(raw.w) || 1, so the legitimate w = 0 of any exact-180° rotation read back as w = 1 and re-normalized into a wrong orientation; the fallback-to-1 now applies only when w is non-finite.
KNOWN OPEN AT FOLD (5.2.1 staging draft, 2026-07-13 — named honestly for the walk and the release packet; the draft:false flip is the train's separate human-gated step):
- Houses / transform-fighting cluster — RELEASE GATE, BLOCKED: multi-part assemblies oscillate between two poses on master (the 07-13 houses briefing). Receipts: two transform families are both replicated AND forwarded, and lume reads the local family via the presentation smoother while world-family forwards exist — two writers fighting at the presentation seam. Sim state is correct per tucker; renderer-read side, not physics. The 5.2.1 released flip is blocked pending jure/tucker's local session.
- 2.5D sprite TAA blur: motion vectors are absent for sprites under TAAU, so moving sprites smear/ghost under the temporal resolve. Jure's ruling: known design gap, its own thread — P0 per tiger. (World text landed its velocity writes in this draft; the sprite lane remains.)
- Text motion vectors: the remaining text surfaces on the same MRT velocity lane — plan p-0ea8336e in flight.
- Flat-world horizon: the deck streaming radius ends before vision does, so flat worlds show a content edge at the far horizon (ledger 1276) — a design item, not a regression.
- Audio clip quota cap now parks gracefully and recovers when clips free up, with a Savi-visible budget diagnostic (Jul-13 speech findings, #9087).
- 10x decoration deck limits on desktop (Jacob's ruling, #inventors 2026-07-13: "fuck it 10x the upper limit. make sure rungs degrade so we don't blow mobile memory limits"): the desktop tier baseline carries
decorationBudgetScale/decorationDistanceScale10 (geometry-budget.ts) and the desktop scatter admission ceiling rises 200k → 2M instances (perf-static-data.ts SCATTER_BUILD_INSTANCE_BUDGETS) — the default deck builds 2,000,000 instances reaching 1,120 m (was 196,775 @ 112 m skirt). Measured basis: the 07-13 flat-meadow L4 ladder (real GPU timestamps, eye-height worst-case view) — 10x is GPU-time-flat vs 1x (far rings thin under the LOD keep, blade screen size collapses); the honest cost is instance-buffer VRAM (~200 MB at ~100 B/instance — instances 80 B + survivors 4 B + bounds 16 B — for the default deck). Tablet/phone tiers hold their shipped scales and ceilings (unmeasured silicon — the ruling's named mobile-memory hazard), and the ladder's geometry cuts compose multiplicatively on the new baseline (10 × 0.5 / 10 × 0.25), rebuilding the deck smaller so instance VRAM sheds with the rung. NEUTRAL_GEOMETRY_SCALES is now its own identity object instead of aliasing the desktop baseline (composing a baseline with identity must not square the 10s); on desktop the rung-0 admission clamp is now the routine deck shape (every shipped deck lands exactly at the 2M ceiling), not a pathology signal. - Rim height ramp for the decoration deck (jacob's occlusion-by-height pick, #inventors 2026-07-13): ground-cover cards (grass-kind + card sprites ≤ 1.5 m — the same class that defaults to the 80 m card far) grow height-only from 1.0× to
DECOR_RIM_RAMP_MAX(1.75×) over the last 30% (DECOR_RIM_RAMP_START0.7) of the item's own draw distance, holding max through the #8691 far-skirt fade — so at eye height the taller rim occludes the bare terrain band beyond the streaming radius on flat worlds instead of the field ending flush with the ground. The sampler carries the item's far in the header's spare float (countPad.w, 0 = off) and scales only the instance matrix's up column, so width/wind/tint and the fade's own scale-to-zero are untouched; the ramp is a pure function of placement distance (deterministic per seed), placement and counts are byte-identical (zero new instances), and per-entry cull spheres grow by the ramp max so tall rim blades never pop at frustum edges. Tall cards and primitives keep their authored silhouette (rimRampFar0). - World text billboards now resolve current and previous camera-facing poses in the vertex shader, eliminating camera-driven instance-buffer repacks while retaining entity-motion vectors. The TAAU path splits fully opaque glyph interiors from the blended antialiasing fringe, keeps distance fading continuous, preserves motion history across appearance-only edits, and uses the renderer's cutout ordering for discard-heavy cores. Scene-view captures evaluate the fade at their final output resolution.
textandsign.textnow support the sprite-compatiblesortingLayerandsortingOrderfields in 2D scenes. Explicitly layered text joins the sprite depth band; unlayered and mounted text keeps physical depth. Orthographic pixel snapping now reaches live, synthetic-harness, and recorded-replay text consistently.- Reverted the steady-state camera divergence-adoption gate (#9059, squash 5a2d7822; ledger 1265). The gate false-adopts on self-integrating authored camera rigs that keep private angle state: a script that integrates its own yaw/pitch at its own sensitivity from its own base and publishes once per update runs a second integrator against the renderer's display-rate one — two integrators at different gains diverge without bound, the ingest-interval echo envelope only extends directionally (2× headroom in the direction of motion), so the slower integrator falls off the BACK of the envelope and
adoptAuthoredAnglessnaps backward every few ingests (~49.5° sawtooth reproduced while rotating). No threshold fixes cumulative unbounded divergence, and extending the envelope backward would un-fix the static death-cam grip the gate shipped for. Camera steady-state behavior is restored to exact 5.2.0 semantics: custom-kind-only angle mirror (writeCustomCameraAnglesBack), parameters-only steady-stateupdateConfig, no divergence-adoption path. The ruling's durable shape (write-provenance authored-authority latch, #9059's own named follow-up) is ledgered separately and rides a later fold. - Scripted-primitive transparent draws now use the shared sort-order.ts convention (planar view depth + renderOrder fold) instead of radial camera distance, restoring paint-order parity with sprites/text/effects (resx's parity report, #8776). Draw-order change only, and only for transparent scripted-material primitives — three-era parity restored.
- Reverted the 10x desktop terrain-decoration instance raise (#9095, squash 2598900b). The raised budget overflows the default 128MiB storage-buffer binding on dense default decks: the device-budget clamp caps instance COUNT (2M) but nothing caps binding BYTES, so a new game's default meadow deck binds 2M × ~77 B ≈ 154,838,720 B > the 134,217,728 B default
maxStorageBufferBindingSize(the adapter supports 4GiB, but we never request it via requiredLimits) — instant GPU validation failure at CreateBindGroup ("terrain decor sampler", entries[8], compute stage) on first load. Desktop returns to prior values (decorationBudgetScale/decorationDistanceScale1, scatter admission ceiling 200k — geometry-budget.ts / perf-static-data.ts SCATTER_BUILD_INSTANCE_BUDGETS) while a bytes-aware budget + adapter-limit negotiation is designed. The rim height ramp (#9084) is unrelated to buffer sizing and stays.