Spawn
spawn / swhat we're building

pinned

start herewhat spawn isfaqfrequently asked questionsthe betthe spawn bet

updates

engine v5.0For Real1 weekengine v4.6Atelier1 weekengine v4.5Surface Tension2 weeksengine v4.4Solid3 weeksengine v4.3GroovyMay 13, 2026engine v4.2ContinuumMay 9, 2026engine v4.1FoundationsMay 4, 2026engine v0.1GenesisApril 29, 2026

pinned

what spawn isstart herefrequently asked questionsfaqthe spawn betthe bet

updates

For Realengine v5.01 weekAtelierengine v4.61 weekSurface Tensionengine v4.52 weeksSolidengine v4.43 weeksGroovyengine v4.3May 13, 2026Continuumengine v4.2May 9, 2026Foundationsengine v4.1May 4, 2026Genesisengine v0.1April 29, 2026
← All posts
← All posts

engine v5.0.15

Engine v5.0.15

June 12, 2026

A patch in the For Real line.

what's new

  • Multiple moving shadow-casting lights now all keep their shadows — previously only one moving light at a time could.
  • Fixed long lag-behind sessions endlessly rubber-banding — the game now does one clean catch-up resync instead.
  • Fixed buttons and controls permanently dying after a slow game load — presses no longer "work for a split second, then snap back". If your game's UI broke when you updated past 5.0.7, this is that fix.
  • Lights are now easy to select in god mode — the click target is the light itself, not its entire glow radius.
  • Fixed a rare condition where the game stopped responding to your controls after a server restart until you reloaded.
  • Fixed a bug where a world's ground and collision could fail to load (players falling or floating at spawn) when an asset server was slow — terrain now builds reliably even when assets are having a bad day.
  • Worlds with freshly generated models load more reliably — slow first-time asset processing no longer makes physics and models fail to show up until a retry.
  • Ground textures in 2D games no longer shimmer while the camera moves.
  • If your world ever stops drawing, the engine now notices within seconds and tells Savi exactly what happened — that it's a graphics-driver problem a browser restart fixes, not a bug in your game — instead of leaving a silent black screen.
›technical notes
  • ShadowAtlasScheduler's dirty render queue is now ordered oldest-rendered-first (the stale queue's existing age idiom; never-rendered faces lead, score breaks age ties) instead of score-first. A moving shadow-casting light re-dirties every face every frame, so under a per-frame face budget smaller than the standing dirty set the score-first order replayed the strongest light's first budget faces forever — starving every other dirty light AND the strongest light's own remaining faces whenever its face count exceeded the budget (high tier: rendersPerFrame 4 < a point light's 6 faces, so no orbiting point light could ever complete acquisition and ready/fade stayed pinned at 0 — the jure 1-of-3 field repro: three shadowed point lights, two on orbit/bob behaviors, only the static one ever cast). Age ordering round-robins the budget across all dirty faces, bounding per-face staleness at ceil(dirtyFaces / budget) frames: every moving light keeps a slightly-lagged shadow instead of one light keeping a live one. The budget cap itself is unchanged — fairness redistributes who gets the renders, not how many happen.
  • Client behind-server resync (ledger 631, dump 7797adb0 "Ark Caves"): a client whose local clock fell moderately behind the server's tick (under the suspend-resume cut's instant threshold, beyond what the lead-recovery slew was healing — Ark Caves sat ~40 ticks behind for hours) used to fire a mismatch correction on every compare (20,554/20,554 resims, ~81ms/s replay burn) because its input frames reached the server after their ticks were already simulated. Behind is now a resync condition, not a mismatch condition: (a) prediction compares are suppressed while the client is strictly behind the newest authoritative tick (a properly leading client keeps full drift detection — the gate is strictly client-behind-server), and (b) a fall-behind that sustains above 2× the join-style lead for 30 consecutive ticks with fresh authoritative arrivals triggers ONE full resync through the existing suspend-resume cut (authoritative-snapshot adopt + clock re-lead + input baseline reset + prediction recapture — never fabricated state, tick-exact input apply untouched). Flap-guarded: one resync per 15s; a re-trip inside the window is held and escalated loudly once.
  • Clock rebases size their lead for the worst tolerated RTT when none is measured (ledger 637, the Vacuo 5.0.8 input-death regression). RTT samples ride input-frame acks, so a boot-time stall that blocks the server before any frame is acked reaches the hard-adopt rebase with RTT=null; the old RTT-blind lead (~8 ticks, no transit term) parked the client clock inside the server's consumption horizon for any real RTT above it. Every subsequent frame arrived too_late — hard-dropped, actions acked-as-dropped, the client un-applying each predicted action (buttons "work a split second, then snap back") — and since no frame was ever accepted, no ack ever minted an RTT sample, so the next adopt re-rebased blind: permanent, self-sealing input death, invisible to the behind-server detector (the clock stays nominally ahead of the newest received snapshot while behind the horizon). Both rebase sites (hard-adopt and suspend-resume cut) now fall back to JOIN_LEAD_MAX_RTT_MS when RTT is unmeasured: overshooting strands the clock slightly too_far — the side with the buffer's empty-rebase rescue and the input-throttle grind — while undershooting has no rescue by contract. Field-confirmed on master before the fix (arrived=2262 tooLate=2261 appliedPresent=0, self-heal re-poisoning within 5 minutes).
  • God-mode lights now pick by a compact bulb-sized handle instead of effectively by their photometric range. A light has no raycastable geometry of its own — its pick surface is the editor visual, and while selected that visual spawned two range-radius torus rings (range-flat/range-vertical, radius = min(distance, 16)) whose hits resolved up TomeParent to the light: every click inside the glow radius could land on a ring and re-select the light (the jure field report: "very difficult to work with", selection low-percentage, a huge mostly-empty sphere competing for picks). Three changes: (1) editor-visual GUIDE geometry (range rings, beam edges, emission wireframes — anything nested under …/__god_mode_visual/) is now non-pickable in both god-mode pick consumers (hover's pickable() and click-to-select's hit filters, the same idiom as the spline connector and the bed-footprint ribbon) — the visual ROOT (the marker) stays pickable, it is the light's click target; (2) the light marker is bulb-sized (octahedron radius 0.25 ≈ 0.5-unit click target, fixed, never scaled by distance) instead of the 0.11/0.14 gem all markers shared; (3) the selection-outline subtree walk skips guide entities, so the silhouette cue traces the light + its marker — never the range-radius rings (the giant artifact-y outline in the screenshots). The range visualization itself is unchanged: rings still appear only while selected, at the real falloff radius, as hairline wireframes. Regression gate: editor-visual test pins every pickable entity under a light to ≤ 0.6 u bounds for distance 5/18/100.
  • Input-ingress health observability (ledger 631, dump 7797adb0 "Ark Caves"): the server's per-client input acceptance counters (framesReceived, framesDroppedInvalid, the per-client truth row, the buffer's too_late/too_far tallies) were stats-only — visible in a debug dump, never logged — so a wholesale input-acceptance failure (server booking absent for every tick of a connected, egress-receiving client for hours) ran past two real diagnoses. The netcode egress system now samples a per-client ingress-health monitor (engine/runtime/server/input-ingress-health.ts) every tick: (a) one tome.input.ingress_health info line per client per ~30s carrying the window's arrival/apply/drop deltas and absent-tick ratio, and (b) a loud tome.input.ingress_wedged warn when ≥90% of a ~10s window's ticks book absent while the same socket delivered egress — the half-open wedge signature (ingress dead, egress alive). Suspended and grace-detached connections are never sampled; clients that never enqueued an input frame (singleplayer authorities, spectators) emit nothing. Observe-only: no engine behavior change.
  • Input-ingress self-heal (ledger 631, dump 7797adb0 "Ark Caves"): when a client's input ingress wedges half-open — every tick books an absent ack while the same socket keeps delivering egress (the ingress-health detector's condition) — the netcode server now forces ONE input-session re-handshake instead of letting the session stay input-dead until the room dies. The cure reuses existing handshake shapes only (no new wire message, no invented state): resetClient tears down the per-client input session (the tick buffer whose consumption floor gets poisoned when a fresh buffer seeds its baseline from a reconnecting client's stale-clock frames, plus pending acks/flow control), the Loaded welcome re-grants the session's live entities (the hello that arms client input sampling), and a dedicated reset snapshot (resetProjection) triggers the client's existing re-sync (handleProjectionReset: clock re-lead + input baseline reset). Flap-guarded: one re-handshake per client per 5 minutes; a second wedge inside the window logs a loud tome.input.ingress_wedge_unhealed error once and leaves the session for a human. Healthy clients can never trigger it (the detector requires a ≥90%-absent window with egress delivered, and suspended/grace-detached connections are never sampled).
  • Fixed the fleet chunk-build wedge (2026-06-12, ledger 634): a job whose fn never resolved (e.g. a glb-bounds fetch against a 524ing Magic CDN) leased its server job-pool worker forever — deadlines were only checked after the fn returned, so nothing errored and every job submitted afterwards (all terrain chunk builds) starved silently until the queue-side stuck window failed them en masse.
  • Job deadlines are now a live race in the worker harness: a job that outlives its deadlineMs is aborted (its env.signal fires) and fails with deadline_exceeded immediately, freeing the worker. deadlineBehavior: "warn" keeps the legacy accept-late-results semantics.
  • The server pool gained a main-thread lease watchdog (60s cap): a worker whose job outlives the cap — sync-spinning or abort-deaf jobs the in-worker race can't reach — is terminated, the job fails loudly with lease_expired, and a replacement spawns.
  • A worker that exits cleanly while running a job no longer silently strands that job; crash requeues are capped (2) before the job fails with worker_crash instead of crash-looping the pool. Async worker-spawn failures now drain the queue with pool_unavailable errors instead of stranding jobs.
  • glb-bounds fetch attempts are hard-bounded at 4s (transient, so in-worker retries stay reachable) instead of holding a connection ~100s until Cloudflare 524s it.
  • Observability: terrain chunk-build failure / chunk-rescue / collider-watchdog logs now route through the identity-stamped logger (appId/roomId/engineVersion instead of appid:unknown), and job workers inherit the bound log identity at spawn.
  • Fixed the Magic CDN async probe skipping every server-side asset load (ledger 636, cold-asset 524s): isMagicCdnUrl only matched /cdn/ paths, but the server-side AssetService rewrites /cdn/ → /magic/ (convertCdnPathForServer) before the loaders run — so container-side loads (model warming, collider hull fetches) never sent x-magic-cdn-async and held the connection open through a 3–6 minute cold cook, dying as proxy 524s. /magic/ paths now take the probe path too: cold assets answer 202 + Retry-After and the load retries on cooldown instead of riding a doomed connection.
  • 2D pixel-art filter grammar: tiling surface textures (texture-* filename grammar) now keep linear+mips sampling instead of inheriting filter: "pixel" from a -pixel- moodboard scope or pixel- name. The pixel inference sets min AND mag to NearestFilter, leaving the KTX2 mip chain unused — a ground tiled 48×48 sits several mip levels down, so every fractional camera offset re-picked texels and the floor scintillated with camera motion (4 sightings in wave-7 2D testing). The fix extends the existing plate exception (backdrop-*/strip-*/floor-*) to the tiling-surface class via isTilingSurfaceTextureId, mirrored in both inference copies (renderer-asset-service + worker asset service). Sprites, tilesets, and character art stay nearest/crisp; authored sprite.filter still wins both ways. The general alternative (min=NearestMipmapLinear for the whole pixel class) was considered and deferred — mip averaging thins alpha on cutout sprites.
  • Sim-tick timing ring in the debug dump (ledger 631, Tucker): per-tick sim durations for the last ~45 ticks, client and server, so a dump answers "is the sim itself falling behind the tick budget" directly and crosses with Datadog app metrics to find slow systems. Client: the runtime books each MAIN simulation phase's ms into a fixed 45-slot ring (O(1) per tick, zero per-tick allocation; resim replay steps excluded — burst work already accounted by ResimulationStats), shipped on every perf rollup as payload.simTiming and retained by the kiln parent for the dump capture. Server: each executed tick's ms rides ServerRuntimeTelemetrySnapshot.tickMsRing and the room's /admin/input-stats dump capture. The dump summary renders both as compact min/p50/p95/max lines plus the raw rings. Observe-only: no engine behavior change.
  • World-draw liveness sentinel (ledger 633, dump d96c25f3 "Spawnblock"): detects the silent GPU-process-reset class where a browser reset/eviction zeroes the vertex-pulled voxel bucket arenas / compute-cull storage WITHOUT a device-lost event — frames keep presenting at 2.5ms, every error rail stays empty, the world draws nothing. CPU info stats are structurally blind here (an indirect bucket counts 6 indices × 1 instance in info.triangles whether it draws a million quads or zero), so the trip predicate is CPU expectation vs GPU truth: the CPU-mirror chunk tables (bounds sphere + transform + live quad count — the upload SOURCE for the GPU buffers) walked against the camera frustum say how many quads SHOULD draw, while a ~20-byte async readback of the cull pass's GPU-written indirect instanceCount says how many DO. Expected ≥ 64 while drawn = 0, sustained 8s across consecutive 2.5s probes, fires ONE diagnostic (renderer-world-draw-stalled, allowlisted to Savi's getLogs + DM) carrying a trip-time forensic payload (bucket mesh flags, arena residency, GPU chunk-visible readback, last cull dispatch age, chunks installed, texture bytes). One-shot per session; a live probe re-arms it. Per-frame cost is O(1) — the expectation walk and readback ride the probe cadence, forensics run once at trip time.
  • Voxel-bucket containment weight (#7171 lineage, in the post-#7176 alarm path): when handlePipelineResourceLimitExceeded hides an object whose material is a terrain/voxel-bucket/* pipeline, the diagnostic now says what actually happened — hiding that one mesh hides the WHOLE voxel world (device-limit fallback, world-level impact, not a game-script bug) — instead of the generic "hid the object" line.
  • console.warn forwarding (iframe→parent, ledger 268 lane): the kernel client now forwards console.warn lines that start with the engine's own prefix markers (the [renderer]/[worker-browser-host] family) as source: "console.warn", on their own 10/min budget so a warn storm can never starve error forwarding; creator/third-party warns never leave the page. The renderer host re-emits warn-class worker diagnostics (renderer-build-failed, the pipeline-guard rejections) on the page console as [renderer] warns so they ride the new lane, and the world-draw stall re-emits as console.error so the world-down class rides the existing error forwarding. kiln's parent listener accepts the new source and logs it to Datadog at WARN status (apps/kiln/lib/kernel-iframe-errors.ts).