engine v5.0.14
Engine v5.0.14
June 12, 2026
A patch in the For Real line.
what's new
- Colored and projector light shadows now work for as many lights as your scene needs — they previously stopped working past one or two on many graphics cards.
- Fixed mouse-look games erroring on Firefox when capturing the mouse.
- Worlds with lots of lights and detailed terrain now pick a fitting quality automatically on limited graphics cards instead of failing to draw.
- A texture script that takes too long to draw can no longer freeze your game — the engine stops the draw within its budget, keeps the texture's last good image, and tells Savi exactly which script ran long and for how long.
›technical notes
- Special-spot shadows (IES/projector/custom-color spots) folded into the local-light ShadowAtlas: the atlas grew a lazy transmitted-color layer (
transmittedRenderTarget— atlas-shaped, shares the depth texture, so a special-spot cell renders depth + tint in ONE scheduled pass at the ordinary per-face budget cost), and each special spot's stock per-light ShadowNode is replaced by a customlight.shadow.shadowNode(SpecialSpotAtlasShadowNode, the SunCascadeShadow/directional-bank seam) that samples the shared atlas depth + slot records + transmitted layer. N colored-shadow lights now cost a flat +1 sampled texture and +1 sampler (the per-light era cost 2N of each — on the universal 16-sampler grant that capped colored shadows at 1-2 lights, the jure field report). Scheduling is the atlas's existing top-K/budget/caching, with special spots competing as ordinary spot candidates via the data node's extra-lights channel. - The #7176 special-spot admission machinery is deleted (
spot-shadow-admission.ts:admitSpecialSpotShadows,releaseSpotShadowRequest, the demotion diagnostics;maxAdmittedSpecialSpotShadows,SPECIAL_SPOT_SHADOW_SAMPLED_TEXTURESin quality.ts) — there are no per-light shadow pairs left to admit. The demand model is composition-free: the lit stack counts the atlas as a trio (depth + slots + transmitted; tiers land at 14, low 11),terrainPipelineSampledTextureDemand/selectTerrainShadingVariantlose their special-spot parameter, and the pipeline-rejection alarm handler is unchanged. Atlas-less sessions (low tier, single-sun device floor) give special spots a shared neutral shadow node — lit, castless, structurally unable to mint per-light shadow targets, matching the sun-only semantic every other local light already has there. - The atlas tap TSL (slot decode, gutter window, rotated-grid compare kernel) is extracted to
shadow-atlas/atlas-tap.tsand shared verbatim by the froxel loop and the special-spot node; the transmitted tap samples at explicit mip 0 (uniformity-safe). WGSL receipts: 5 shadow-casting custom-color spots compile to byte-equal binding/sampler counts as 1 (4 textures + 2 samplers for the whole lighting stack in the test material), inside a 16/16 grant, dominance-clean. - Firefox pointer-lock fix (ledger 629, prod DD receipts:
TypeError: can't access property "catch"on every acquire): Chrome/WebKit return a Promise fromrequestPointerLock, Firefox returns undefined — the pointer-lock manager chained.catchdirectly on the return, so on Firefox every acquire threw in the edge-triggered reconcile. The lock request itself was issued before the throw, but the rejection handler / gesture-gate arming / prompt update never ran and the TypeError propagated up the reconcile path. The return is now normalized throughPromise.resolvebefore chaining (return-normalization only — no gate/reconcile semantics change), and Firefox's void-return signature joins the documented browser realities in the manager header. Pinned by a Firefox-shaped test (void-returning request mock: acquire never throws, lockchange still settles, gate behavior intact). - Capability admission replaces the texture-budget ladder (Jacob's ruling: never request more resources than the adapter supports — rungless; rungs are for performance, not device capability). The sampled-texture demand model is complete: shadow-casting IES/projector/custom-color spots bind a per-light depth + transmitted pair in every lit shader (previously uncounted — the gray-screen mechanism on 16-grant devices, partner report "Higher": counted terrain stack 11 + 3 uncounted pairs = 17).
maxAdmittedSpecialSpotShadowscaps those pairs per scene composition at the light-membership refresh (spot-shadow-admission.ts— deterministic creation-order demotion with a one-time diagnostic, restored when budget frees), andselectTerrainShadingVariantpicks the terrain shading variant (full / simplified-lit / unlit) statically from the grant at terrain-resources creation. No pipeline whose stage demand exceeds the grant is ever built. - The reactive walk machinery is gone:
degradeTerrainTextureBudget, the scene-leveltextureBudgetRungescalation, and the@budgetmaterial-name re-arm regex are deleted. The simplified-lit and unlit terrain compile paths survive as static selection targets. A pipeline-resource rejection now hides the object and reports an engine resource-accounting bug — it is an alarm, never an input. - The texture-budget suite pins the demand model against compiled WGSL (full variant 5 fragment bindings, simplified-lit/unlit 3), proves demand ≤ grant by construction for worst-case compositions on every tier and grant down to the WebGPU spec minimum, and pins the deterministic composition-change demotion order.
- Client-wedge class fix (ledger 628 forensics, game "Press Quest", app 8f40c804: a scripted texture re-baked client-side for ~446s and silently froze the player): the texture-script bake budget (
TEXTURE_SCRIPT_BAKE_BUDGET_MS, 50ms wall clock) is now enforced MID-DRAW instead of only after the draw returns. Every script-facing 2D context is wrapped in a budget proxy (clock sampled every 32 ops, methods bind-cached),ctx.random()andctx.canvas()sample the clock, andctx.atlas()checks at every cell boundary — a runaway draw aborts within the budget with the existing budget-fault park instead of running for minutes. This closes the unbounded synchronous hole in the inline bake transport (hosts without nestedWorker, where the bake runs on the renderer host thread); on the worker transport it also retires most 5s-watchdog kills. The residual case (a loop that never touches the ctx surface) remains covered by the worker watchdog. - A faulted RE-bake now keeps the previous texture: the asset service's script-edit sweep restores the old cache entry when the new bake parks (compile/runtime/budget), so consumers keep the last good texels instead of dropping to a placeholder and the retired-texture grace sweep can no longer dispose art that is still on screen. A later fixing edit swaps and retires normally.
- Budget diagnostics now carry the elapsed time (
elapsedMsin the report data and in the message) and state what the engine did ("The engine kept the texture's last good image (or its loading placeholder if it never baked)"), riding the existingtexture-script-budgetrail to getLogs + DM so Savi can see a client-side bake stall instead of misreading it as engine perf.