Spawn

Make Games with Words

Learn about spawn
spawn / swhat we're building

pinned

start herewhat spawn isfaqfrequently asked questionsthe betthe spawn bet

updates

engine v5.0For Real3 daysengine v4.6Atelier1 weekengine v4.5Surface Tension2 weeksengine v4.4Solid3 weeksengine v4.3Groovy3 weeksengine v4.2ContinuumMay 9, 2026engine v4.1FoundationsMay 4, 2026engine v0.1GenesisApril 29, 2026
← All posts

engine v5.0.7

Engine v5.0.7

June 7, 2026

A patch in the For Real line.

what's new

  • Games now automatically turn down expensive graphics on hardware that can't keep up — effects cadence first, then shadow distances, then bloom and resolution, and only on the struggling device. Same game, same content, smooth on more machines; the device remembers where it landed so the next session starts there.
  • When something goes wrong inside a game, the Spawn team can now see it and fix it faster — crashes and errors in your worlds reach our monitoring instead of vanishing into the void.
  • Fast place switching no longer leaves the screen frozen on a stale frame with graphics errors. A rendering hiccup during a place swap could previously wedge the renderer into a state where nothing new reached the screen until another effect kicked in — the frame now always presents to the canvas.
  • Effects no longer hitch the game the first time they appear on screen.
  • God mode effects are now dramatically cheaper — fire, rain, snow and friends run on the GPU, so you can scatter them around your world without the lag. They're also real effect scripts now: ask Savi to open scripts/effects/fire.fx.js and make it yours, and tune Intensity/Scale right from the chip strip.
  • Walking back into a place you just left is fast now — the world you built there stays warm instead of rebuilding from scratch, so door round-trips (cabin to wilderness and back) no longer freeze the game.
  • Big foggy worlds load lighter: the engine no longer prepares terrain your screen can't actually see past the fog.
  • Effects that mix several sprite textures — most legacy fire/smoke/sparks/rain presets and any effect using a texture pack — now run on the GPU like everything else. The last big class of laggy particles is gone.
  • The F2 inspector's Parameters tab now has a Tier control: see what tier your device detected and force another one to taste your game as a phone/tablet or at low/medium/high/ultra quality (reload applies it). The label always shows forced vs detected, so you know exactly what you're looking at.
  • Fixed the root cause of a rare mid-session flicker-and-repair (or, before 5.0.3, a permanent black screen) on some graphics setups — usually after resizing the window or entering fullscreen with refractive water or similar depth effects in the scene. The render pipeline no longer disagrees with itself about MSAA, so the repair never needs to fire.
  • Singleplayer mode no longer drops fps while moving on terrain games — the engine was building and tearing down the same far terrain ring about once a second, and now it doesn't.
  • World text with characters the font doesn't have — emoji, fancy symbols — now shows a small gap instead of a ?. Curly quotes, em-dashes, ellipses, and accented letters render as their plain-text equivalents, so "the keeper's door" finally reads right.
›technical notes
  • Adaptive quality governor (ledger #193): tier selection was capability-only, so a device that ADVERTISES desktop-class WebGPU (2017 iMac → "ultra") got desktop shadows/MSAA/effects regardless of how it performed. The quality governor now has a third, budget-rail-driven effects axis that consumes the frame-budget guard's own windowed measurement (max of cpu / gpu / GPU-bound presented interval, with the #188 startup/compile/load grace) — no parallel sensor — and steps quality down under sustained ≥10s degradation past the 28ms line.
  • Ladder order is the locked #228 wave-2 ruling — effects before resolution: E1 sky-capture cadence + shadow refresh budget (invisible at rest), E2 sun/local shadow distances one tier down, E3 shipped-floor distances + bloom pyramid paused, then cross-escalation only: render scale (existing QUALITY_LADDER), then geometry rungs (existing GEOMETRY_LADDER). Every step value is a shipped lighting-tier preset row (one config surface, no parallel quality vocabulary), applied through the existing seams (DPR plumb, no-bloom post topology, geometry scales, plus per-frame identity-reconciled runtime knobs on SunCascadeShadow maxFar, the shadow-atlas render budget/fade distance, and the sky environment capture threshold).
  • Stability: sticky-down only in-session (no mid-game quality pops — re-promotion is cross-session), 25/28ms hysteresis band, 8s settle between steps, and steps never fire during load/compile storms. On constrained devices the shipped mobile wall-rail behavior is untouched; the budget rail drives only the effects axis there (strictly slower + higher-threshold, so mobile resolution always reacts first).
  • Persistence: the landing tier is stored client-side (spawn.adaptiveQuality.v1) keyed by a capability fingerprint (GPU identity + device class + browser major) so the next session STARTS at the landing instead of re-suffering the descent. Construction-frozen knobs ride persisted boot steps: a session that stays overloaded ≥60s at the full in-session floor makes the NEXT session boot with MSAA off, then with the lighting tier stepped down (froxel grid, shadow atlas, cascades, sky LUTs, fx arena all re-init cheaper) — the hardcawcanary manual fix, automated. A fully calm session relaxes one step for the next session; a fingerprint change clears everything.
  • Honesty: every step (and a reduced-quality boot) logs an adaptive-quality-step engine diagnostic — getLogs-visible, log-only (no DM; no new perf-notify category; the 1/hr pointer gate untouched); chronic frame-budget warnings now name the governor's current landing as context; F2's Budget row and the perf rollup carry the effects rung, transitions, and boot steps.
  • Client error observability (ledger 268): the game iframe now forwards uncaught window errors, unhandled promise rejections, and console.error calls to the parent page over the existing postMessage rails (spawn:kernel:client-error, client-error-forwarding.ts). The kiln host re-emits them through its Datadog browser-logs pipeline with origin:iframe — the iframe itself loads no observability SDK. The channel is bounded: max 20 forwards per rolling minute, identical (source, message) deduped for 30s with a suppressed count on the next forward, messages truncated at 1,000 chars and stacks at 4,000. Payloads carry appId/variantId (from __SPAWN_CONTEXT__) plus roomMode/engineSemver (from the iframe URL). Worker crashes ride the existing worker-browser-host console.error relays, so no in-worker capture was added. Old kiln parents ignore the unknown message type.
  • Container logger (leg 3a fix): logger.ts now resolves stage from SPAWN_STAGE ?? DATADOG_ENV and reads the DATADOG_SERVICE/DATADOG_SOURCE/DATADOG_LOG_TAGS env vars that game-container.ts has been passing all along. On the cf-edge path nothing sends x-spawn-stage, so every prod/staging container previously logged to Datadog as stage:dev — in the colorized dev format, shipping ANSI codes to the intake. Prod/staging logs are now JSON-formatted and honestly tagged (resolveLoggerIdentity + pinned tests).
  • Fast place swapping could leave the screen frozen on a stale frame while the console repeated GPUValidationError: [Texture "output"] usage (TextureBinding|RenderAttachment) includes writable usage and another usage in the same synchronization scope (ledger #262, /zoo place swaps). Pre-existing on every 5.0.x — surfaced, not introduced, by the 5.0.6 window.
  • Mechanism: the fork's PassNode.updateBefore has no try/finally around its nested renderer.render(scene, camera) — a throw mid-scene-render (place-swap churn is the throw factory; the frame loop's catch keeps the loop alive) skips the setRenderTarget restore and latches the renderer's sticky render-target state on the scene pass's own target. Subsequent no-look frames ran postProcessing.render() with no explicit target: the output quad rendered INTO the latched scene-pass target while its material graph SAMPLES scenePass.getTextureNode("output") — one pass attaching and binding the same texture, rejected by Dawn at encoder finish, so frames stopped presenting until a look-active frame (which sets its target explicitly) happened to heal the latch.
  • Fix: the no-look path now pins the canvas target explicitly — renderer.setRenderTarget(null) before postProcessing.render() (post-processing.ts), mirroring the look-active path. The quad never inherits ambient render-target state, killing the entire latch class regardless of which throw latched. The fork-side hardening (try/finally around the nested render in PassNode.updateBefore) goes upstream at the next fork bump — no fork respin here.
  • Pinned red→green: post-processing-latched-target.test.ts drives the real fork PassNode.updateBefore + the real engine post chain headlessly — constructs the latch (throwing nested render leaves the renderer aimed at the scene-pass RT, whose "output" texture the active output topology provably samples), then asserts the next no-look quad render is issued against null (pre-fix it inherited the latched RT — the conflicting pass verbatim). Doubles as the fork-bump tripwire for the try/finally.
  • Fixed runtime shader compilation on fx deck appearance (ledger #263). The GPU fx batch path minted a fresh storage node per population deck, and WGSL names storage buffers NodeBuffer_<node.id> — so every same-shape deck generated different shader text, missed three's program/pipeline caches, and compiled a brand-new render pipeline synchronously at its first draw. The storage node now carries a stable name (fxGpuRenderInstances), making same-shape deck WGSL byte-identical (pinned by fx-deck-wgsl-identity.test.ts for both batch paths).
  • Every brand-new fx batch (CPU and GPU) now rides the async material-compile queue at creation — not just the first batch of each blend × align shape — so a deck's pipelines are compiled off-frame before its first draw. The only queue skip left is the rebuild of a live, visible family (capacity regrow / arena window move), which is flicker-sensitive and now a true cache hit by WGSL identity. Regrows that replace a never-seen batch (a first burst bigger than the initial capacity, or a rebuild racing its own first compile) re-queue.
  • The fx-gpu spawn compute pipeline is warmed on the arena's first compute (zero-work dispatch, threads early-out on the empty spawn map) instead of compiling at the first frame something actually spawns.
  • God-mode Effects-tab prefabs (fire, smoke, dust, sparks, magic, leaves, fireflies, rain, snow, embers, mist) now place fx-arena decks instead of legacy particles emitters: arming the tool vendors a first-party scripts/effects/<kind>.fx.js into spec.scripts (skipped if the game already defines that path) and the placed object carries fx: { script, params: { intensity, scale } }. The legacy payloads were all GPU-ineligible (sprite.textures per-particle variance; sparks added a ribbon sink), so every placed prefab ran on the CPU particle backend — per-particle JS sim on the renderer worker plus one draw batch, one material, and a full dynamic-buffer re-upload per texture per frame (~55 batches for one of each kind). The new decks are faithful ports (same rates, lifetimes, motion, size/color/alpha curves) authored to be GPU-eligible: one texture per population, velocity-stretched sparks instead of ribbons, a two-population leaf mix.
  • effectIneligibility is exported from renderer/three/fx-gpu/backend.ts so first-party content can regression-test GPU-arena eligibility against the real routing policy.
  • Existing games are untouched: properties.particles specs keep working on the legacy path (its lowering/VM test corpus is frozen in engine/particles/__tests__/legacy-emitter-fixtures.ts), and already-placed emitters keep their payloads.
  • Acute frame-drop (hitch) telemetry, ledger #267. Existing metrics average frame times over windows, so a single 100–250ms spike — exactly what an fx compile on frustum entry, a place-swap collapse, or a god-mode prefab build feels like — was invisible, and DD percentile aggregation is disabled on most distributions. New HitchDetector (engine/renderer/hitch-detector.ts, pure state machine in the frame-budget-guard mold) measures the wall gap between consecutive RENDERED frame starts in the renderer worker and buckets gaps over the 50/100/250ms ladder (exclusive buckets: 50_100, 100_250, 250_plus; constants in one place).
  • Visibility robustness: a backgrounded tab is NOT a hitch. Two guards: the host's forwarded visibilitychange drops the gap baseline (notePause, same signal the quality governor consumes), and a 2.5s gap ceiling (mirroring the renderer's FRAME_LOOP_PAUSED_AFTER_MS contract) classifies occlusion parks — which fire no visibilitychange — as pauses, never hitches.
  • Attribution at capture, from existing rails only (no parallel sensors). Each hitch is attributed with the PREVIOUS frame's facts — the frame whose work and aftermath filled the gap: compile_sync (node-builder cache delta inside the render call — the #152 #46 counter's signal, now hoisted per frame), compile_async (compileQueued start, the guard's compile-grace source), terrain_ingest (per-frame voxel chunk geometry installs, hoisted and shared with the perf sampler), collect_backlog (#164 backlog count), asset_load (the warmer's realPendingLoads, reused from the frame-budget guard's 250ms sample so the manifest is never re-walked per frame), gc_hint (performance.memory heap shrink ≥ 8MB across the gap — Chrome-only, named heuristic), gpu_bound (gap > 2× the frame's measured CPU work — the guard's unexplained-interval idiom from #199), else honest unattributed. Phase tag startup (first 10s) vs steady — tagged, never suppressed.
  • Emission: counters, not distributions — aggregation-proof. Window counts ride the existing perf rail unchanged in shape: renderer worker 1s sample (RendererPerfSample.hitches, absent on hitch-free windows) → sim-worker 15s rollup accumulator (RendererRollupBlock.hitches, counts summed per bucket × cause × phase, capped at the 48-line tag space) → kiln perf-rollup route → DD counter spawn.kernel.client.renderer.hitch.count tagged hitch_bucket/hitch_cause/phase (route half ships as its own additive commit; the route strips the key until then — ordering-safe).
  • Debug-dump breadcrumbs: the detector keeps a session-cumulative worst-10 ring (timestamp, duration, verdict, raw facts; ~2KB JSON, size-bounded by test). Snapshots ride the rollup only on change (worstRing), the studio surface retains the latest (kiln/core/kernel/hitch-breadcrumbs.ts), and the debug-dump capture includes it as diagnostics.hitches — "it froze" reports now carry receipts.
  • Tests: detector buckets/visibility/pause-ceiling/attribution-precedence/phase/window-drain/ring-bounds (hitch-detector.test.ts, 17 cases), rollup accumulation + ring carry (renderer-perf-rollup.test.ts), kiln retention (hitch-breadcrumbs.unit.test.ts).
  • Fixed the place-entry terrain build storm (ledger #261): entering a terrain-heavy place could hold fps at <=5 for 30+ seconds while the client cold-built the full extended-profile desired set (2,400-3,250 chunks through the place's heightAt/materialAt generator on the 1-2 client job workers — ledger #192's mechanism on the place-ENTRY path).
  • The extended-band visibility clamp (#149) now bounds the screen corner with the client's actual render-surface aspect instead of a worst-case 32:9 display (fallback until the host reports a surface). A 16:9 desktop with linear fog far 380 streams band radius 19 instead of 28 — a 53% smaller desired set on extended-high.
  • Client terrain streaming retains the chunks of the last heightmap place the local viewer left (the default-place never-evict rule extended to exactly one more place), and the client build system's place-switch reset keeps installed-output records, so returning to a just-left place re-validates resident chunks by inputs hash (budgeted, zero rebuild jobs) instead of rebuilding the whole place. Voxel places are not retained (renderer bucket arena slots must free for the next place); server streaming is unchanged.
  • Multi-texture sprite sinks (sprite.textures) now ride the GPU fx arena (ledger #269): texture packs render as one batch sampling a texture_2d_array (per-particle layer in the reserved instMisc.y lane — no new buffers or bind-group entries), the array composited at runtime from each layer's source raster, refcounted per ordered pack, content-scale compensated. One new material shape per blend × align, warmed through the async compile queue at creation. Packs >32 layers stay on the CPU backend with an explicit stats().ineligible reason. This was the routing gate that kept 11/11 legacy god-mode prefabs and every textureIds emitter on the CPU path.
  • Worker-start hardening (ledger #281): the per-boot container instance id is generated in a try/catch — workerd forbids global-scope randomness and the wrangler Worker bundle shares this module, so an uncaught throw killed every isolate at start (local dev + any worker deploy). Bun containers keep real per-boot ids.
  • Debug-dump capture (ledger #284): every 15s perf rollup now carries a misprediction breadcrumb ring (≤50 rows/30s from the SAME MismatchTracker evidence the F3 panel reads — tick, entity, component, drift/push/skew class, first differing srv/cli leaf) plus an SP/MP identity block (execution topology + AOI player count). The kiln dump summary renders mode, engine, per-place physics kinds, the mispred ring, and a scrubbed ring of the last ~30 parent+iframe console errors up front.
  • Fall-through + adopt-storm fix (ledger #285, P0): three composing client bugs made a player fall through their own terrain and rubber-band violently forever ([terrain/chunk-rescue] firing thousands of times, remediation rebuilds never healing, prediction resync: adopted authoritative baseline walls with a frozen mismatchTick).
  • Terrain chunk entities are now locally owned everywhere replication touches them. Chunk entities are dual-owned by design — the client installs its own collider plane (slim PhysicsBodyConfig + never-replicated TerrainChunkColliderPayload + WorldFeetPosition) on the SAME entity ids the server replicates edit baselines through (the place-global TerrainChunkEdits expansion). Three paths treated every replicated-class component on a chunk as server-owned:
    • The projection-reset sweep (room-delta-ingest) despawned edited chunk entities wholesale, destroying the client's mesh + collider payload while the surviving install bookkeeping (same entity id) blocked the rebuild. Resets now reconcile chunks in place; chunks the reset no longer shows get only their stale edit baseline stripped. AOI-exit delete rows for chunks (edits cleared by a terrain regen — the ledger #93 family) likewise strip the baseline instead of despawning the client's chunk.
    • The resimulation authoritative-state apply (resimulation.ts) stripped ALL replicated-class components from a chunk in the rollback scope when the server had no row at the mismatch tick (un-edited chunks never replicate) — including the freshly installed PhysicsBodyConfig/WorldFeetPosition. The physics cleanup then disposed the realized collider: the predicted CC free-fell through its own ground, and every chunk-rescue remediation rebuild was re-stripped by the next resim. The apply now reconciles ONLY the chunk-replicable set (TERRAIN_CHUNK_REPLICABLE_COMPONENT_NAMES: edits + membership for writes; edits only for removals — predicted edits still roll back to server truth, preserving the voxel-extrude fix). Chunk entities are never despawned as strays, with or without their ClientEntity tag.
    • Chunk create/update rows are filtered to the same set on ingest, so a row carrying the server's own (delta-suppressed, create-time-stale) collider config can never clobber the client's locally built config/payload pair.
  • Hard-adopt now clears the projection-reset replay anchor (pendingProjectionResetReplayTick). A reset whose baseline was already older than the resim cap when consumed (ingest backlog, main-thread stall) re-anchored every runResimulationIfNeeded at the frozen reset tick — live input frames re-armed it each tick, so the client cap-adopted on every 30-tick cooldown forever (frozen mismatchTick, zero normal corrections: the felt adopt-storm flicker). The adopt is a wholesale baseline replacement at least as new as the reset's, and it clears the input tail the anchor would replay — the anchor dies with it.
  • Boot terrain collider gate (the 5.0.7 TTI fast lane) verified unaffected: the shared collider-gate module still requires the support chunk's realized, payload-matched collider before first unpause; the fast lane only re-prioritizes the build. The #285 episodes start mid-session, and the same class exists on 5.0.6 (no TTI engine half) — the gate change neither created nor gates this bug.
  • F2 Renderer Inspector → Parameters: manual tier override (Ledger #293). A "Tier" control forces any shipped tier preset — device render tiers (phone/tablet/desktop: governor geometry baseline, wall ladder, MSAA/shadow/terrain-pbr floors) or lighting quality tiers (low/medium/high/ultra: lighting/sky/shadow config, effects-ladder baseline). Persisted in spawn.tierOverride.v1 (replaces the undocumented spawn.lightingTier.v1 knob), applied at renderer init so construction-frozen settings honor it; the display is honest ("forced — detected: …") and mid-session changes label "reload to apply". Forced tiers pin the governor's baseline (no persisted boot steps, no landing-state read/write) while the adaptive emergency rungs stay live.
  • Rooted out the GPU validation bursts the #187 recovery rail was built to survive (ledger #289, fork patch three-0.184.19-spawn.3.tgz). A depth texture with no render-target anchor — three's shared viewport depth buffer, the viewportDepthTexture singleton refractive water samples — answered its sample-ness question from whatever renderer state happened to be current at call time, and the three artifacts derived from that answer (the WGSL declaration, the bind group layout, the cached GPUTexture) are each created at a different moment: a frame-top compileAsync sees no bound target and a 0-sample currentSamples while the GPUTexture was created inside the 4x MSAA scene pass. Nothing bumps the texture's version to reconcile a drifted answer, so a freshly-recomputed single-sample layout meets the cached multisampled GPUTexture and every submit fails validation ("Sample count (4) … doesn't match expectation (multisampled: 0)" → Invalid BindGroup → Invalid CommandBuffer) until the burst rail rebuilds the chain. The fork's getTextureSampleData now applies two rules at the single seam all three consumers share: once the GPU object exists it IS the truth (layouts and shaders describe the texture that will actually be bound, whatever is bound when they build), and before it exists the evaluation reads the render context being prepared — compileAsync routes through the same framebuffer-target logic render() uses but never sets the public render target — before falling back to public state, so compile-time and render-time answers agree. The #187/#249 recovery rail stays as the backstop for exotic topologies (live consumers across a runtime MSAA flip); in the engine's topology the burst class no longer exists. Pinned by a CPU-side behavioral test driving the real fork evaluation/layout/bind-group code (bind-group-sampleness.test.ts) that doubles as the fork-bump tripwire.
  • Hybrid-GPU adapter selection (ledger #294): every navigator.gpu.requestAdapter() in the engine now asks for powerPreference: "high-performance". Without it, dual-GPU laptops hand the renderer the low-power INTEGRATED adapter — a discrete-class gaming laptop ran the whole game on iGPU silicon (34fps GPU-bound in the zoo). The contract lives in WEBGPU_ADAPTER_OPTIONS (engine/client/device-perf.ts) and is non-overridable at the renderer chokepoint (createRendererBackend).
  • Detection now measures the GPU rendering uses: the device-limit probes (renderer-backend.ts maxTextureArrayLayers, webgpu-limits.ts sampled-texture/sampler limits), the lighting-tier GPUAdapterInfo fingerprint, the boot wall's adapter probe, and the WebGPU telemetry summary all pass the same options. The WebGL fingerprint probe (detectGpuSummary) requests a powerPreference: "high-performance" context for the same reason — a default-power WebGL context on a hybrid laptop fingerprints the integrated chip and the tier ladder grades the wrong GPU (an RTX-class machine tiered as integrated Intel).
  • The renderer-backend texture-array-layer probe no longer requests a featureLevel: "compatibility" adapter — the engine never sets compatibilityMode, so the real device is created on a core adapter; probing a compatibility adapter read different (lower) limits than the device actually grants.
  • Kiln's pre-game GPU readiness check (hardware-acceleration.ts) probes the same high-performance adapter so it vouches for the GPU the engine will render on.
  • AMD APU tiering (the confirmed #294 fingerprint — an integrated Radeon 880M active while the discrete RTX idled): "Radeon … Graphics"-named integrated Radeons (880M/780M, bare "Radeon(TM) Graphics", Vega APUs) now classify as integrated and land the high lighting tier; the bare amd|radeon match had classed every APU as discrete and handed it ultra (4×4096 CSM cascades, 4096 shadow atlas, 48 shadowed lights). Discrete Radeons (RX naming) keep ultra.
  • Fixed the singleplayer dual-streamer fight (ledger #286): singleplayer glue runs every server-only system in the client world deduplicated by system NAME, so terrain/chunk-streaming (server) and terrain/client-streaming both survived and fought over the shared terrain/stream/<place>/<key> entity namespace with disagreeing desired sets (client extended desktop bands ~2,600 chunks vs server standard ~700). reconcileExistingChunks made the server pass adopt the client's far-band chunks, evictUndesiredChunks despawned them after the 30-tick keep-alive, the client respawned them and queued fresh builds — re-fired on every player chunk-coord change, a ~1,900-chunk rebuild/evict annulus per second while moving (5 fps; standing still was stable).
  • In a singleplayer world ONE streamer now owns the namespace: the server streaming pass suppresses itself (isSoleClientStreamerWorld — client-mode world + singleplayer spec), and the client pass absorbs the server pass's single unique responsibility, the guaranteed LOD0 chunk set under physics anchors (dynamic bodies / character controllers / awake vehicles always get resident chunks + colliders, derived with the same gatherers and AOI resolution the server pass uses). The guard is client-world-scoped: the room container's server-mode world for a singleplayer spec keeps its streamer, and real multiplayer worlds are untouched.
  • Removed the server streaming pass's write-only ran-tick bookkeeping (markServerStreamingRanTick — no readers anywhere) and documented the glue dedup loophole at the seam so the next differently-named server/client system pair decides namespace ownership explicitly.
  • draw/text glyph resolution no longer substitutes the replacement glyph (?) for codepoints the font atlas doesn't cover (ledger #292). The default GeistPixel atlas covers exactly printable ASCII (U+0020–U+007E), so every curly apostrophe, em-dash, accented letter, or emoji in creator/Savi-authored text rendered as tofu ("Open the keeper?s door"). Resolution now runs per grapheme cluster (Intl.Segmenter, codepoint fallback), in order: the font's own glyph wins; uncovered typographic punctuation falls back to its covered ASCII equivalent (smart quotes → '/", en/em-dashes and minus → -, tab → space, prime marks, fraction slash); NFKD compatibility decomposition recovers what it can ("José" reads "Jose", … → ..., no-break/fixed-width spaces → space, ² → 2, all-or-nothing per character so nothing half-renders); anything still uncovered renders nothing — one zero-area, space-advance gap per perceived character (a skin-toned or ZWJ-composed emoji leaves one gap, not five), while zero-width formatting codepoints (ZWJ, variation selectors, bidi marks, controls) collapse entirely. \r\n, \r, and U+2028/U+2029 now count as line breaks instead of resolving through the glyph path.

pinned

what spawn isstart herefrequently asked questionsfaqthe spawn betthe bet

updates

For Realengine v5.03 daysAtelierengine v4.61 weekSurface Tensionengine v4.52 weeksSolidengine v4.43 weeksGroovyengine v4.33 weeksContinuumengine v4.2May 9, 2026Foundationsengine v4.1May 4, 2026Genesisengine v0.1April 29, 2026
← All posts