@godot-scene-web/html 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/extents-R8RIim2S.d.ts +1223 -0
- package/dist/extents-R8RIim2S.d.ts.map +1 -0
- package/dist/index.d.ts +346 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5330 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime-BkfFs_eO.js +6540 -0
- package/dist/runtime-BkfFs_eO.js.map +1 -0
- package/dist/runtime.d.ts +225 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +2 -0
- package/package.json +54 -0
- package/vendor/LICENSE-OpenSans +201 -0
- package/vendor/OpenSans_SemiBold.woff2 +0 -0
- package/vendor/VENDOR.md +51 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-BkfFs_eO.js","names":["textureCache","glTextureOf","STATIC_FRAME_CACHE_LIMIT","staticFrameCache","hasWebgpuApi","backingDimLimit","syncCanvasSize","createBinding"],"sources":["../src/css-values.ts","../src/diagnostics.ts","../src/render-structure.ts","../src/effects-loop-pacing.ts","../src/effects-suspend.ts","../src/shader-dormant.ts","../src/surface-image-swap.ts","../src/webgl/bake-texture.ts","../src/webgl/shared-gl.ts","../src/webgpu/device.ts","../src/webgpu/still-capture.ts","../src/particles/extents.ts","../src/particles/render-backend.ts","../src/webgpu/textures.ts","../src/particles/render-webgpu.ts","../src/particles/spec.ts","../src/particles/static-frame-cache.ts","../src/particles/runtime.ts","../src/runtime-options.ts","../src/webgl/shader-backend.ts","../src/webgpu/render-shader.ts","../src/webgl/runtime.ts","../src/runtime.ts"],"sourcesContent":["import {\n type ColorMatrix,\n type GodotVariant,\n isColorValue,\n} from \"@godot-scene-web/core\";\n\n/**\n * A linear RGB color transform (`out_rgb = rows · in_rgb`, no offset, alpha\n * unchanged), stored row-major. This is the common representation for both a\n * diagonal modulate tint and an arbitrary `feColorMatrix` shader tint (e.g. an\n * HSV color-adjust shader), so the two compose uniformly.\n */\nexport type { ColorMatrix } from \"@godot-scene-web/core\";\n\nconst COLOR_MATRIX_EPSILON = 1e-6;\n\nexport function diagonalColorMatrix(\n r: number,\n g: number,\n b: number,\n): ColorMatrix {\n return {\n rows: [\n [r, 0, 0],\n [0, g, 0],\n [0, 0, b],\n ],\n };\n}\n\nexport function colorMatrixIsDiagonal(matrix: ColorMatrix): boolean {\n const r = matrix.rows;\n return (\n Math.abs(r[0][1]) < COLOR_MATRIX_EPSILON &&\n Math.abs(r[0][2]) < COLOR_MATRIX_EPSILON &&\n Math.abs(r[1][0]) < COLOR_MATRIX_EPSILON &&\n Math.abs(r[1][2]) < COLOR_MATRIX_EPSILON &&\n Math.abs(r[2][0]) < COLOR_MATRIX_EPSILON &&\n Math.abs(r[2][1]) < COLOR_MATRIX_EPSILON\n );\n}\n\nexport function colorMatrixIsIdentity(matrix: ColorMatrix): boolean {\n const r = matrix.rows;\n return (\n colorMatrixIsDiagonal(matrix) &&\n Math.abs(r[0][0] - 1) < COLOR_MATRIX_EPSILON &&\n Math.abs(r[1][1] - 1) < COLOR_MATRIX_EPSILON &&\n Math.abs(r[2][2] - 1) < COLOR_MATRIX_EPSILON\n );\n}\n\n/**\n * Compose two color transforms applied to a column vector. `a` is the outer\n * transform (applied last): the result computes `a · b`. `undefined` is treated\n * as identity, and an identity result collapses back to `undefined` so callers\n * can skip painting a tint.\n */\nexport function composeColorMatrix(\n a: ColorMatrix | undefined,\n b: ColorMatrix | undefined,\n): ColorMatrix | undefined {\n if (!a && !b) {\n return undefined;\n }\n const ra = (a ?? IDENTITY_COLOR_MATRIX).rows;\n const rb = (b ?? IDENTITY_COLOR_MATRIX).rows;\n const cell = (i: number, j: number): number =>\n ra[i][0] * rb[0][j] + ra[i][1] * rb[1][j] + ra[i][2] * rb[2][j];\n const matrix: ColorMatrix = {\n rows: [\n [cell(0, 0), cell(0, 1), cell(0, 2)],\n [cell(1, 0), cell(1, 1), cell(1, 2)],\n [cell(2, 0), cell(2, 1), cell(2, 2)],\n ],\n };\n return colorMatrixIsIdentity(matrix) ? undefined : matrix;\n}\n\nconst IDENTITY_COLOR_MATRIX: ColorMatrix = {\n rows: [\n [1, 0, 0],\n [0, 1, 0],\n [0, 0, 1],\n ],\n};\n\n/**\n * CSS `rgba(...)` for the diagonal multiply path (`background-blend: multiply`).\n * Only valid when the matrix is diagonal; off-diagonals are ignored.\n */\nexport function colorMatrixDiagonalCss(matrix: ColorMatrix): string {\n const r = matrix.rows;\n return `rgba(${toByte(r[0][0])}, ${toByte(r[1][1])}, ${toByte(r[2][2])}, 1)`;\n}\n\n/** The 20-value SVG `feColorMatrix` row form (alpha passes through). */\nexport function colorMatrixFeValues(matrix: ColorMatrix): string {\n const r = matrix.rows;\n return [\n round(r[0][0]),\n round(r[0][1]),\n round(r[0][2]),\n 0,\n 0,\n round(r[1][0]),\n round(r[1][1]),\n round(r[1][2]),\n 0,\n 0,\n round(r[2][0]),\n round(r[2][1]),\n round(r[2][2]),\n 0,\n 0,\n 0,\n 0,\n 0,\n 1,\n 0,\n ].join(\" \");\n}\n\nexport function cssSize(value: string | undefined): number | undefined {\n if (!value) {\n return undefined;\n }\n const parsed = Number.parseFloat(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nexport function styleAttribute(style: Record<string, string>): string {\n return Object.entries(style)\n .map(([name, value]) => `${name}:${value}`)\n .join(\";\");\n}\n\nexport function colorCss(value: GodotVariant | undefined): string | undefined {\n if (!isColorValue(value)) {\n return undefined;\n }\n const [r = 0, g = 0, b = 0, a = 1] = value.args;\n return `rgba(${toByte(r)}, ${toByte(g)}, ${toByte(b)}, ${clamp(a, 0, 1)})`;\n}\n\nexport function colorAlpha(value: GodotVariant | undefined): number {\n return isColorValue(value) ? clamp(value.args[3] ?? 1, 0, 1) : 1;\n}\n\nexport function modulateTint(\n props: Record<string, GodotVariant>,\n): ColorMatrix | undefined {\n const tint = { r: 1, g: 1, b: 1 };\n for (const value of [props.modulate, props.self_modulate]) {\n if (!isColorValue(value)) {\n continue;\n }\n const [r = 1, g = 1, b = 1, a = 1] = value.args;\n // A fully transparent modulate (alpha 0) makes the node invisible at rest, so its RGB\n // multiply has no visible effect — but baking it would DESTROY the texture's colors\n // (a near-zero multiply blacks out the raster), and a later opacity+tint override (e.g.\n // a focus gold ring revealed by clearing this override) could never recover them. Skip\n // the RGB contribution of an alpha-0 modulate; the alpha is applied separately as opacity.\n if (a === 0) {\n continue;\n }\n tint.r *= clamp(r, 0, 1);\n tint.g *= clamp(g, 0, 1);\n tint.b *= clamp(b, 0, 1);\n }\n if (tint.r === 1 && tint.g === 1 && tint.b === 1) {\n return undefined;\n }\n return diagonalColorMatrix(tint.r, tint.g, tint.b);\n}\n\nexport function escapeHtml(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\");\n}\n\nexport function escapeAttribute(value: string): string {\n return escapeHtml(value).replace(/'/g, \"'\");\n}\n\nexport function cssUrl(value: string): string {\n return value.replace(/\"/g, '\\\\\"');\n}\n\nexport function toByte(value: number): number {\n return Math.round(clamp(value, 0, 1) * 255);\n}\n\nexport function clamp(value: number, min: number, max: number): number {\n return Math.max(min, Math.min(max, value));\n}\n\nexport function round(value: number): number {\n return Math.round(value * 1000) / 1000;\n}\n\nexport function safeClassSegment(value: string): string {\n return value.replace(/[^A-Za-z0-9_-]/g, \"-\");\n}\n\n/**\n * A texture URL is \"embedded\" when it carries its own bytes (a `data:` URI), so\n * an inline `<image>` SVG can rasterize it as a CSS background/border image.\n * External URLs (e.g. `/api/asset/...` served out-of-band by `presentation\n * serve`) cannot: browsers load CSS-image SVGs in a restricted mode that blocks\n * external `<image>` references, so those paths use a plain raster background\n * plus a CSS `filter: url(#id)` color matrix instead of baking the tint into the\n * SVG.\n */\nexport function isEmbeddedAssetUrl(url: string | undefined): boolean {\n return typeof url === \"string\" && url.startsWith(\"data:\");\n}\n","// Fail-loud diagnostics for the live shader + particle runtimes. When a node can't be rendered by the runtime\n// — an unsupported shader construct, a compile failure, an unresolved `.gdshader` source, or a malformed\n// particle spec — the node silently falls back to its CSS/SVG/preview paint. That silent degrade is fine for\n// end users, but it used to HIDE real gaps behind hand-curated allow-lists (\"only these shaders/particles are\n// known to work\"). With the runtimes now driven generically (every shader/particle attempted), the failure of\n// any one must be VISIBLE instead of gated away — so the runtime reports it here.\n//\n// Reports are deduped by (kind, id, reason): a node that fails every reconcile warns once, not every frame.\n// The default sink is `console.warn`; a consumer (e.g. a CI corpus gate) can pass an `onUnsupported` reporter\n// to collect the failures programmatically instead.\n\nexport type UnsupportedRenderKind = \"shader\" | \"particle\";\n\nexport interface UnsupportedRenderInfo {\n kind: UnsupportedRenderKind;\n /** Shader identity (uid/path) or particle node path — enough to locate the offending node. */\n id: string;\n /** Short reason, e.g. \"unsupported shader construct\", \"shader failed to compile\", \"shader source\n * unresolved\", \"malformed particle spec\". */\n reason: string;\n /** The underlying error, when the failure threw. */\n error?: unknown;\n}\n\nexport type UnsupportedRenderReporter = (info: UnsupportedRenderInfo) => void;\n\nconst reported = new Set<string>();\n\n// Report an unrenderable shader/particle exactly once per (kind, id, reason). Routes to `onUnsupported` when\n// supplied, else a deduped `console.warn` — so a generic run surfaces exactly which shaders/particles gsw\n// could not render, in place of the old silent allow-list gate.\nexport function reportUnsupportedRender(\n info: UnsupportedRenderInfo,\n onUnsupported?: UnsupportedRenderReporter,\n): void {\n const key = `${info.kind}:${info.id}:${info.reason}`;\n if (reported.has(key)) return;\n reported.add(key);\n if (onUnsupported) {\n onUnsupported(info);\n return;\n }\n if (typeof console !== \"undefined\") {\n const suffix = info.error === undefined ? \"\" : ` (${describeError(info.error)})`;\n console.warn(`[gsw] unsupported ${info.kind} \"${info.id}\": ${info.reason}${suffix}`);\n }\n}\n\nfunction describeError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** TEST-ONLY: clear the dedup set so a test observes reports from a clean slate. */\nexport function __resetUnsupportedRenderReportsForTest(): void {\n reported.clear();\n}\n","import { escapeAttribute } from \"./css-values\";\nimport type { GodotHtmlNode, GodotHtmlTintFilter } from \"./types\";\n\n// Class names shared by every renderer (DOM, Vue, HTML string). The self-layer\n// carries each node's paint/text styling; the stage is the scene root wrapper.\nexport const SELF_LAYER_CLASS = \"godot-scene-self-layer\";\nexport const STAGE_CLASS = \"godot-scene-stage\";\n// Optional fill-parent wrapper that centers the stage and paints letterbox bars\n// when Window content-scale is active. Absent in the default fixed-size render.\nexport const FRAME_CLASS = \"godot-scene-frame\";\n\n// A hidden `<svg>` carrying the `<filter>` defs referenced by `filter: url(#id)`\n// color-matrix tints (external textures). Emitted once per render by every\n// renderer (DOM, HTML string, Vue), so the `url(#id)` references resolve.\nexport function tintFilterDefsMarkup(\n tintFilters: GodotHtmlTintFilter[] | undefined,\n): string {\n if (!tintFilters || tintFilters.length === 0) {\n return \"\";\n }\n const filters = tintFilters\n .map(\n // `markup` is renderer-built SVG (controlled `feColorMatrix`/`feFlood`\n // values, not user input), emitted verbatim inside the `<filter>`.\n (filter) =>\n `<filter id=\"${escapeAttribute(filter.id)}\" color-interpolation-filters=\"sRGB\">${filter.markup}</filter>`,\n )\n .join(\"\");\n return `<svg width=\"0\" height=\"0\" aria-hidden=\"true\" style=\"position:absolute\"><defs>${filters}</defs></svg>`;\n}\n\n// A node's OWN paint layer. Must be a DIRECT-child lookup: `show_behind_parent`\n// children render BEFORE the self-layer, so a descendant `querySelector` would\n// return the behind-child's layer instead (binding e.g. a WebGL shader to the\n// wrong element and a solid-white fallback texture).\nexport function ownSelfLayer(node: HTMLElement): HTMLElement | null {\n return node.querySelector<HTMLElement>(`:scope > .${SELF_LAYER_CLASS}`);\n}\n\n// Nodes flagged with `show_behind_parent` paint before the parent's own\n// self-layer; everything else paints after it.\nexport function isShowBehindParent(node: GodotHtmlNode): boolean {\n return node.attributes[\"data-godot-show-behind-parent\"] === \"true\";\n}\n\n// Resolve a node's children and split them into the two paint groups every\n// renderer needs. The self-layer is inserted between `behind` and `normal` by\n// each renderer (it cannot live here because the emitted form differs per\n// target: DOM element, vnode, or HTML string).\nexport function partitionChildren(\n node: GodotHtmlNode,\n nodeByPath: Map<string, GodotHtmlNode>,\n): { behind: GodotHtmlNode[]; normal: GodotHtmlNode[] } {\n const children = node.children\n .map((path) => nodeByPath.get(path))\n .filter((child): child is GodotHtmlNode => child !== undefined);\n return {\n behind: children.filter(isShowBehindParent),\n normal: children.filter((child) => !isShowBehindParent(child)),\n };\n}\n","// The LOOP-PACING contract shared by the two live effect runtimes (`./webgl/runtime`'s WebGL\n// shader runtime and `./particles/runtime`'s particle runtime).\n//\n// WHY: both loops are FPS-CAPPED (`shaderFps` / `particleFps`) but used to stay armed on a\n// per-DISPLAY-frame rAF chain: a capped tick that fired before its cap boundary re-armed rAF\n// and returned, so a 30fps cap on a 60Hz display cost TWO main-thread wakeups per rendered\n// frame — one that renders, one that only computes \"too early\". A phone trace showed that spin\n// as a measurable slice of sustained main-thread busy, paid on screens where nothing changes.\n//\n// CONTRACT\n// A capped loop asks the pacer for its next wakeup with the SECONDS REMAINING until its cap\n// boundary. The pacer either:\n// - `\"timer\"` (default): PARKS on a `setTimeout` for that interval, then re-enters through\n// exactly ONE rAF, so the render itself is still frame-aligned. No rAF is registered\n// while parked, so the browser can skip those frames on the main thread entirely.\n// - `\"raf\"`: re-arms rAF immediately, i.e. the pre-pacing spin, verbatim. The kill switch.\n//\n// ONE wakeup at a time: `arm` is a no-op while a rAF or a park timer is already in flight, so\n// an invalidation/wake path (a host reconcile, a texture load, a quality retune) never\n// double-arms. A pending park already guarantees a tick within one cap interval — exactly the\n// worst-case latency of the old skip-and-re-arm path — so respecting it costs no latency.\n//\n// SLOP (`\"timer\"` only): the park is shortened by `PARK_SLOP_S` and a boundary within that\n// slop counts as reached (`isDue`). Without it a park that ends a hair AFTER the boundary\n// would re-enter on the NEXT vsync (a whole display frame late, i.e. 30fps → 20fps), or worse,\n// land a hair BEFORE it and pay a second wakeup to re-park. The cost is that the effective\n// rate may exceed the cap by at most `PARK_SLOP_S` per frame. `\"raf\"` pacing uses zero slop,\n// so its cap check stays bit-identical to the pre-pacing one.\n//\n// The pacer owns NOTHING else: no cap bookkeeping, no dirty/dormancy/suspend state, no clock —\n// each runtime keeps its own and passes the remaining time in.\n\n/** How a capped effect loop arms its next tick (see `GodotHtmlRenderOptions.effectsLoopPacing`). */\nexport type EffectsLoopPacing = \"timer\" | \"raf\";\n\n/** Slop (seconds) around a cap boundary, `\"timer\"` pacing only (see the module doc). */\nexport const PARK_SLOP_S = 0.004;\n\n/** The wakeup scheduler of one capped effect loop (see `createEffectsLoopPacer`). */\nexport interface EffectsLoopPacer {\n /** Whether a boundary `remaining` seconds away counts as reached now (absorbs the park slop). */\n isDue(remaining: number): boolean;\n /** Whether a wakeup (rAF or park timer) is already in flight. */\n isArmed(): boolean;\n /** Arm the next tick `remaining` seconds from now. No-op while already armed. */\n arm(remaining: number): void;\n /** Drop a pending park (NOT a pending rAF) — for a cap change that invalidates its deadline. */\n cancelPark(): void;\n /** Drop every pending wakeup. */\n cancel(): void;\n}\n\n/**\n * Create the wakeup scheduler for one capped effect loop. `tick` is the loop body; it runs\n * inside a rAF callback in both pacing modes.\n */\nexport function createEffectsLoopPacer(\n tick: () => void,\n pacing: EffectsLoopPacing | undefined,\n): EffectsLoopPacer {\n const parks = (pacing ?? \"timer\") === \"timer\";\n const slop = parks ? PARK_SLOP_S : 0;\n let rafId: number | null = null;\n let timer: ReturnType<typeof setTimeout> | null = null;\n const onFrame = (): void => {\n rafId = null;\n tick();\n };\n const requestFrame = (): void => {\n if (rafId === null) rafId = requestAnimationFrame(onFrame);\n };\n const onPark = (): void => {\n timer = null;\n requestFrame();\n };\n return {\n isDue: (remaining: number): boolean => remaining <= slop,\n isArmed: (): boolean => rafId !== null || timer !== null,\n arm(remaining: number): void {\n if (rafId !== null || timer !== null) return;\n if (!parks || remaining <= slop) {\n requestFrame();\n return;\n }\n timer = setTimeout(onPark, Math.ceil((remaining - slop) * 1000));\n },\n cancelPark(): void {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n },\n cancel(): void {\n if (rafId !== null) {\n cancelAnimationFrame(rafId);\n rafId = null;\n }\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n },\n };\n}\n","// The EFFECT-SUSPEND contract shared by the two live effect runtimes (`./webgl/runtime`'s\n// WebGL shader runtime and `./particles/runtime`'s particle runtime).\n//\n// WHY: both runtimes keep a rAF loop alive for as long as ANY of their bindings is animated\n// (a `TIME`-reading shader, a looping ambient emitter). That cost is paid even when the\n// nodes are not visible to the user — e.g. a combat scene fully covered by a full-screen\n// dialog, or a subtree the host knows is occluded/off-screen. Neither runtime can decide\n// that on its own: occlusion is a host/layout question. So the host publishes it in the DOM\n// and the runtimes obey.\n//\n// CONTRACT\n// Attribute: `data-godot-effects-suspended` (see `EFFECTS_SUSPENDED_ATTR`), any value —\n// presence alone is the signal (`\"\"`/`\"1\"`/`\"occluded\"` are all equivalent).\n//\n// Scope: a binding is suspended when the attribute is on its own node OR on ANY ancestor\n// (`Element.closest`), so a host suspends a whole subtree by stamping ONE container. The\n// walk is not limited to the scene root, so a wrapper above the root works too.\n//\n// Evaluation: ONLY during each runtime's `reconcile()` — never polled per frame. Occlusion\n// changes are host-driven, and the host calls `reconcile()` whenever it renders a frame, so\n// a reconcile always follows the DOM change that adds/removes the attribute.\n//\n// While suspended:\n// - shader bindings skip their per-frame render and do NOT keep the rAF loop alive; the\n// node canvas keeps its last rendered frame (it's covered, so nothing to see).\n// - particle bindings skip simulate + draw: the simulation state is FROZEN, never reset,\n// and they do NOT count as \"live\" for keeping the loop alive. Their canvas is also\n// PARKED — hidden, so it stops costing a compositor layer and a backing store, with its\n// owed re-sizes deferred to the wake and the binding disposed outright if it stays parked\n// for ~`DORMANT_DISPOSE_SECONDS` (the same park `./shader-dormant` defines, driven here by\n// this attribute instead of a per-node one; see `particleDormant` in `./types` for the\n// kill switch that restores \"the canvas keeps its last drawn frame\").\n// - a runtime whose bindings are ALL suspended parks its rAF loop entirely (zero\n// per-frame cost).\n//\n// On resume (the reconcile that no longer finds the attribute):\n// - shader bindings are re-rendered at the CURRENT `TIME` (marked dirty + the loop kicked).\n// - particle systems un-hide, pay the ONE `sizeCanvas` their park owed, and CONTINUE from\n// their frozen state (no reset, no dt catch-up spike — the loop's clock is reset on wake\n// and dt is clamped). A binding the park already expired is re-created instead, which does\n// reset that one system's simulation.\n//\n// The attribute is deliberately inert for every other part of the renderer: it changes no\n// layout, no style, and no non-runtime paint.\n\n/** The DOM attribute that suspends gsw's live effect runtimes for a subtree (see the module doc). */\nexport const EFFECTS_SUSPENDED_ATTR = \"data-godot-effects-suspended\";\n\nconst EFFECTS_SUSPENDED_SELECTOR = `[${EFFECTS_SUSPENDED_ATTR}]`;\n\n/**\n * Whether `element` sits inside (or is) a subtree the host marked suspended with\n * `data-godot-effects-suspended`. Attribute-only — no layout is forced.\n */\nexport function isEffectsSuspended(element: Element): boolean {\n return element.closest(EFFECTS_SUSPENDED_SELECTOR) !== null;\n}\n","// The SHADER-DORMANT contract for the live WebGL shader runtime (`./webgl/runtime`).\n//\n// WHY: hosts flip nodes in and out of \"shader off\" states constantly (a card leaves the hand, a\n// glow tier is downgraded, a panel closes). Today that means the binding is DISPOSED and, when the\n// node comes back, a fresh one is created — and every new binding pays a `syncCanvasSize`, whose\n// `clientWidth`/`clientHeight` read is a FORCED LAYOUT. A burst of them (playing a card re-keys a\n// whole hand) is a measurable main-thread spike on phones.\n//\n// Dormancy is the cheap middle state: the host says \"this shader node is off for now, but keep it\",\n// and the runtime parks the binding instead of tearing it down. A wake is then free — no program\n// lookup, no texture re-resolve, and (crucially) no forced layout, because the deferred\n// `syncCanvasSize` runs once, at wake, rather than once per flip.\n//\n// CONTRACT\n// Attribute: `data-godot-shader-dormant` (see `SHADER_DORMANT_ATTR`), any value — presence alone\n// is the signal (`\"\"`/`\"1\"`/`\"offscreen\"` are all equivalent).\n//\n// Scope: the SHADER NODE'S OWN element only (unlike `../effects-suspend`, which walks ancestors).\n// Dormancy is a per-node statement about one binding, and the runtime already keys bindings by\n// that element, so no tree walk is needed or wanted.\n//\n// Evaluation: ONLY during `reconcile()`, like the suspend contract — the host stamps/unstamps the\n// attribute and then renders, and a reconcile always follows.\n//\n// While dormant:\n// - the binding is KEPT (same object identity across the dormant window),\n// - its canvas is hidden (`display: none`) so it neither paints nor composites,\n// - the render loop skips it and it does NOT keep the rAF loop alive,\n// - the batched screen-rect read skips it (no layout on its behalf),\n// - `syncCanvasSize` is DEFERRED — a resize, a `setRenderScale`, a UV-window change or a\n// first-ever creation all just mark the sync pending.\n//\n// On wake (the reconcile that no longer finds the attribute):\n// - the deferred `syncCanvasSize` runs (ONE box read, for however many flips happened),\n// - the canvas is unhidden and the binding is re-rendered at the CURRENT `TIME`.\n//\n// Expiry: a binding that stays dormant for roughly `DORMANT_DISPOSE_SECONDS` is disposed for real\n// by the runtime's single sweep, so a node parked forever doesn't leak its canvas/textures. The\n// sweep is ONE timer per runtime, never one per binding.\n//\n// The attribute is inert for every other part of the renderer: it changes no layout, no style, and\n// no non-runtime paint.\n\n/** The DOM attribute that parks a gsw WebGL shader binding without disposing it (see the module doc). */\nexport const SHADER_DORMANT_ATTR = \"data-godot-shader-dormant\";\n\n/** How long a binding may stay dormant before the runtime disposes it for real. Shared with the\n * PARTICLE runtime's park (`./particles/runtime`, driven by `../effects-suspend` rather than by the\n * attribute above): same question, same answer, and one window means a device probe reads one\n * number. */\nexport const DORMANT_DISPOSE_SECONDS = 30;\n\n/**\n * Whether `element` is itself marked dormant with `data-godot-shader-dormant`. Own-element only\n * (see the module doc) and attribute-only — no layout is forced.\n */\nexport function isShaderDormant(element: Element): boolean {\n return element.hasAttribute(SHADER_DORMANT_ATTR);\n}\n","// FROZEN SURFACES AS `<img>` — \"the surface image swap\".\n//\n// A generic mechanism, driven by HOST POLICY: this module owns the swap (encode, stand-in, revert,\n// refcount, pacing, watchdog); the consumer owns the question of WHICH surfaces may freeze and WHEN\n// (see `StaticSurfacePolicy`). It is structurally typed on `{node, canvas, dirty, dormant,\n// staticImage}` and imports nothing — no WebGL, no scene graph — so any runtime that owns a\n// per-node `<canvas>` can use it (`./webgl/runtime` is the first).\n//\n// WHY. A canvas that never changes is not free: it keeps a compositor layer, and (with a blend) a\n// render surface, and it is re-filled on the GPU every composited frame. Measured on a phone by the\n// perf harness's `static-surfaces` scenario (`docs/perf-harness.md`, S5), for 24 surfaces that\n// really never change:\n//\n// canvas: 31 layers, 4 render surfaces, 140 ms GPU clear/fill, worst activation gap 93 ms\n// <img>: 6 layers, 0 render surfaces, 29 ms GPU clear/fill, worst activation gap 21 ms\n//\n// THE TRAP, and why this file is mostly a gate rather than an encoder. The SAME scenario INVERTS\n// when the surfaces are regenerated every 2 s: the `<img>` arms go to a frame-cost p95 of 18.0 ms\n// against the canvas arms' 4.9, main-thread busy 2,934 ms against 1,792, and 70-86 image re-decodes\n// against zero. A surface that changes is strictly WORSE as an image, because every change is an\n// encode plus a decode on the thread that produces frames. So a surface is only ever swapped after a\n// GATE has produced evidence that it is standing still, and any post-swap movement puts it back on\n// its canvas.\n//\n// THE TWO GATES (`StaticSurfacePolicy.gate`):\n// - `content-key` (the default, and what shipped first): the host names each painted frame with a\n// CONTENT KEY, and the surface swaps once that key has been observed unchanged `observations`\n// times (default 3). An observation is either a re-render that produced the same key, or a\n// `reconcile()` in which the surface came out clean. Correct BY CONSTRUCTION: a key change is\n// reported, so the swap can be undone before the stale image is ever wrong. A churn bench over\n// recorded consumer sessions found 93.6% of 1,258 shader nodes never change any component of\n// that key, which is the population this exists for; the other 6.4% pay at most one wasted\n// encode each, once.\n// - `quiet-window`: for surfaces with NO usable content key (a `SCREEN_UV` vignette, a CRT\n// overlay, a particle system — anything whose output is not a pure function of node-local\n// inputs). A surface becomes eligible when `now() - lastDrawAt >= quietMs`, and thaws the\n// INSTANT it draws again.\n//\n// The clock is this module's own per-draw signal (`noteStaticFrame`, called from the render path\n// after the pixels land — cache-hit blits included), NEVER `reconcile()`. A host is free to gate\n// `reconcile()` on its own dirty flag, so counting reconciles would stretch a \"3 observations\"\n// gate into seconds of latency and, worse, would call a surface quiet that had just repainted.\n//\n// Because a keyless surface can repaint without telling us anything, the quiet-window gate gives\n// up the content-key invariant — so it MUST be paired with the WATCHDOG below, which is why\n// `watchdogMs` defaults ON for it and OFF for `content-key`.\n//\n// KEYED-OR-QUIET (`keyedQuietMs`, default = `quietMs`, i.e. inert). A quiet-window host may\n// report a NON-NULL key for SOME of its paints, and where it does, that key is content evidence\n// and not merely a share key. Two things then change for that surface, and only for it:\n// - it becomes eligible `keyedQuietMs` after its last draw instead of `quietMs`, so a host\n// that pins `0` freezes it the instant it paints;\n// - a repaint reporting the SAME key does NOT revert a live swap. The pixels are identical by\n// the key's own contract — that is what the key MEANS — so the repaint is a RE-STATEMENT of\n// the frame the `<img>` is already showing, not movement. A DIFFERENT key, or a null one,\n// reverts exactly as before.\n// A null key keeps the plain quiet window, unchanged, which is what a surface that cannot name\n// its frames still gets.\n//\n// THIS OVERTURNS HALF OF AN EARLIER ARGUMENT, and the half it leaves standing is the important\n// one. The particle runtime's `notePaint` used to withhold its static-frame key from this module\n// deliberately, on the grounds that the key answers \"do these two systems render the same\n// bitmap?\" and NOT \"is this surface standing still\" — a system can re-blit one cached frame\n// forever and still be a moving target. That is still true as an argument about STILLNESS, and\n// it is why the quiet window is still the fallback and why `keyedQuietMs` defaults to inert.\n// What is new is the weaker claim this contract actually needs: a host only ever reports a key\n// where the frame is a PURE FUNCTION of it, so two paints under one key are the same pixels\n// whatever else moved. Re-blitting a cached frame under its own key is exactly that case.\n// IF A HOST REPORTS A KEY IT CANNOT HONOUR — the same key over genuinely different pixels — the\n// result is a stale `<img>` standing over a canvas that has moved on, and nothing here will\n// notice: the `drawSeq`/size proxies all say \"explained\", because a paint WAS explained. Only\n// the watchdog's remaining proxies (the canvas leaving the DOM, a re-allocation, a pending\n// re-render) can catch it, and none of them is a pixel compare. The key is a promise; this is\n// what it costs to break it.\n//\n// INVALIDATION (`StaticSurfacePolicy.onInvalidate`):\n// - `\"block\"` (default, today's behavior): a key change after a swap disqualifies that surface for\n// the life of its binding. The right default when a key IS available: a key that moved once is\n// evidence about that node's content.\n// - `\"retry\"`: revert, reset the gate, do NOT block. Any app with a resizable canvas re-keys on\n// `WxH` at every breakpoint, so \"block forever\" would disqualify its entire population after one\n// rotation. `retry` also covers a FAILED encode/decode: instead of blocking the surface it is\n// rescheduled on the encode pacing cadence, forever.\n// Runtime-WIDE deliberate changes (a re-size, a mode flip, the kill switch, a host `invalidate`)\n// never block under EITHER setting: they say nothing about this surface's content.\n//\n// THE WATCHDOG (`StaticSurfacePolicy.watchdogMs`). A standing check over the swapped set that each\n// one is still legitimately frozen: its canvas is still in the DOM, has no re-render pending, has\n// not been re-allocated (a `width`/`height` write CLEARS a canvas), and has drawn nothing since the\n// freeze. Anything else reverts it. It also re-syncs a stand-in whose canvas MOVED (the box is\n// copied at freeze time, so a later placement write would leave the image at the old box).\n// HONEST LIMIT: it detects unexplained REPAINTS through those observable proxies. It cannot detect\n// an arbitrary pixel write into a same-size canvas that reports nothing — that would need a\n// readback per surface per poll. The content-key gate does not have this hole (the key is\n// reported); the quiet-window gate does, and the proxies are the mitigation.\n//\n// ENCODE PACING (`StaticSurfacePolicy.encode`). A mass freeze must not park the main thread:\n// downstream, an unbatched `toBlob` across 72 surfaces measured a 736 ms park. So at most `slice`\n// encodes (default 4) are kicked per `intervalMs` WINDOW (default 120) — the head of a burst inline,\n// the remainder queued `smallest-first` by backing-store area, so the bulk of a fleet of small\n// surfaces swaps early and the handful of room-sized monsters go last, one late slice each. See\n// `pumpEncodes` for why the budget is on the clock rather than per queue flush, and for the one\n// thing that costs: the inline head of a burst is unsorted.\n//\n// `slice` bounds THROUGHPUT, not the park. That distinction was learned the expensive way: a slice of\n// four drains back-to-back inside ONE timer task, so on a device whose GPU was already saturated the\n// same four readbacks that cost 6-13 ms each when the screen was calm cost ~290 ms each, and the task\n// that held all four measured 1,163 ms with 97% self-time inside native `toBlob` (Moto G86, 30-card\n// shuffle). `encode.perTask` is the lever that bounds the PARK: at most `perTask` readbacks per TASK,\n// with at least `taskGapMs` between two tasks of the same window. The window budget is untouched —\n// with `perTask: 1` a window's four encodes land in four tasks 16 ms apart instead of one task of\n// four — so throughput is identical and only the GRANULARITY moves. Two consequences follow and are\n// the point: the longest main-thread park this mechanism can cause falls to ONE readback, and `busy`\n// (consulted once per PASS) is now consulted once per ENCODE, so a host that becomes busy after the\n// first readback stops the second. `staticImageBusyDeferrals` therefore RISES for an identical\n// workload — it counts passes, and there are now more of them; the diagnosis pair below is a ratio\n// and stays valid. `perTask` defaults to `slice`, i.e. inert, so no existing consumer's pacing moves.\n//\n// `encode.deferHead` (default `false`, i.e. today's behavior above) trades that inline head away: a\n// caller that reaches `maybeSwap` from its OWN hot path (a `reconcile()`, a `TimerFire`) pays the\n// head's `toBlob` on its own stack. Setting `deferHead: true` routes every encode — including the\n// head — through the timer seam, so the caller that made a surface eligible never itself blocks; the\n// trade is a lone surface waiting one scheduler tick instead of swapping the instant it qualifies.\n// See `pumpEncodes`'s `allowInline` parameter.\n//\n// `encode.busy` — WHY THE CLOCK ALONE IS NOT ENOUGH. Pacing decides HOW MANY encodes run per window;\n// it cannot decide WHEN in that window they land, and for this encode the \"when\" is the whole cost. A\n// per-node canvas is GPU-accelerated, so `toBlob` is a GPU→CPU READBACK that blocks until the driver\n// hands the pixels over: a device trace of a consumer's fleet measured 2.8 ms of encoder CPU against\n// ~30 ms of WALL time per surface (52.8 ms of `toBlob` plus 17.1 ms of `createObjectURL` inside ONE\n// `TimerFire` on a Moto G86). One such block dropped into the middle of the host's own draw burst\n// costs frames however small the slice was. `busy` is the host's \"not now\": a predicate consulted\n// ONCE per drain pass — including the `deferHead` pass — and a pass that defers just re-arms at\n// `intervalMs` instead of encoding.\n// BOUNDED, ALWAYS. `busyMaxDeferMs` (default `DEFAULT_ENCODE_BUSY_MAX_DEFER_MS`) caps one unbroken\n// run of deferrals, so a host that is busy forever still drains a slice per bound. Deferring is a\n// SLOWDOWN, never a stop: a surface that has not swapped yet is still a live canvas being re-filled\n// every composited frame, which is the cost this entire file exists to remove. The probe for a stuck\n// predicate is `staticImageBusyForcedEncodes` ≈ `staticImageBusyDeferrals`.\n// WHY THE HOST OWNS THE SIGNAL. gsw cannot see the host's frame loop, and every self-detecting\n// proxy costs something this module refuses to spend: a rAF probe would arm a timer under the\n// DEFAULT policy, which today arms exactly zero (see `nextSweepAt`'s fast path), and there is no rAF\n// at all in jsdom, where this module's own tests run. The host already knows when it is mid-burst.\n// THE STALE-SIGNAL TRAP, and why `busy` alone is not enough either. A host predicate is usually\n// derived from its FRAME LOOP (\"a frame was produced in the last N ms\"), and a long readback\n// SUPPRESSES exactly the frames that signal is made of: the jam manufactures its own \"idle\" reading\n// at the instant the system is most overloaded. Measured: two recovery gaps of 362 ms and 674 ms in\n// the middle of the 1,163 ms stall above, both of which a 250 ms frame-recency predicate reported as\n// quiet, releasing the drain back into the hole. Two mitigations, and they are not equals. HOST\n// side: make the predicate say \"work is ARMED\" (a booked rAF, a live animation) rather than \"a frame\n// HAPPENED\" — a missing frame then reads as a starved loop instead of a finished animation. gsw\n// side: `encode.slowEncodeMs`, the module's own measurement of the PREVIOUS readback, which cannot\n// be faked by a suppressed frame and self-clears the instant the load passes.\n// REJECTED: an AREA budget per window (encode at most N pixels rather than N surfaces). It answers\n// the wrong question — one 2520×1080 readback is a single unsplittable ~30 ms block whatever budget\n// it is charged against — and `smallest-first` already keeps the room-sized monsters out of the\n// early slices, which is the only thing an area rule would have bought.\n// REJECTED: a fixed not-busy COOL-DOWN (hold N ms, or N consecutive quiet passes, after the\n// predicate goes quiet). The measured jank gaps were 362 ms and 674 ms, so a cool-down long enough\n// to cover them is a guess at one device's number that also delays every legitimate freeze on every\n// other device. The previous readback's MEASURED cost answers the same question with evidence this\n// module already has in hand, for free.\n//\n// CAPTURE-HOOK SOURCES (WebGPU). Everything above assumes the surface's own canvas can be READ — it\n// is what `toBlob` is called on. A WebGPU canvas cannot: `drawImage`/`toDataURL`/`toBlob` all go\n// through the presentation path, which is blank under SwiftShader, produces nothing in headless\n// Chrome (it never composites a WebGPU canvas) and is pathological on Android (S7: the blit-shaped\n// arm ran at 23 Hz against 87 for direct presentation). v1 therefore simply never attached such a\n// binding, and every counter here stayed 0 for it.\n//\n// A binding may now instead supply `captureCanvas` — an ASYNC hook that returns a fresh 2D canvas\n// holding the surface's current frame, produced however that renderer can produce it. Both gsw\n// runtimes implement it the one sanctioned way: re-render the frozen frame into an offscreen\n// `rgba8unorm` texture and `copyTextureToBuffer` it back (`../webgpu/readback`), then unpremultiply\n// into a 2D canvas (`../webgpu/still-capture`). The canvas that reaches `toBlob` is therefore an\n// ORDINARY CPU one, and every path below it — dedup, pacing, parked stills, the watchdog, the\n// stand-in — is byte-identical to the canvas-sourced case. Absent hook ⇒ the canvas is read\n// directly, exactly as v1.\n//\n// THE CONTRACT the hook has to keep: return the pixels of the frame the surface is CURRENTLY\n// showing, at its backing-store size, or null. Null (or a throw, or a degenerate canvas) is a\n// FAILURE, not a wedge — it books `staticImageCaptureFailures`, feeds the ordinary `fail()`\n// semantics (retry on the encode cadence, or block), and leaves the surface on its canvas.\n//\n// BLANK CAPTURES — THE FAILURE THAT LOOKS LIKE A SUCCESS (`STATIC_CAPTURE_BLANK`,\n// `staticImageBlankCaptures`). A capture can complete, throw nothing, report nothing, and produce a\n// frame with no visible pixels in it for a surface that was visibly painting. Measured on this box\n// (docs/perf-harness.md, S8's traps): headed under Xvfb on Chrome's DEFAULT ANGLE backend, all 12\n// WebGPU surfaces of both effect arms swapped perfectly — `staticImagesLive 12/12`, zero capture\n// failures, zero encode failures — and every stand-in was a PNG of nothing. The systems vanished\n// from the screenshot, because the canvas is hidden by then and the `<img>` over it is empty, and\n// every counter in this file said the mechanism had worked. That is a SHIPPED risk, not a harness\n// one: any device on which a capture cannot produce pixels blanks a frozen surface in production the\n// same way. (On that rung the readback itself was FINE and the accelerated 2D canvas was the broken\n// link — `../webgpu/still-capture` records both failures and the evidence for each.)\n//\n// So a hook may answer `STATIC_CAPTURE_BLANK` instead of a canvas: \"I captured this surface and got\n// nothing visible, for a frame I know I drew\". It is treated as a capture failure — the surface\n// keeps its live canvas, which is exactly the state it was in before the mechanism existed — and it\n// books `staticImageBlankCaptures` beside `staticImageCaptureFailures`, so the condition is a NUMBER\n// rather than a hole in the numbers.\n//\n// WHO MAY SAY IT is the whole judgement, and it is the PRODUCER's, never this module's. A genuinely\n// empty surface — a particle system that has emitted nothing, one whose particles have all faded to\n// alpha 0, a shader that outputs transparent — is legitimately all-transparent, and rejecting its\n// capture would leave a live canvas up forever and defeat the swap. This module therefore never\n// inspects pixels; it takes a verdict from the one place that knows what it just drew. gsw's own\n// producers answer it from the draw they encoded for THAT capture (`../webgpu/still-capture`'s\n// `expectCoverage`): the particle runtime from its packed instance count — which is exactly the\n// number its live path uses to decide between DRAWING and CLEARING, so a zero-count frame is blank\n// on the canvas too — and the shader runtime from a full-viewport quad whose node modulate is not\n// zero (a fragment program's output is not knowable from outside it; the residual is stated at the\n// call site).\n//\n// TERMINAL FOR THAT SURFACE, whatever `onInvalidate` says — the one place a `\"retry\"` policy does\n// not retry. A blank capture is a statement about the DEVICE (or the launch mode), not about this\n// frame: retrying it costs a full GPU re-render plus a `copyTextureToBuffer` per surface per encode\n// interval, forever, and cannot succeed while the cause holds. The surface it holds on its canvas is\n// already correct, so being terminal costs an optimization and never a pixel.\n//\n// WHY THE DIRECT PATH IS NOT GUARDED — a documented hole, on evidence, rather than a guess in either\n// direction. The obvious symmetry (\"a `toBlob` is a canvas read, so a canvas that cannot be read\n// publishes a blank still whatever put the pixels there\") is FALSE on the very rung that motivated\n// all of this, and the measurements are these (this box, 2026-08-21, headed under Xvfb, default\n// ANGLE, RTX 2060; docs/perf-harness.md, S8):\n// - the WebGL arms of the swap perf scenario, on that rung, publish CORRECT stills: 12/12 surfaces\n// swapped, all 12 presence samples hit, `nonEmptyRatio` identical to the healthy rung. `toBlob`\n// on a host-painted canvas works there.\n// - and yet a FRESH canvas in that same page, written with `putImageData`, reads back alpha 0 —\n// which is exactly what the capture path's guard catches, and it is right to: the WebGPU arms on\n// that rung really did publish PNGs of nothing.\n// So the only cheap probe available for the direct path — paint a scratch canvas, read a pixel of it\n// back — reports BROKEN in an environment where the direct path is demonstrably FINE. Shipping it\n// would have refused every WebGL surface on a rung where the mechanism works, which is worse than\n// the hole it closes. The per-surface variant is no better: reading a witness pixel out of the\n// surface's own canvas uses the same instrument, and there is no third instrument — inspecting the\n// encoded PNG means decoding it, and the only in-page way to look at a decoded image is the canvas\n// read that is broken.\n// WHAT REMAINS UNCOVERED, stated plainly: on a device where `toBlob` over a host-painted canvas\n// yields an empty PNG (one launcher on this box does exactly that — every 2D canvas is dead there,\n// `drawImage` included), a 2D-backed surface still publishes a blank `<img>` and nothing here\n// notices. `staticImageBlankCaptures` stays 0, because no capture hook was involved.\n//\n// CAPTURE TIME AND ENCODE TIME ARE MEASURED SEPARATELY, and that split is the point.\n// `staticImageEncodeMs`/`MaxMs` mean one thing — SYNCHRONOUS main-thread park — and a GPU\n// `mapAsync` readback does not park the main thread at all; folding its wall time in would corrupt\n// the one number that answers \"what did this mechanism cost the frame loop?\". So the capture's WALL\n// time books `staticImageCaptureMs`/`MaxMs` and only the `toBlob` tail books encode-ms. The adaptive\n// backoff (`encode.slowEncodeMs`) does read the capture wall time, because there it is measuring the\n// GPU's willingness to hand pixels over, which is exactly the condition the backoff exists for.\n//\n// NO WORKER, still — for the same reason as below, and one more: the readback here is already off\n// the main thread, so the only thing a worker could take is the PNG encode, which the harness\n// measured as the cheap half.\n//\n// ONE ENCODE PER KEY, NOT PER SURFACE. When the host names a content key, identical surfaces share\n// one encode and one object URL, refcounted (on a real device 7 shader canvases were only 3 distinct\n// keys). A keyless (quiet-window) surface gets a private synthetic key, so it is never shared —\n// there is no identity to share on.\n//\n// PARKED STILLS (`encode.parkedStillBytes`, default 0 = off). Key dedup buys nothing for a KEYLESS\n// surface, and the quiet-window population reverts constantly for reasons that are not repaints: a\n// host `invalidate`, a dormancy wake, a watchdog proxy tripping. Every one of those revokes the URL,\n// so the next freeze pays a full readback for pixels the canvas is still holding — `state.drawSeq`\n// counts actual PAINTS, and it did not move. So a revert PARKS its entry instead of revoking it,\n// stamped with the `drawSeq` and backing-store size it was encoded at, and the next freeze of that\n// same surface RE-ATTACHES it for zero readback when the stamp still matches and the binding is not\n// dirty (`staticImageReuseHits`). Anything else — a paint, a re-allocation, a decode failure — is a\n// disqualification, and a disqualified still is revoked on the spot rather than kept on a hunch.\n// BOUNDED BY BYTES, not by count: the pool is module-wide and evicts least-recently-parked first\n// once `parkedStillBytes` is exceeded, because what is at stake is retained pixel memory (the\n// blob's own `size`, recorded at publish). Parking is exactly a memory-for-readback trade, which is\n// why it is opt-in with a host-chosen budget rather than a default: the module cannot know whether\n// a consumer's device has 24 MB to spend on stills it may never reclaim.\n//\n// RETAINED STILLS (`encode.stillCacheBytes`, default 0 = off). A parked still is claimable by ONE\n// surface — the one that parked it — and dies with it. That is right for the KEYLESS population it\n// was written for (there is no identity to share on, so the fingerprint IS the claim) and wrong for\n// a KEYED one, where the pixels belong to the key rather than to whoever happened to hold it last.\n// So an entry whose last holder lets go — a revert OR a dispose — under a real (non-solo) key is\n// RETAINED instead of revoked: it stays in `entriesByKey`, held by nobody, and the next surface to\n// reach that key attaches to it for zero readback. That is what makes a surface's SECOND-EVER\n// appearance free, which is the whole point (see `claimStaticStill`).\n// ONE POOL, not two. Parked and retained stills are the same commodity — retained blob bytes with\n// no holder — so they share one module-wide LRU (`stillPool`), one running byte total and one\n// eviction walk, budgeted at `parkedStillBytes + stillCacheBytes`. The alternative, two pools with\n// two fixed budgets, is a worse allocator for the same memory: each would starve on its own while\n// the other sat half empty, and an eviction walk would have to guess which one to raid. What the\n// two options still mean SEPARATELY is admission — a keyless entry needs `parkedStillBytes`, a\n// keyed one needs `stillCacheBytes` — so a host can switch either mechanism off without touching\n// the other, and a host that sets neither has no pool at all.\n// NEVER A SOLO KEY. A keyless surface encodes under a private synthetic key (`\\0solo:N`, below);\n// retaining one would pin bytes no lookup can ever hit. Those still park, by fingerprint, or are\n// revoked.\n// The retention is per SWAPPER, for teardown only: an entry is retained on behalf of the swapper\n// whose surface last released it, and that swapper's `dispose()` revokes it. Module-wide sharing\n// is unaffected while both live (`entriesByKey` is document-wide, as it always was), but the leak\n// contract does not bend — `staticImageUrlsLive` still returns to 0 once the last runtime is gone.\n//\n// CLAIMING A STILL (`claimStaticStill`). Everything above is a GATE: a surface paints, waits, is\n// measured, is encoded, and only then becomes an `<img>`. For a surface whose frame has ALREADY been\n// encoded under a key this document has seen, every one of those steps is redundant — the pixels are\n// in hand before the surface exists. `claimStaticStill` is the shortcut: no gate, no paint, no\n// encode, just refcount the entry and attach the stand-in. A host that can name a frame before\n// rendering it can therefore mount the second, third and thirty-fifth copy of that frame as an\n// `<img>` and never configure their canvases at all.\n//\n// BAKING A STILL (`bakeStill`). The other half of the same trade: a surface that is about to\n// DISAPPEAR still holds pixels, and if its key has never been encoded, those pixels are the only\n// copy anyone will ever have cheaply. `bakeStill` enqueues an encode for a key with NO waiting\n// surface, through the identical pacing/capture/publish tail, and publishes an entry held by nobody\n// straight into the retained pool. The host decides when to spend that readback;\n// `StaticSurfaceSwapper.queueLength()` exists so it can hold speculative bakes until the surfaces\n// that actually need a still have drained.\n//\n// PRIMING UNSEEN KEYS (`encode.primeUnseenKeys`, default false). The deferral apparatus above\n// (`busy`, `slowEncodeMs`, `busyMaxDeferMs`) was tuned by a consumer against ~4821×2156 surfaces\n// measured at ~290 ms per readback on a loaded phone: at that size, WHEN a readback lands is the\n// whole cost. A different population — ~320 px canvases whose entire fleet is ~2 distinct keys — is\n// three orders of magnitude off that: the first encode of each key is the ONLY encode that key will\n// ever need (35 surfaces share it), and deferring it does not shave a park, it just leaves 35 live\n// canvases in the composite for another window. On a CAPTURE-HOOK source the argument is stronger\n// still — the readback is asynchronous and parks the main thread not at all. So a queued job whose\n// key has never been encoded in this document may SKIP the deferral check. It bypasses WHEN, never\n// HOW MANY: `slice`, `perTask` and `taskGapMs` all still bind, so no pass can turn into a burst of\n// readbacks. OFF by default precisely because the trade above is a per-consumer judgement about\n// surface size, and this module cannot see a surface's cost from here.\n//\n// NO WORKER. The encode is inline `HTMLCanvasElement.toBlob`. gsw has no `Worker`/`OffscreenCanvas`\n// anywhere, key-dedup keeps the encode count at a handful, and the harness's own fan-out sweep says\n// ONE worker is slower than inline (1,827 ms vs 1,725) — a pool is what wins, and a pool is a lot of\n// machinery to add on speculation. It would not even address the cost measured since: handing the\n// pixels to another thread means capturing them first, and on a GPU-resident source\n// `createImageBitmap` captures SYNCHRONOUSLY on the calling thread (576 ms measured), so the transfer\n// pays exactly what the `toBlob` pays. The readback is the cost, not the codec — which is why the\n// lever this file grew is `encode.busy` (WHEN), not a thread.\n// A single readback is UNSPLITTABLE, and no lever here pretends otherwise: `encode.perTask` cannot\n// make one readback shorter, it only stops four of them sharing a task. `encode.maxDim` is the one\n// lever that shrinks a readback itself, and it does so by reading back fewer PIXELS — which is a\n// fidelity decision, hence off by default.\n//\n// PNG, NOT WEBP. Both are lossless here, so this is decided on cost and on trap-avoidance:\n// - the harness's bake probe measures PNG as the FASTEST codec on the phone (1,789 ms for 50\n// regions from a 4096² page, against 2,244 for lossless webp — webp is ~25% SLOWER, not faster),\n// - `HTMLCanvasElement.toBlob(cb, \"image/webp\")` with no quality argument is LOSSY (4.4x smaller,\n// up to 71 levels of channel error), while `OffscreenCanvas.convertToBlob` with the same\n// omission is lossless. That difference has already been sprung once in this project's ecosystem.\n// PNG is byte-identical through BOTH calls, so there is no quality argument to get wrong.\n// WebP's only win is size (185 KB vs 201 KB per region), and these blobs are held in memory in\n// ones and twos, not shipped over a wire.\n//\n// LIFETIME. A leaked object URL pins its bytes for the life of the document, so the refcount is the\n// contract: an entry exists only while at least one surface holds it, and the URL is revoked the\n// moment the last one lets go (revert, block, dispose, runtime teardown, kill switch). An eviction\n// from the host's own frame cache RETIRES the entry (`onStaticFrameEvicted`) — it leaves the shared\n// lookup so no NEW surface attaches to it — but does not revoke under a surface that is still\n// showing it: the pixels for a key are immutable, and revoking a URL whose `<img>` has not finished\n// loading blanks the surface.\n//\n// DISPLAY OWNERSHIP. This module is the SINGLE writer of a swapped surface's `display`\n// (`applySurfaceVisibility`): it composes \"dormant\" (the host's park) with \"swapped\", and when it\n// un-hides it restores EXACTLY the value the host had before it first hid — never a blanket `\"\"`,\n// which would resurrect a canvas the host itself had hidden. A surface with no swap state is\n// untouched beyond the byte-identical dormant park.\n//\n// The `<img>` REPLACES the canvas visually but not structurally: the canvas stays in the DOM,\n// hidden with `display: none` (no box, no paint, no layer), and the `<img>` is inserted immediately\n// before it so it takes the canvas's exact place in paint order. That keeps two things working for\n// free: a SCREEN_TEXTURE-style capture path that reads an earlier layer's pixels straight off its\n// `<canvas>` child, and the revert, which just unhides a canvas that still holds the right frame.\n\n/** The DOM attribute stamped on the `<img>` that stands in for a frozen surface's canvas. */\nexport const STATIC_SURFACE_IMAGE_ATTR = \"data-godot-shader-image\";\n\n/** Default `content-key` gate: how many consecutive unchanged observations of a surface's content key\n * are required before it is swapped. Small on purpose — the revert is the real safety net, and the\n * clock ticks many times a second — so 3 costs a fraction of a second of latency and excludes a\n * surface that is merely between two states. */\nexport const STABLE_OBSERVATIONS_BEFORE_SWAP = 3;\n\n/** Default `quiet-window` gate: a surface's own draws must hold still this long before it freezes. */\nexport const DEFAULT_QUIET_WINDOW_MS = 1000;\n\n/** Default watchdog cadence, for the gates that need one (see the module doc). */\nexport const DEFAULT_SURFACE_WATCHDOG_MS = 3000;\n\n/** Default encode pacing: surfaces per batch (see the module doc). */\nexport const DEFAULT_ENCODE_SLICE = 4;\n/** Default encode pacing: gap between batches, ms. */\nexport const DEFAULT_ENCODE_INTERVAL_MS = 120;\n/** Default cap on one unbroken run of encode deferrals, for EITHER reason (see the module doc). Long\n * enough that an ordinary burst of host activity is ridden out whole, short enough that a host whose\n * predicate is stuck ON degrades to \"the fleet freezes slowly\" rather than \"the fleet never\n * freezes\". */\nexport const DEFAULT_ENCODE_BUSY_MAX_DEFER_MS = 3000;\n\n/** Default `encode.perTask`: `0` reads as \"the whole slice\", i.e. a window's entire budget drains\n * back-to-back in one timer task — the behavior every existing consumer already has. A host that has\n * MEASURED its readbacks in the tens or hundreds of ms should pin `1`; see `pumpEncodes`. */\nexport const DEFAULT_ENCODE_PER_TASK = 0;\n/** Default gap between two encode TASKS inside one pacing window (only reachable when\n * `perTask < slice`). 16 ms ≈ one 60 Hz display frame, chosen so the host's own rAF gets to run\n * BETWEEN two readbacks — which is what makes a frame-derived `busy` predicate fresh again instead\n * of stale for the whole drain. */\nexport const DEFAULT_ENCODE_TASK_GAP_MS = 16;\n/** Default `encode.slowEncodeMs`: `0` = the adaptive backoff is OFF, so nothing changes for a host\n * that has not asked for it. */\nexport const DEFAULT_ENCODE_SLOW_MS = 0;\n/** Default hold after a readback measured at or over `encode.slowEncodeMs`. */\nexport const DEFAULT_ENCODE_SLOW_BACKOFF_MS = 1000;\n/** Default `encode.maxDim`: `0` = the source canvas is read back at its full backing-store size,\n * exactly as it always has been. */\nexport const DEFAULT_ENCODE_MAX_DIM = 0;\n/** Default `encode.parkedStillBytes`: `0` = parked stills are OFF and a revert revokes immediately,\n * which is what every consumer has today. The trade is retained pixel memory for skipped readbacks\n * (see the module doc's \"PARKED STILLS\"), so the budget is the host's to choose — 24 MB is the\n * measured shape of one fleet's worth of quiet-window stills, not a number this module may assume. */\nexport const DEFAULT_PARKED_STILL_BYTES = 0;\n/** Default `encode.stillCacheBytes`: `0` = KEYED retention is off, so an entry whose last holder\n * lets go is revoked exactly as it always has been. Same reasoning as `parkedStillBytes` — the\n * budget is memory this module cannot know a device has — with one difference that makes it worth\n * more per byte: a retained still is claimable by KEY, so its bytes serve every surface that ever\n * reaches that frame rather than the one surface that parked it. */\nexport const DEFAULT_STILL_CACHE_BYTES = 0;\n\n/** Lossless, and identical through `toBlob` and `convertToBlob` — see the module doc. */\nconst ENCODE_MIME = \"image/png\";\n\n/** What a capture hook answers INSTEAD of a canvas to report the failure that otherwise reads as a\n * success: the capture completed and holds NO VISIBLE PIXELS for a frame the producer knows it drew\n * (see the module doc's BLANK CAPTURES). A plain `null` stays what it always was — \"I could not\n * produce these pixels at all\" — and the two are counted apart. */\nexport const STATIC_CAPTURE_BLANK = \"blank\";\n\n/** What `StaticImageSwapBinding.captureCanvas` resolves to: the frame as an ordinary 2D canvas,\n * `STATIC_CAPTURE_BLANK`, or null. */\nexport type StaticSurfaceCapture =\n | HTMLCanvasElement\n | typeof STATIC_CAPTURE_BLANK\n | null;\n\n// ---- policy (public) --------------------------------------------------------------------------\n\n/** Whatever the injected `setTimeout` seam returns; only ever handed back to the injected\n * `clearTimeout`. Deliberately opaque so a host can inject any scheduler. */\nexport type StaticSurfaceTimerHandle = unknown;\n\n/** The default gate: the host names each painted frame with a content key, and the surface swaps\n * once that key has been observed unchanged `observations` times (default\n * `STABLE_OBSERVATIONS_BEFORE_SWAP`). */\nexport interface StaticSurfaceContentKeyGate {\n kind: \"content-key\";\n observations?: number;\n}\n\n/** The keyless gate: a surface becomes eligible `quietMs` (default `DEFAULT_QUIET_WINDOW_MS`) after\n * its LAST DRAW, and thaws the instant it draws again. See the module doc on why the clock is the\n * per-draw signal and never `reconcile()`, and on why this gate needs the watchdog. */\nexport interface StaticSurfaceQuietWindowGate {\n kind: \"quiet-window\";\n quietMs?: number;\n /** The window for a surface whose LAST reported key was non-null (see the module doc's\n * KEYED-OR-QUIET). Default: `quietMs`, i.e. this option is inert unless a host asks for it — a\n * host that reports no keys, or that wants keyed surfaces held to the same window as keyless\n * ones, is unaffected by its existence.\n *\n * `0` means \"eligible the instant it paints\", which is the setting for a host whose key is a\n * complete description of the frame: there is nothing to wait FOR, because a second paint under\n * the same key would not change a pixel and a paint under a different key reverts anyway. The\n * cost of pinning it wrongly is stated at the module doc: a stale `<img>` no proxy can catch. */\n keyedQuietMs?: number;\n}\n\nexport type StaticSurfaceGate =\n | StaticSurfaceContentKeyGate\n | StaticSurfaceQuietWindowGate;\n\n/** Batching for the (main-thread) encodes — see the module doc. */\nexport interface StaticSurfaceEncodePacing {\n /** Encodes kicked per batch. Default `DEFAULT_ENCODE_SLICE`. */\n slice?: number;\n /** Gap between batches, ms. Default `DEFAULT_ENCODE_INTERVAL_MS`. */\n intervalMs?: number;\n /** `\"smallest-first\"` (default) orders a batch by backing-store area; `\"dom\"` keeps the order the\n * surfaces became eligible in. */\n order?: \"smallest-first\" | \"dom\";\n /** `true` defers even the head of a burst through the timer seam instead of encoding it inline on\n * the caller's stack (see the module doc's \"ENCODE PACING\" section). Default `false` — the head\n * stays inline, which is today's behavior and every existing consumer's default. */\n deferHead?: boolean;\n /** HOST BUSY SIGNAL: \"is this a bad instant to spend ~30 ms on a GPU readback?\". Consulted ONCE per\n * drain pass (never per queued surface), and a pass that it defers re-arms at `intervalMs` rather\n * than encoding — the head included, `deferHead` or not. The host owns this because only the host\n * can see its own frame loop; absent, nothing about the pacing changes. A predicate that THROWS\n * fails OPEN (the pass encodes): a host bug must not be able to stop the mechanism. */\n busy?: () => boolean;\n /** Cap on one unbroken run of deferrals, ms — ONE bound shared by `busy` and by the adaptive\n * backoff below, so neither of them (nor the two together) can stop the fleet. Default\n * `DEFAULT_ENCODE_BUSY_MAX_DEFER_MS`; `0` means NEVER DEFER, i.e. the whole deferral apparatus is\n * switched off — `busy` is not consulted and `slowEncodeMs` is not consulted (the A/B lever for a\n * host that wants both wired but disabled). Once the bound elapses one pass goes out against a\n * still-busy host and the bound restarts, so deferring can only ever slow the fleet down — see the\n * module doc. */\n busyMaxDeferMs?: number;\n /** Readbacks allowed in ONE task. Default: `slice` (today: the whole slice drains back-to-back).\n * `1` is the setting for a host whose surfaces are GPU-resident and big: `toBlob` on such a canvas\n * is a SYNCHRONOUS GPU→CPU readback, so N of them in one task is one unsplittable park of\n * N × readback — measured downstream at 1,163 ms for four ~4821×2156 surfaces on a phone whose GPU\n * was already saturated, against 6-13 ms each for the SAME surfaces once the load passed. `slice`\n * still bounds THROUGHPUT per `intervalMs`; this bounds the BLOCK. Setting it below `slice` costs\n * `taskGapMs` per extra task and buys back the ability to be interrupted. */\n perTask?: number;\n /** Gap between two encode tasks inside one window, ms. Default `DEFAULT_ENCODE_TASK_GAP_MS`.\n * Unreachable (and therefore inert) while `perTask >= slice`. */\n taskGapMs?: number;\n /** ADAPTIVE BACKOFF, the module's own evidence about readback cost — a readback whose SYNCHRONOUS\n * part took at least this long holds the next one for `slowBackoffMs`. On a CAPTURE-HOOK source\n * (`StaticImageSwapBinding.captureCanvas`) there is no synchronous part to measure, so what is\n * compared against this threshold is the capture's WALL time — the GPU readback's own latency,\n * which is precisely the condition this lever exists to back off from. Either way a surface over\n * the threshold books `staticImageSlowEncodes`. Default\n * `DEFAULT_ENCODE_SLOW_MS` (0 = off). This exists because a host's `busy` predicate is usually\n * derived from its frame loop, and a long readback SUPPRESSES the frames that signal is made of:\n * the jam manufactures a stale \"idle\" reading exactly when the system is most overloaded (the\n * module doc's stale-signal trap). The measured cost of the previous readback cannot be faked that\n * way. Bounded by `busyMaxDeferMs` like every other deferral. */\n slowEncodeMs?: number;\n /** How long a slow readback holds the next one, ms. Default `DEFAULT_ENCODE_SLOW_BACKOFF_MS`. */\n slowBackoffMs?: number;\n /** READBACK CLAMP: longest edge (backing-store px) the encode may read. Over it, the surface is\n * blitted into a scratch canvas at the clamped, ASPECT-PRESERVED size and that is what `toBlob`\n * reads — a GPU-side downscale, so a 4821×2156 surface reads back 5.5× fewer pixels. Default\n * `DEFAULT_ENCODE_MAX_DIM` (0 = no clamp).\n * FIDELITY: the stand-in is presented at the canvas's CSS box with `object-fit: fill`, so a clamp\n * is a resampling, not a re-layout — but it IS visible on a surface whose backing store was denser\n * than its CSS box. Soft output (a vignette, fog, a glow) survives it; sharp output does not. Ship\n * it behind a host A/B, never as a silent default.\n * KEY SHARING: `entry.key` names the FRAME, not the encode size, and two surfaces on one key may\n * already have different backing sizes — the first to reach the gate encodes and the other\n * stretches it. The clamp does not change that contract, only the pixel size of the shared\n * frame. */\n maxDim?: number;\n /** PARKED STILLS budget, bytes (see the module doc). A revert parks its encoded frame instead of\n * revoking it, so a re-freeze of the same, unpainted surface costs no readback at all; this is the\n * ceiling on the retained blob bytes that buys. Default `DEFAULT_PARKED_STILL_BYTES` (0 = off,\n * today's immediate revoke). The pool is MODULE-wide (key dedup already is), evicts\n * least-recently-parked first, and applies the budget of whichever policy last parked into it — a\n * document running several swappers should give them the same number. */\n parkedStillBytes?: number;\n /** RETAINED STILLS budget, bytes (see the module doc's \"RETAINED STILLS\"). An entry under a real\n * content key whose LAST holder lets go — by revert or by dispose — is kept, held by nobody, so\n * the next surface to reach that key attaches for zero readback (`claimStaticStill`) and so\n * `bakeStill` has somewhere to publish. Default `DEFAULT_STILL_CACHE_BYTES` (0 = off, i.e. the\n * revoke every consumer has today).\n * SHARES ONE POOL AND ONE EVICTION WALK with `parkedStillBytes`: the budget the pool is trimmed\n * to is the SUM of the two, and either kind may evict the other, least-recently-pooled first.\n * What stays separate is admission — this number alone decides whether a KEYED entry may be\n * retained — so the two mechanisms can still be switched on and off independently. */\n stillCacheBytes?: number;\n /** Exempt the FIRST encode of each key from the deferral apparatus (`busy`, `slowEncodeMs`), not\n * from the pacing (see the module doc's \"PRIMING UNSEEN KEYS\"). Default `false`.\n * The judgement it encodes is about SURFACE SIZE: deferral was tuned against ~4821×2156 readbacks\n * measured at ~290 ms on a loaded phone, and a fleet of ~320 px canvases collapsing to ~2 distinct\n * keys is three orders off that — there, holding the one encode a key will ever need buys no park\n * back and leaves the whole fleet in the composite for another window. A host with big surfaces\n * must leave this off. */\n primeUnseenKeys?: boolean;\n}\n\n/**\n * The host's policy for the surface image swap. `true` (or an absent option) is exactly\n * `{ gate: { kind: \"content-key\" }, onInvalidate: \"block\" }` — the behavior that shipped first;\n * `false` disables the mechanism entirely and the runtime takes the path it took before it existed.\n */\nexport interface StaticSurfacePolicy {\n /** How a surface earns its swap. Default `{ kind: \"content-key\" }`. */\n gate?: StaticSurfaceGate;\n /** What a post-swap invalidation (a content-key change, a failed encode/decode) does to the\n * surface. Default `\"block\"` — never offer it again. `\"retry\"` reverts, resets the gate, and lets\n * it re-earn (failed encodes are rescheduled on the encode cadence, forever). */\n onInvalidate?: \"block\" | \"retry\";\n /** Encode batching. */\n encode?: StaticSurfaceEncodePacing;\n /** HOST VETO, consulted inside the \"should I swap this?\" decision right beside the dormancy check.\n * Return false to keep a surface on its canvas — for a host running its own occlusion /\n * virtualizer pass over the same DOM, this is how it says \"I already claimed this element\",\n * which is the only way two mechanisms can avoid both owning one element's `display`. */\n canFreezeSurface?: (node: HTMLElement, canvas: HTMLCanvasElement) => boolean;\n /** Standing verification cadence over the swapped set, ms (see the module doc). 0 disables it.\n * Default: `DEFAULT_SURFACE_WATCHDOG_MS` for the `quiet-window` gate (which REQUIRES it), 0 for\n * `content-key` (whose invariant makes it unnecessary, and which therefore keeps costing zero\n * idle wakeups). */\n watchdogMs?: number;\n /** RESERVED FOR THE HOST RUNTIME — the code that owns the bindings, not the end consumer that\n * configures it. Called once after a swap is undone, for ANY cause, with the revert already\n * complete (entry released, `<img>` gone, canvas visibility restored), so a re-entrant\n * `revertStaticImage` from inside it finds nothing to revert and is a no-op.\n *\n * WHY IT EXISTS. A revert normally uncovers a canvas that still holds the right frame — that is\n * the mechanism's safety net, and it needs no notification. `claimStaticStill` breaks that\n * assumption on purpose: a surface mounted straight from a cached still has NEVER painted, so\n * under its `<img>` is a canvas with no pixels (and, in the runtime this was written for, no\n * context and no backing store either). An AUTONOMOUS revert — the watchdog, a host\n * `invalidateStaticSurfaces`, a re-size — would therefore uncover a blank surface with nothing\n * scheduled to fix it. This is how that runtime hears about it in time to build the surface and\n * draw, in the same task, before anything composites.\n * A runtime that wraps a consumer's policy MUST run the consumer's handler too, if one was given;\n * this module calls exactly the one function it is handed.\n * A handler that throws is a host bug and is swallowed here: it must not be able to leave a\n * half-reverted surface behind. */\n onRevert?: (binding: StaticImageSwapBinding) => void;\n /** Injectable monotonic clock, ms. Default `performance.now()` (falling back to `Date.now()`). */\n now?: () => number;\n /** Injectable timer seam. Both must be supplied together; absent ⇒ the globals, and where there is\n * no timer host at all (SSR) every deferred path is simply inert. */\n setTimeout?: (fn: () => void, ms: number) => StaticSurfaceTimerHandle;\n clearTimeout?: (handle: StaticSurfaceTimerHandle) => void;\n}\n\n/** The public option shape: `true`/`false` keep their original meaning, an object supplies policy. */\nexport type StaticSurfaceOption = boolean | StaticSurfacePolicy;\n\n// ---- counters (public) ------------------------------------------------------------------------\n\n/** Why a live swap was undone. `staticImageRevertsByCause` splits the aggregate by these. */\nexport type StaticImageRevertCause =\n /** The content key moved (the `content-key` gate's churn case). */\n | \"key-change\"\n /** The surface stopped producing frozen output at all (the host reported a null key). */\n | \"not-frozen\"\n /** The surface drew again (the `quiet-window` gate's thaw). */\n | \"draw\"\n /** A host `invalidateStaticSurfaces` call. */\n | \"host-invalidate\"\n /** The watchdog found a surface that was no longer legitimately frozen. */\n | \"watchdog\"\n /** A runtime-wide deliberate change: a re-size, a pixel-ratio pin, a mode flip, the kill switch,\n * or a geometry move of the canvas itself. */\n | \"resize\"\n /** A dormancy wake whose next repaint cannot be attributed (see `noteStaticSurfaceWake`). */\n | \"dormancy-wake\"\n /** The `<img>` would not decode. */\n | \"decode-failure\";\n\nconst REVERT_CAUSES: readonly StaticImageRevertCause[] = [\n \"key-change\",\n \"not-frozen\",\n \"draw\",\n \"host-invalidate\",\n \"watchdog\",\n \"resize\",\n \"dormancy-wake\",\n \"decode-failure\",\n];\n\n/** The counters this module bumps on the host's live stats object (`WebglShaderRuntimeStats`\n * extends this). Build one with `createStaticImageSwapCounters()` so a later field addition does\n * not break every construction site. */\nexport interface StaticImageSwapCounters {\n /** COUNTER. Surfaces whose `<img>` went live (canvas hidden). Monotonic; a surface that swaps,\n * reverts and swaps again counts twice. */\n staticImageSwaps: number;\n /** COUNTER. Live swaps undone, for ANY reason — the aggregate of `staticImageRevertsByCause`,\n * kept under its original name so existing dashboards keep working. Disposal does NOT count\n * (the surface is gone, not reverted). */\n staticImageReverts: number;\n /** COUNTER, per cause (see `StaticImageRevertCause`). Sums to `staticImageReverts`. */\n staticImageRevertsByCause: Record<StaticImageRevertCause, number>;\n /** COUNTER. Frames encoded — ONE per distinct content key, however many surfaces share it.\n * Counted when the blob is in hand. */\n staticImageEncodes: number;\n /** COUNTER. Encodes/decodes that failed or are unsupported (`toBlob` missing, a null blob, an\n * `<img>` that would not decode). Every one of them leaves the surface on its canvas. */\n staticImageFailures: number;\n /** COUNTER. Drain passes the host's `encode.busy` predicate sent away — one per PASS, whatever the\n * queue length. Zero unless a host supplies the predicate. NOTE, for anyone comparing across\n * versions: a host that pins `encode.perTask` below its `slice` makes MORE passes for an identical\n * workload, so this number rises with it. The diagnosis pair below is a ratio and is unaffected. */\n staticImageBusyDeferrals: number;\n /** COUNTER. Passes that encoded against a still-deferring signal because `busyMaxDeferMs` had\n * elapsed. The diagnosis pair: `staticImageBusyForcedEncodes` ≈ `staticImageBusyDeferrals` means\n * the host's predicate is stuck ON (every deferral ran the bound out), while forced ≪ deferrals is\n * the signal working as intended — bursts ridden out, quiet windows drained normally. */\n staticImageBusyForcedEncodes: number;\n /** COUNTER, ms. Summed SYNCHRONOUS cost of every readback (`toBlob`'s own call, any `maxDim` clamp\n * blit included) — the main-thread park this module is charged for, as a number rather than a\n * trace. */\n staticImageEncodeMs: number;\n /** HIGH-WATER, ms. The single worst readback. The regression probe for \"N readbacks in one task\":\n * under `encode.perTask: 1` this IS the longest task the mechanism can produce. */\n staticImageEncodeMaxMs: number;\n /** COUNTER. Readbacks measured at or over `encode.slowEncodeMs`, i.e. the ones that armed the\n * adaptive backoff. Zero unless a host asks for it. */\n staticImageSlowEncodes: number;\n /** COUNTER. Passes the ADAPTIVE BACKOFF sent away. Sibling of `staticImageBusyDeferrals`: the two\n * split the deferral total by who asked for it (the host's predicate, or this module's own\n * measurement of the previous readback). */\n staticImageBackoffDeferrals: number;\n /** COUNTER. Encodes that read a downscaled scratch instead of the source canvas (`encode.maxDim`). */\n staticImageClampedEncodes: number;\n /** COUNTER. Frames produced through a binding's `captureCanvas` hook — one per ENCODE (so one per\n * distinct content key, like `staticImageEncodes`), not one per surface. Zero on a runtime whose\n * surfaces are all directly readable canvases; on a WebGPU runtime it should track\n * `staticImageEncodes` exactly. */\n staticImageCaptures: number;\n /** COUNTER. Capture hooks that answered null, threw, or handed back a degenerate canvas. Each one\n * ALSO lands in `staticImageFailures` (it goes through the same `fail()`), so the aggregate keeps\n * its meaning; this is the split that says the failure was the READBACK rather than the codec. */\n staticImageCaptureFailures: number;\n /** COUNTER. Captures REFUSED because they held no visible pixels for a frame the producer knew it\n * had drawn (`STATIC_CAPTURE_BLANK`, see the module doc's BLANK CAPTURES). A SUBSET of\n * `staticImageCaptureFailures` — a capture that produced nothing is a capture failure — split out\n * because it is the one failure that would otherwise have looked like a success: without it, a\n * device that cannot produce still pixels reads as `staticImagesLive N/N` over N invisible\n * surfaces.\n *\n * NON-ZERO MEANS ONE OF TWO THINGS, and they are told apart by whether the affected surfaces ever\n * freeze again: a capture path that produces nothing on this device/launch mode (every capture\n * blank, nothing swaps, and the surfaces stay on their canvases — the correct outcome), or a\n * producer claiming coverage for a frame that really is invisible, which costs that surface its\n * freeze and nothing else. Each blank is TERMINAL for its surface under either `onInvalidate`.\n *\n * ZERO IS NOT A CLEAN BILL OF HEALTH FOR A 2D-BACKED FLEET. Only a CAPTURE HOOK can book this;\n * a surface read directly through `toBlob` has no equivalent check, on purpose and on evidence\n * (the module doc's WHY THE DIRECT PATH IS NOT GUARDED). */\n staticImageBlankCaptures: number;\n /** COUNTER, ms. Summed WALL time of every capture hook. Deliberately NOT part of\n * `staticImageEncodeMs`: that number means synchronous main-thread park, and a GPU `mapAsync`\n * readback does not park the main thread (see the module doc's CAPTURE-HOOK section). */\n staticImageCaptureMs: number;\n /** HIGH-WATER, ms. The single slowest capture — the probe for a GPU that has stopped handing\n * pixels over promptly, and the number `encode.slowEncodeMs` is compared against on this path. */\n staticImageCaptureMaxMs: number;\n /** COUNTER. Freezes served from a PARKED still — a re-attach that cost no readback at all because\n * the canvas had not been painted or re-allocated since the entry was encoded (see the module\n * doc). Zero unless a host sets `encode.parkedStillBytes`. */\n staticImageReuseHits: number;\n /** COUNTER. `claimStaticStill` calls that found an attachable entry for the key — one with a URL\n * in hand, live or retained. THE measurement of the second-appearance shortcut: a hit is a\n * surface that skipped the gate, the paint and the encode entirely. */\n staticStillCacheHits: number;\n /** COUNTER. `claimStaticStill` calls that found nothing attachable, so the caller must render the\n * surface itself — no entry, a failed one, or an encode still IN FLIGHT (which has no URL yet;\n * the caller cannot wait, so it renders, and the ordinary gate will swap it when it settles). A\n * steady stream of these against a stable key population means the stills are not surviving —\n * check `staticStillRetainedEntries` and `encode.stillCacheBytes` before blaming the keys. */\n staticStillCacheMisses: number;\n /** COUNTER. Claimed stills whose `<img>` actually went live (decoded and mounted). Sits below\n * `staticStillCacheHits` by exactly the claims that were undone before their decode finished — a\n * revert, a dispose, or a decode failure — which is the only way to tell \"the key was there\" from\n * \"the pixels reached the screen\". */\n staticStillMounts: number;\n /** COUNTER. `bakeStill` calls that really enqueued an encode (a bake for a known key, a solo key,\n * or an unreadable canvas is a no-op and books nothing). One per key, by construction. */\n staticStillBakes: number;\n /** GAUGE — entries sitting in the module-wide still pool RIGHT NOW, parked and retained together\n * (see the module doc's \"RETAINED STILLS\"). Read through `staticStillPoolStats()`; like\n * `staticImageUrlsLive` it is document-wide rather than per runtime, and a host refreshes it on\n * each `stats()` read. */\n staticStillRetainedEntries: number;\n /** GAUGE — bytes those entries pin (each blob's own `size`, as recorded at publish). The number to\n * compare against `parkedStillBytes + stillCacheBytes`: at the budget, the pool is evicting. */\n staticStillRetainedBytes: number;\n /** GAUGE — how many OBJECT URLS are alive right now, MODULE-wide (across every runtime in the\n * document), refreshed on each `stats()` read. This is the leak probe: it must come back to 0\n * after the last runtime is disposed. NOT a swap count — one URL can back many surfaces, and under\n * `encode.parkedStillBytes` a URL held by NO surface still counts, because its bytes are still\n * pinned. Dispose revokes those too, so the probe is unaffected. */\n staticImageUrlsLive: number;\n /** GAUGE — how many SURFACES are swapped right now (`<img>` up, canvas hidden) against THIS\n * counters object, i.e. per runtime. Rises on swap, falls on revert AND on dispose, so it comes\n * back to 0 at teardown. This is the \"is the mechanism actually engaged?\" measurement (`72/72`);\n * `staticImageUrlsLive` cannot answer that, because one URL can back many surfaces. */\n staticImagesLive: number;\n}\n\n/** A zeroed counters object (see `StaticImageSwapCounters`). */\nexport function createStaticImageSwapCounters(): StaticImageSwapCounters {\n const byCause = {} as Record<StaticImageRevertCause, number>;\n for (const cause of REVERT_CAUSES) byCause[cause] = 0;\n return {\n staticImageSwaps: 0,\n staticImageReverts: 0,\n staticImageRevertsByCause: byCause,\n staticImageEncodes: 0,\n staticImageFailures: 0,\n staticImageBusyDeferrals: 0,\n staticImageBusyForcedEncodes: 0,\n staticImageEncodeMs: 0,\n staticImageEncodeMaxMs: 0,\n staticImageSlowEncodes: 0,\n staticImageBackoffDeferrals: 0,\n staticImageClampedEncodes: 0,\n staticImageCaptures: 0,\n staticImageCaptureFailures: 0,\n staticImageBlankCaptures: 0,\n staticImageCaptureMs: 0,\n staticImageCaptureMaxMs: 0,\n staticImageReuseHits: 0,\n staticStillCacheHits: 0,\n staticStillCacheMisses: 0,\n staticStillMounts: 0,\n staticStillBakes: 0,\n staticStillRetainedEntries: 0,\n staticStillRetainedBytes: 0,\n staticImageUrlsLive: 0,\n staticImagesLive: 0,\n };\n}\n\n// ---- per-surface state ------------------------------------------------------------------------\n\n/** Per-surface swap state. Present only while the mechanism is enabled — `null` is the kill switch,\n * and the host's render path then takes exactly the code it took before this module existed. */\nexport interface StaticImageState {\n /** The content key of the frame the canvas currently holds (null until the first cacheable render,\n * and typically null throughout for a keyless `quiet-window` surface). */\n key: string | null;\n /** Consecutive unchanged observations of `key` (the `content-key` gate). */\n stable: number;\n /** The stand-in element, once one exists (created at swap time, dropped on revert). */\n img: HTMLImageElement | null;\n /** The refcounted per-key object-URL entry this surface holds, or null. Non-null covers BOTH \"the\n * encode/decode is in flight\" and \"the `<img>` is live\" — see `shown`. */\n entry: StaticImageEntry | null;\n /** The entry this surface's LAST freeze left parked (`encode.parkedStillBytes`), held by nobody and\n * claimable only by this surface. Mutually exclusive with `entry`: a surface holds its still or\n * parks it, never both. */\n parked: StaticImageEntry | null;\n /** The reuse fingerprint `parked` was stamped with — the paint count and backing-store size the\n * entry was ENCODED at. `reclaimParkedStill` re-attaches only while the canvas still reports all\n * three unchanged, which is the whole correctness argument for handing back old pixels. */\n parkedDrawSeq: number;\n parkedW: number;\n parkedH: number;\n /** The `<img>` is mounted and standing in for the canvas. */\n shown: boolean;\n /** The entry this surface currently holds was taken by `claimStaticStill` rather than earned\n * through the gate — i.e. this canvas has never painted the frame the `<img>` is showing, and may\n * never have painted at all. Cleared when the entry is let go. Read only to split\n * `staticStillMounts` out of the ordinary swap count; the mechanics below treat a claimed surface\n * exactly like any other, which is deliberate — its stand-in reverts, re-syncs and is refcounted\n * by the same code. */\n claimed: boolean;\n /** Disqualified for the life of the binding (only reachable under `onInvalidate: \"block\"`). */\n blocked: boolean;\n /** The swapper this surface belongs to: its policy, its encode queue, its timers. Every free\n * function in this module reads the policy from here, so one document can run several runtimes\n * under different policies. */\n swapper: SwapperContext;\n /** The counters object this surface's TIMER-driven paths (sweep, watchdog, retry, dispose) bump.\n * Seeded when the swapper attaches the binding and refreshed by every call that carries one, so\n * a deferred revert lands on the same object the synchronous ones did. */\n counters: StaticImageSwapCounters | null;\n /** `now()` of the last reported paint into this canvas — the `quiet-window` gate's clock. */\n lastDrawAt: number;\n /** Monotonic count of reported paints; the watchdog compares it against `drawSeqAtFreeze`. */\n drawSeq: number;\n drawSeqAtFreeze: number;\n /** Backing-store size at freeze time: a `width`/`height` write REALLOCATES (and clears) a canvas,\n * which the watchdog reads as an unexplained repaint. */\n frozenW: number;\n frozenH: number;\n /** The canvas's inline style at freeze time. The stand-in copies the box ONCE, so a later\n * placement write would leave it at the old box — the watchdog re-syncs on a mismatch. */\n boxCss: string;\n /** Earliest `now()` at which a failed encode/decode may be retried (`onInvalidate: \"retry\"`). */\n retryAfter: number;\n /** THIS module hid the canvas (so it, and only it, may un-hide it). */\n hidCanvas: boolean;\n /** The canvas's `display` as the HOST left it, captured the moment this module first hid it and\n * restored verbatim when it un-hides. */\n hostDisplay: string;\n}\n\n/** The subset of a host runtime's node binding this module touches. Structural on purpose: it keeps\n * the swap independently testable and keeps the host runtimes free of a back-import. */\nexport interface StaticImageSwapBinding {\n /** The host's node element — passed to `canFreezeSurface`, never otherwise read or written. */\n node: HTMLElement;\n canvas: HTMLCanvasElement;\n /** Set whenever the canvas may not match `state.key` yet (a pending re-render, a realloc that\n * cleared it). The gate refuses to encode from a dirty binding. */\n dirty: boolean;\n /** Parked by the host: observes nothing, and hides BOTH surfaces. */\n dormant: boolean;\n /** ASYNC ENCODE SOURCE, for a surface whose own canvas cannot be read back — a WebGPU one, whose\n * every canvas-read path is blank headless and pathological on Android (see the module doc's\n * CAPTURE-HOOK SOURCES). Returns a fresh 2D canvas holding the frame the surface is CURRENTLY\n * showing, at its backing-store size, or null when it cannot be produced; this module encodes\n * that canvas and then releases it. `STATIC_CAPTURE_BLANK` is the third answer: the capture\n * completed and holds NOTHING VISIBLE, for a frame the producer knows it drew (see the module\n * doc's BLANK CAPTURES). ABSENT ⇒ `canvas` is read directly, which is what every 2D-backed\n * surface does and is exactly the path that shipped first. */\n captureCanvas?: () => Promise<StaticSurfaceCapture>;\n staticImage: StaticImageState | null;\n}\n\n/** One encoded frame, shared by every surface on that content key. */\nexport interface StaticImageEntry {\n key: string;\n /** Surfaces holding this entry (swapped or waiting for the encode). At 0 the URL is revoked. */\n refs: number;\n url: string | null;\n /** The encode failed / is unsupported and no one may retry this key. Only ever set under\n * `onInvalidate: \"block\"` — a `retry` policy drops the entry instead, so the key stays open. */\n failed: boolean;\n /** Encoded size in bytes, captured from the blob at publish (0 until then). The still pool budgets\n * on this: what a pooled entry costs is its retained pixels, not its pixel count. */\n bytes: number;\n /** Surfaces waiting for the in-flight encode. */\n waiters: Set<StaticImageSwapBinding>;\n /** BORN HELD BY NOBODY: this entry was created by `bakeStill` for a key with no waiting surface,\n * so `refs: 0` is its normal state and not the \"everyone let go\" that the encode tail drops an\n * entry for. The three places that read `refs <= 0` as abandonment — the queue drain, the capture\n * tail and `publish` — consult this to tell the two apart. A published bake goes straight into\n * the retained pool, where the flag stops mattering: from then on it behaves like any other\n * unheld entry. */\n bake: boolean;\n}\n\n// ---- module registry --------------------------------------------------------------------------\n\n/** Live entries by key, DOCUMENT-wide (key dedup is worth more than swapper isolation). An entry\n * leaves this map when its last holder releases it (revoked) or when the host evicts the key\n * (retired — current holders keep it, no new holder attaches). */\nconst entriesByKey = new Map<string, StaticImageEntry>();\nlet liveUrls = 0;\n/** Set once when the environment has no usable `toBlob`/`createObjectURL`, so a swapless environment\n * (jsdom, an old engine) costs one boolean instead of an entry per key. */\nlet encodeUnsupported = false;\n/** Mints the private synthetic keys keyless surfaces encode under (see the module doc). */\nlet soloKeySeq = 0;\n/** The prefix of those synthetic keys — a NUL, written as an ESCAPE here and not embedded, so this\n * file stays greppable (an embedded NUL makes `grep` treat the whole source as binary). A host key\n * is a shader path, a params digest, a spec string; none of them can begin with one, which is the\n * point: a synthetic key is NOT a shareable identity, and must never be retained, baked or\n * claimed. */\nconst SOLO_KEY_PREFIX = \"\\0solo:\";\n/** Whether a key is one of this module's own private synthetic ones (see `SOLO_KEY_PREFIX`). */\nfunction isSoloKey(key: string): boolean {\n return key.startsWith(SOLO_KEY_PREFIX);\n}\n\n/** One unheld entry's place in the still pool. */\ninterface PooledStill {\n /** The ONE surface state allowed to reclaim it by FINGERPRINT (a parked still, see `parkStill`),\n * or null for a still retained by KEY — claimable by whatever surface reaches that key next, and\n * therefore owned by nobody. */\n owner: StaticImageState | null;\n /** The swapper the entry was pooled on behalf of, so a teardown can revoke exactly the bytes it is\n * responsible for. An entry re-enters the pool on each release, so this always names the LAST\n * swapper to let go of it — which is the only one that can still be said to be holding the bytes\n * open. */\n ctx: SwapperContext;\n}\n\n/** THE STILL POOL, DOCUMENT-wide: entries with no holder whose pixels may still be re-attachable —\n * PARKED (owner-bound, fingerprint-checked) and RETAINED (keyed, ownerless) together, because they\n * are one commodity and one LRU is a better allocator over it than two (see the module doc's\n * \"RETAINED STILLS\"). Map iteration is insertion order and pooling always inserts fresh, which is\n * what makes the eviction scan a plain least-recently-pooled walk. Bounded by `pooledBytes` against\n * the sum of the two budgets. */\nconst stillPool = new Map<StaticImageEntry, PooledStill>();\nlet pooledBytes = 0;\n/** Keys this document has ever ENCODED, for `encode.primeUnseenKeys` — the exemption is for the\n * FIRST encode of a key, and nothing else here remembers a key after its entry is gone.\n * CAPPED, and the cap fails safe: past `PRIMED_KEY_MEMORY` the set stops growing, every key then\n * reads as \"seen\", and the exemption simply stops applying. A host churning through thousands of\n * distinct keys is not the population this option is for, and an unbounded set of strings pinned\n * for the life of the document would be a leak dressed as an optimization. */\nconst encodedKeys = new Set<string>();\nconst PRIMED_KEY_MEMORY = 512;\n\n// ---- the swapper ------------------------------------------------------------------------------\n\n/** `SwapperContext.busyDeferSince` when no run of `busy` deferrals is in progress. `+Infinity` so the\n * bound test (`now - since >= busyMaxDeferMs`) is false by arithmetic rather than by a special case. */\nconst NOT_DEFERRING = Number.POSITIVE_INFINITY;\n\ninterface ResolvedPolicy {\n quietWindow: boolean;\n observations: number;\n quietMs: number;\n /** The quiet window for a surface whose last reported key was non-null (`keyedQuietMs`, defaulted\n * to `quietMs` here so every read site is one lookup and the inert case costs nothing). */\n keyedQuietMs: number;\n retry: boolean;\n slice: number;\n intervalMs: number;\n smallestFirst: boolean;\n deferHead: boolean;\n busy: (() => boolean) | null;\n busyMaxDeferMs: number;\n perTask: number;\n taskGapMs: number;\n slowEncodeMs: number;\n slowBackoffMs: number;\n maxDim: number;\n parkedStillBytes: number;\n stillCacheBytes: number;\n /** What the shared pool is trimmed to: the two budgets added, because the two kinds of unheld\n * still are the same bytes (see the module doc). Precomputed so the eviction walk reads one\n * number. */\n poolBytes: number;\n primeUnseenKeys: boolean;\n watchdogMs: number;\n canFreeze: ((node: HTMLElement, canvas: HTMLCanvasElement) => boolean) | null;\n onRevert: ((binding: StaticImageSwapBinding) => void) | null;\n now: () => number;\n setT: ((fn: () => void, ms: number) => StaticSurfaceTimerHandle) | null;\n clearT: (handle: StaticSurfaceTimerHandle) => void;\n}\n\ninterface EncodeJob {\n entry: StaticImageEntry;\n canvas: HTMLCanvasElement;\n /** The binding's async encode source, or null for the ordinary \"read the canvas\" path. Captured\n * at ENQUEUE so the job stays self-contained: the queue outlives the call that made the surface\n * eligible, and the pacing must not have to reach back into a binding to drain. */\n capture: (() => Promise<StaticSurfaceCapture>) | null;\n area: number;\n counters: StaticImageSwapCounters;\n /** `bakeStill`'s completion hook, or undefined for an ordinary surface-driven encode (whose\n * completion IS its `<img>` going up). Called exactly once, wherever the job ends. */\n settle?: (published: boolean) => void;\n}\n\n/** One policy + its bookkeeping. Shared by every surface the swapper attached. */\ninterface SwapperContext {\n policy: ResolvedPolicy;\n counters: StaticImageSwapCounters | null;\n bindings: Set<StaticImageSwapBinding>;\n queue: EncodeJob[];\n /** Start of the current encode WINDOW, and how many encodes it has already kicked (see\n * `pumpEncodes`). */\n sliceAt: number;\n sliceCount: number;\n /** Readbacks the CURRENT task has already spent (`encode.perTask`). Reset only where a task\n * boundary is actually observable — inside a timer callback — because the module is handed the\n * thread many times per task: one `runSweep` calls `maybeSwap` for every eligible surface, and\n * each of those reaches `pumpEncodes` on the SAME stack. A per-call counter would therefore bound\n * nothing at all on the path that produced the measured 1,163 ms park. */\n taskKicks: number;\n /** `now()` the CURRENT unbroken run of deferrals started, or `NOT_DEFERRING`. The bound is measured\n * from here and restarts on every forced pass, so it caps a RUN rather than the queue's total wait\n * — and it is ONE run whichever reason is deferring (see `encodeDeferred`). */\n busyDeferSince: number;\n /** `now()` until which the adaptive slow-encode backoff holds encodes (0 = not backing off). */\n backoffUntil: number;\n /** The scratch canvas `encode.maxDim` downscales into, allocated on the first clamped encode and\n * released at dispose. ONE per swapper: `toBlob` snapshots its source synchronously in Blink (the\n * guarantee gsw's own perf harness already depends on — `scenarios/static-surfaces.ts`), so reuse\n * cannot race a blob still being compressed. */\n scratch: HTMLCanvasElement | null;\n drainTimer: StaticSurfaceTimerHandle | null;\n sweepTimer: StaticSurfaceTimerHandle | null;\n /** Absolute `now()` the armed sweep fires at (so an arm never postpones an earlier one). */\n sweepAt: number;\n lastWatchdogAt: number;\n disposed: boolean;\n}\n\n/** The host-facing handle: everything that needs to enumerate the swapper's surfaces. Per-surface\n * signalling stays in the free functions below, which read the policy off the surface's state. */\nexport interface StaticSurfaceSwapper {\n /** Give a binding swap state and start watching it. Idempotent. */\n attach(binding: StaticImageSwapBinding): void;\n /** Drop the stand-in, release the URL ref and stop watching (dispose / kill switch). */\n detach(binding: StaticImageSwapBinding): void;\n /** HOST-DRIVEN revert WITHOUT block: all attached surfaces, or just the given ones. The gate\n * restarts, so each surface re-earns its swap. */\n invalidate(bindings?: Iterable<StaticImageSwapBinding>): void;\n /** How many of this swapper's surfaces are swapped right now (the `staticImagesLive` gauge, per\n * swapper rather than per counters object). */\n liveSwapCount(): number;\n /** Jobs waiting in this swapper's encode queue. The seam a host needs to hold SPECULATIVE work\n * (`bakeStill`) behind the surfaces that actually want a still: the queue is ordered\n * smallest-first, not by who asked, so a bake enqueued mid-burst competes with live candidates\n * for the same slice budget. `0` is the host's \"the fleet has drained, spend a readback on\n * something nobody is waiting for\". */\n queueLength(): number;\n /**\n * Encode `key` from `source` with NO waiting surface, and retain the result (see the module doc's\n * \"BAKING A STILL\"). For banking the pixels of a surface that is about to disappear, so the NEXT\n * surface to reach that key can claim it instead of rendering it.\n *\n * `source` is structural — `{canvas, captureCanvas?}`, the same pair a binding carries — so the\n * whole existing encode tail applies unchanged: pacing, `perTask`, `busy`, `maxDim`, the capture\n * hook, and publication into `entriesByKey`. Deliberately NOT gated on `canvas.isConnected`, which\n * the ordinary freeze path does check: the whole point is a surface on its way out, and a canvas\n * out of the document reads back exactly as well as one in it.\n *\n * A NO-OP, booking nothing, when the key is already known in ANY state (live, retained, in flight\n * or failed — its pixels either exist or have been proven unobtainable), when the key is a private\n * synthetic one (nothing could ever look it up), when retention is off (`encode.stillCacheBytes`),\n * when the canvas is zero-sized, or when the environment cannot encode.\n *\n * `onSettled` — if given — fires exactly once, with `true` when the entry was published and\n * retained and `false` for every other outcome, the synchronous no-ops included. It exists so a\n * host holding a donor surface alive for its pixels can release it the moment the bake lands,\n * rather than polling.\n */\n bakeStill(\n source: StaticStillBakeSource,\n key: string,\n counters: StaticImageSwapCounters,\n onSettled?: (published: boolean) => void,\n ): void;\n /** Revert everything, cancel every timer, forget every surface. */\n dispose(): void;\n}\n\n/** What `bakeStill` reads pixels from: a canvas, or — where that canvas cannot be read at all — the\n * same async capture hook a binding supplies (see `StaticImageSwapBinding.captureCanvas`).\n * Structural on purpose: a host may pass a live binding, or a detached surface it is holding open\n * only for its pixels. */\nexport interface StaticStillBakeSource {\n canvas: HTMLCanvasElement;\n captureCanvas?: () => Promise<StaticSurfaceCapture>;\n}\n\nfunction defaultNow(): number {\n return typeof performance !== \"undefined\" &&\n typeof performance.now === \"function\"\n ? performance.now()\n : Date.now();\n}\n\nfunction resolvePolicy(\n option: StaticSurfaceOption | undefined,\n): ResolvedPolicy {\n const policy: StaticSurfacePolicy =\n option === undefined || typeof option === \"boolean\" ? {} : option;\n const gate = policy.gate ?? { kind: \"content-key\" };\n const quietWindow = gate.kind === \"quiet-window\";\n const encode = policy.encode ?? {};\n const hostSetTimeout = policy.setTimeout;\n const hostClearTimeout = policy.clearTimeout;\n let setT: ((fn: () => void, ms: number) => StaticSurfaceTimerHandle) | null;\n let clearT: (handle: StaticSurfaceTimerHandle) => void;\n if (\n typeof hostSetTimeout === \"function\" &&\n typeof hostClearTimeout === \"function\"\n ) {\n setT = hostSetTimeout;\n clearT = hostClearTimeout;\n } else if (typeof setTimeout === \"function\") {\n setT = (fn, ms) => setTimeout(fn, ms);\n clearT = (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>);\n } else {\n // No timer host at all (SSR/shell): every deferred path is simply inert.\n setT = null;\n clearT = () => {};\n }\n const slice = Math.max(1, encode.slice ?? DEFAULT_ENCODE_SLICE);\n const quietMs =\n gate.kind === \"quiet-window\" && typeof gate.quietMs === \"number\"\n ? Math.max(0, gate.quietMs)\n : DEFAULT_QUIET_WINDOW_MS;\n const parkedStillBytes = Math.max(\n 0,\n encode.parkedStillBytes ?? DEFAULT_PARKED_STILL_BYTES,\n );\n const stillCacheBytes = Math.max(\n 0,\n encode.stillCacheBytes ?? DEFAULT_STILL_CACHE_BYTES,\n );\n return {\n quietWindow,\n observations:\n gate.kind === \"content-key\" && typeof gate.observations === \"number\"\n ? Math.max(1, gate.observations)\n : STABLE_OBSERVATIONS_BEFORE_SWAP,\n quietMs,\n // Absent ⇒ the plain window, so a host that has not asked for the keyed deadline cannot tell\n // this option exists (see `gateSatisfied`, which reads one or the other per surface).\n keyedQuietMs:\n gate.kind === \"quiet-window\" && typeof gate.keyedQuietMs === \"number\"\n ? Math.max(0, gate.keyedQuietMs)\n : quietMs,\n retry: policy.onInvalidate === \"retry\",\n slice,\n intervalMs: Math.max(0, encode.intervalMs ?? DEFAULT_ENCODE_INTERVAL_MS),\n smallestFirst: (encode.order ?? \"smallest-first\") === \"smallest-first\",\n deferHead: encode.deferHead === true,\n busy: typeof encode.busy === \"function\" ? encode.busy : null,\n // 0 is the host asking for the deferral apparatus to be IGNORED, so it is preserved rather than\n // floored to a minimum; a negative number reads the same way.\n busyMaxDeferMs: Math.max(\n 0,\n encode.busyMaxDeferMs ?? DEFAULT_ENCODE_BUSY_MAX_DEFER_MS,\n ),\n // Absent (or nonsense) reads as \"the whole slice\", i.e. today's one-task-per-window drain. A host\n // that pins a number gets at least one readback per task — 0 would be a pump that never encodes.\n perTask:\n typeof encode.perTask === \"number\" && encode.perTask > 0\n ? Math.max(1, encode.perTask)\n : slice,\n taskGapMs: Math.max(0, encode.taskGapMs ?? DEFAULT_ENCODE_TASK_GAP_MS),\n slowEncodeMs: Math.max(0, encode.slowEncodeMs ?? DEFAULT_ENCODE_SLOW_MS),\n slowBackoffMs: Math.max(\n 0,\n encode.slowBackoffMs ?? DEFAULT_ENCODE_SLOW_BACKOFF_MS,\n ),\n maxDim: Math.max(0, encode.maxDim ?? DEFAULT_ENCODE_MAX_DIM),\n parkedStillBytes,\n stillCacheBytes,\n poolBytes: parkedStillBytes + stillCacheBytes,\n primeUnseenKeys: encode.primeUnseenKeys === true,\n // The quiet-window gate has no content invariant, so it does not ship without the watchdog; the\n // content-key gate keeps costing zero idle wakeups unless the host asks for one.\n watchdogMs:\n typeof policy.watchdogMs === \"number\"\n ? Math.max(0, policy.watchdogMs)\n : quietWindow\n ? DEFAULT_SURFACE_WATCHDOG_MS\n : 0,\n canFreeze:\n typeof policy.canFreezeSurface === \"function\"\n ? policy.canFreezeSurface\n : null,\n onRevert: typeof policy.onRevert === \"function\" ? policy.onRevert : null,\n now: typeof policy.now === \"function\" ? policy.now : defaultNow,\n setT,\n clearT,\n };\n}\n\nfunction createContext(\n option: StaticSurfaceOption | undefined,\n counters: StaticImageSwapCounters | null,\n): SwapperContext {\n const policy = resolvePolicy(option);\n return {\n policy,\n counters,\n bindings: new Set(),\n queue: [],\n sliceAt: Number.NEGATIVE_INFINITY,\n sliceCount: 0,\n taskKicks: 0,\n busyDeferSince: NOT_DEFERRING,\n backoffUntil: 0,\n scratch: null,\n drainTimer: null,\n sweepTimer: null,\n sweepAt: Number.POSITIVE_INFINITY,\n lastWatchdogAt: policy.now(),\n disposed: false,\n };\n}\n\n/** The context every surface created by the bare `createStaticImageState()` belongs to: the default\n * policy, no registrations, and therefore no timers of its own. */\nconst defaultContext = createContext(true, null);\n\n/**\n * Create a swapper for one host runtime. `false` ⇒ null (the mechanism is off and the caller keeps\n * its pre-existing path); `true`/undefined ⇒ the default policy; an object ⇒ that policy.\n */\nexport function createStaticSurfaceSwapper(\n option: StaticSurfaceOption | undefined,\n counters: StaticImageSwapCounters,\n): StaticSurfaceSwapper | null {\n if (option === false) return null;\n const ctx = createContext(option, counters);\n return {\n attach(binding: StaticImageSwapBinding): void {\n if (ctx.disposed) return;\n binding.staticImage ??= createStaticImageState(ctx);\n const state = binding.staticImage;\n state.swapper = ctx;\n state.counters ??= counters;\n state.lastDrawAt = ctx.policy.now();\n // The host may already have parked this canvas before any swap state existed (a binding BORN\n // dormant hides it at create). The dormant park is this module's own semantic, so adopt\n // ownership of that hide rather than reading it as \"the host wants this canvas hidden and I\n // must never touch it\" — the value to restore on wake is the pre-park default, which is\n // exactly what the un-owned path used to write.\n if (\n binding.dormant &&\n !state.hidCanvas &&\n binding.canvas.style.display === \"none\"\n ) {\n state.hidCanvas = true;\n state.hostDisplay = \"\";\n }\n ctx.bindings.add(binding);\n // A surface that never draws at all must still become eligible — its window starts here.\n if (ctx.policy.quietWindow) armSweep(ctx);\n },\n detach(binding: StaticImageSwapBinding): void {\n disposeStaticImage(binding);\n ctx.bindings.delete(binding);\n binding.staticImage = null;\n },\n invalidate(bindings?: Iterable<StaticImageSwapBinding>): void {\n for (const binding of bindings ?? ctx.bindings) {\n const state = binding.staticImage;\n if (!state) continue;\n state.counters ??= counters;\n if (state.entry) {\n revert(binding, state.counters ?? counters, false, \"host-invalidate\");\n }\n resetGate(state, ctx.policy.now());\n }\n armSweep(ctx);\n },\n liveSwapCount(): number {\n let live = 0;\n for (const binding of ctx.bindings) {\n if (binding.staticImage?.shown === true) live++;\n }\n return live;\n },\n queueLength(): number {\n return ctx.queue.length;\n },\n bakeStill(source, key, counters, onSettled): void {\n bakeStillInto(ctx, source, key, counters, onSettled);\n },\n dispose(): void {\n ctx.disposed = true;\n cancelTimers(ctx);\n for (const binding of [...ctx.bindings]) {\n disposeStaticImage(binding);\n binding.staticImage = null;\n }\n ctx.bindings.clear();\n ctx.queue.length = 0;\n // Every unheld still this swapper is responsible for goes with it. A RETAINED entry has no\n // holder and no owning surface, so nothing above can have reached it — and it must still not\n // outlive the runtime that banked it, or the leak probe (`staticImageUrlsLive` back to 0 at\n // teardown) would be reporting bytes nobody can free. Entries pooled by ANOTHER live swapper\n // are left alone: the pool is document-wide, and so is the sharing it exists for.\n for (const [entry, pooled] of [...stillPool]) {\n if (pooled.ctx === ctx) dropPooledStill(entry);\n }\n if (ctx.scratch) {\n // A 0×0 backing store releases the pixels immediately rather than at the next GC.\n ctx.scratch.width = 0;\n ctx.scratch.height = 0;\n ctx.scratch = null;\n }\n },\n };\n}\n\nexport function createStaticImageState(\n swapper: SwapperContext = defaultContext,\n): StaticImageState {\n return {\n key: null,\n stable: 0,\n img: null,\n entry: null,\n parked: null,\n parkedDrawSeq: -1,\n parkedW: 0,\n parkedH: 0,\n shown: false,\n claimed: false,\n blocked: false,\n swapper,\n counters: swapper.counters,\n lastDrawAt: swapper.policy.now(),\n drawSeq: 0,\n drawSeqAtFreeze: -1,\n frozenW: 0,\n frozenH: 0,\n boxCss: \"\",\n retryAfter: 0,\n hidCanvas: false,\n hostDisplay: \"\",\n };\n}\n\n/** Object URLs alive across the document (the `staticImageUrlsLive` gauge). */\nexport function liveStaticImageUrlCount(): number {\n return liveUrls;\n}\n\n/** The still pool's two GAUGES, document-wide (`staticStillRetainedEntries` /\n * `staticStillRetainedBytes`) — parked and retained entries together, since they share the budget\n * those numbers are read against. Sampled by a host on each `stats()` read, exactly like\n * `liveStaticImageUrlCount`. */\nexport function staticStillPoolStats(): { entries: number; bytes: number } {\n return { entries: stillPool.size, bytes: pooledBytes };\n}\n\n/** Is there an entry for `key` in ANY state — live, in flight, retained or failed?\n *\n * The question a host asks before spending anything on a key: \"are these pixels already accounted\n * for?\". A `true` means a `bakeStill` would be a no-op and a `claimStaticStill` will probably hit\n * (probably, not certainly — an in-flight encode has no URL yet, and a failed one never will). It\n * is deliberately NOT \"can I claim this right now\": that answer is `claimStaticStill`'s own return,\n * and asking it twice would be two lookups and a race between them. */\nexport function hasStaticStill(key: string): boolean {\n return entriesByKey.has(key);\n}\n\n// ---- per-surface signalling (the free functions the host render path calls) ---------------------\n\n/**\n * The canvas was just PAINTED, with the frame `key` names — the module's one per-draw signal, and\n * therefore the `quiet-window` gate's clock. `key` is null when the paint was not cacheable frozen\n * output at all (live mode, a screen-space shader, textures still loading), which retires a live\n * swap under the `content-key` gate. Called AFTER the pixels are in the canvas, so a revert always\n * uncovers a correct, current frame.\n */\nexport function noteStaticFrame(\n binding: StaticImageSwapBinding,\n key: string | null,\n counters: StaticImageSwapCounters,\n): void {\n const state = binding.staticImage;\n if (!state) return;\n const policy = state.swapper.policy;\n state.counters = counters;\n state.drawSeq++;\n state.lastDrawAt = policy.now();\n // A PAINT is the one thing a PARKED still cannot survive (its claim is the fingerprint, and a\n // paint moves it — see `reclaimParkedStill`), so its bytes go here rather than at the next freeze\n // attempt, which for a surface that has resumed animating is a whole animation away and the pool\n // is budgeted in bytes. A RETAINED still is untouched by this: it is claimed by KEY rather than by\n // fingerprint, is owned by no surface, and this one repainting says nothing about those pixels.\n if (state.parked) dropPooledStill(state.parked);\n\n if (policy.quietWindow) {\n // KEYED RE-STATEMENT (see the module doc's KEYED-OR-QUIET): the surface repainted the frame it\n // is ALREADY showing. By the key's contract those are the same pixels, so reverting would take\n // a correct `<img>` down and pay a whole window plus an encode to put an identical one back —\n // which for a host that re-blits a cached frame is every frame, i.e. the mechanism never\n // engaging at all. The freeze's own evidence follows the paint (`drawSeqAtFreeze`), or the\n // watchdog would revert on the next sweep for the paint this branch just accepted.\n if (key !== null && state.key === key && state.entry) {\n state.lastDrawAt = policy.now();\n state.drawSeqAtFreeze = state.drawSeq;\n state.retryAfter = 0;\n return;\n }\n // KEYLESS: the key is not evidence here (it may be null every time), so the ONLY thing a paint\n // means is \"this surface is not still\". Thaw whatever stands over it and restart its window.\n state.key = key;\n state.retryAfter = 0;\n if (state.entry) {\n // A THAW makes this surface pending again, which can introduce a deadline EARLIER than the\n // one the armed sweep was computed from — so this path pays the full re-arm.\n revert(binding, counters, false, \"draw\");\n armSweep(state.swapper);\n return;\n }\n // A KEYED paint under a SHORTER keyed deadline is the one draw that can pull a surface's\n // eligibility IN rather than push it out (`quietWindowFor` now answers differently for this\n // surface than it did a line ago), so it pays the full re-arm — an armed sweep computed from the\n // plain window would fire a whole `quietMs` after this surface was ready. `armSweep` itself only\n // re-arms for an EARLIER deadline, so this cannot postpone anything.\n if (key !== null && policy.keyedQuietMs < policy.quietMs) {\n armSweep(state.swapper);\n return;\n }\n // HOT PATH (once per animated surface per frame): for an already-pending surface a draw only\n // ever pushes its deadline OUT, so an armed sweep is always early enough — it re-arms itself\n // when it finds nothing due. Re-computing the whole set's next deadline here would be O(N) per\n // draw per surface. Unreachable under a shorter keyed deadline, and untouched without one.\n armSweepIfIdle(state.swapper);\n return;\n }\n\n if (key === null) {\n // Not frozen cacheable output any more: whatever the <img> shows is no longer what this surface\n // renders. Never blocking — leaving frozen mode says nothing about the content.\n // (Already-retired is the LIVE-mode steady state, once per animated surface per frame, so it\n // returns before touching anything.)\n if (state.entry === null && state.key === null) return;\n if (state.entry) revert(binding, counters, false, \"not-frozen\");\n state.key = null;\n state.stable = 0;\n return;\n }\n if (state.key === key) {\n state.stable++;\n maybeSwap(binding, counters);\n return;\n }\n // CHURN. After (or during) a swap this is the case the whole gate exists to catch — the 6.4% — so\n // the surface goes back to its canvas, and under the default policy is never offered again.\n if (state.entry) revert(binding, counters, !policy.retry, \"key-change\");\n state.key = key;\n state.stable = 0;\n if (policy.retry) armSweep(state.swapper);\n}\n\n/**\n * One `reconcile()` in which the binding asked for no re-render — the `content-key` gate's second\n * clock (a frozen node renders once and then nothing calls the render path for it again, so\n * stability has to be observed from the outside). `frozen` is the host's frozen/static mode.\n *\n * DELIBERATELY INERT under the `quiet-window` gate: a host may gate `reconcile()` on its own dirty\n * flag, so reconciles are not a clock — see the module doc.\n */\nexport function noteStaticImageReconcile(\n binding: StaticImageSwapBinding,\n frozen: boolean,\n counters: StaticImageSwapCounters,\n): void {\n const state = binding.staticImage;\n if (!state || !frozen) return;\n if (state.swapper.policy.quietWindow) return;\n state.counters = counters;\n if (state.entry || state.blocked || state.key === null) return;\n // A parked binding measures nothing and paints nothing; a dirty one has a re-render pending, so\n // its canvas is not (yet) the frame `state.key` names.\n if (binding.dormant || binding.dirty) return;\n state.stable++;\n maybeSwap(binding, counters);\n}\n\n/**\n * MOUNT AS `<img>` IMMEDIATELY, from a still this document has already encoded for `key` — no gate,\n * no paint, no encode (see the module doc's \"CLAIMING A STILL\"). Returns whether it did.\n *\n * THE PRECONDITIONS the caller must meet, and what happens when it does not:\n * - the binding must have SWAP STATE (`swapper.attach`). Without it the mechanism is off for this\n * surface, so this returns false and books nothing — a miss counter for a surface that could\n * never hit would just be noise in the one number that says whether the keys are working.\n * - the CANVAS must be in the document. The stand-in is inserted immediately BEFORE it, so a\n * canvas with no parent leaves the swap unfinished (the entry is held, nothing is shown) until\n * the surface is disposed. Not a corruption, but a wasted refcount.\n * - the canvas must already carry the BOX (the inline geometry the stand-in copies verbatim). A\n * canvas whose CSS box is written after the claim leaves the `<img>` at the old box until the\n * watchdog re-syncs it — the same rule the ordinary freeze path lives by, arriving one step\n * earlier.\n * A surface that is already engaged (holding an entry, already shown, or blocked) is left exactly\n * as it is, and reports a miss: two stand-ins over one canvas is not a thing this module allows.\n * A DORMANT surface is NOT refused, unlike the ordinary gate's — a park hides both surfaces either\n * way, and the caller is the one that knows whether a parked surface is worth claiming for. What is\n * refused is an entry whose encode is still IN FLIGHT: there is no URL to attach, and the caller\n * cannot be asked to wait, so it renders and the ordinary gate swaps it when the encode settles.\n *\n * A CLAIMED SURFACE HAS NEVER PAINTED, and the bookkeeping says so honestly: `drawSeq` is whatever\n * it was (0 for a fresh binding), and `frozenW`/`frozenH` are captured from a canvas that may have\n * no backing store at all. So the FIRST time such a canvas is really sized, the watchdog sees a\n * re-allocation it cannot explain and reverts. That is CORRECT, not a bug — a canvas that has just\n * been allocated is blank, and the `<img>` over it is showing a frame nothing under it can vouch\n * for. The caller hears about it through `StaticSurfacePolicy.onRevert` and takes the surface back.\n */\nexport function claimStaticStill(\n binding: StaticImageSwapBinding,\n key: string,\n counters: StaticImageSwapCounters,\n): boolean {\n const state = binding.staticImage;\n if (!state) return false;\n state.counters = counters;\n if (state.entry || state.shown || state.blocked) {\n counters.staticStillCacheMisses++;\n return false;\n }\n const entry = entriesByKey.get(key);\n // A failed key is not a miss to retry from here: it is a key that has been PROVEN unencodable, and\n // the caller's own render path is the answer either way. It books a miss because from the caller's\n // side that is exactly what happened — there is no still to be had.\n if (!entry || entry.failed || entry.url === null) {\n counters.staticStillCacheMisses++;\n return false;\n }\n // Pooled (parked or retained) ⇒ it has a holder again: its bytes stop being the pool's to evict,\n // and any per-surface claim on it is superseded by this one.\n unpoolStill(entry);\n entry.refs++;\n state.entry = entry;\n state.key = key;\n state.claimed = true;\n counters.staticStillCacheHits++;\n attach(binding, entry, counters);\n return true;\n}\n\n/**\n * Undo a live swap and reset the gate WITHOUT blocking — for the host's own deliberate, scene-wide\n * changes (a render-scale step, a pixel-ratio pin, a frozen-mode flip, a canvas geometry move, the\n * kill switch). The surface must re-earn the gate; it is not disqualified.\n */\nexport function revertStaticImage(\n binding: StaticImageSwapBinding,\n counters: StaticImageSwapCounters,\n cause: StaticImageRevertCause = \"resize\",\n): void {\n const state = binding.staticImage;\n if (!state) return;\n state.counters = counters;\n if (state.entry) revert(binding, counters, false, cause);\n resetGate(state, state.swapper.policy.now());\n armSweep(state.swapper);\n}\n\n/**\n * The host woke a DORMANT binding. The wake re-arms a repaint this module may hear nothing useful\n * about (a deferred canvas re-size CLEARS the backing store, and a keyless surface's repaint carries\n * no key to compare), so a stale `<img>` could otherwise sit over a live canvas until the watchdog\n * polls. Reverts — without blocking — when the wake cannot be attributed:\n * - always under the `quiet-window` gate (no key, so no way to confirm the repaint matches), and\n * - whenever the host says a canvas re-size is pending (`resizePending`), whatever the gate.\n * Under the `content-key` gate with no pending re-size this is a NO-OP: the wake's re-render reports\n * its key, and an unchanged key means the stand-in still shows exactly the right pixels.\n */\nexport function noteStaticSurfaceWake(\n binding: StaticImageSwapBinding,\n counters: StaticImageSwapCounters,\n resizePending: boolean,\n): void {\n const state = binding.staticImage;\n if (!state) return;\n state.counters = counters;\n const policy = state.swapper.policy;\n if (!policy.quietWindow && !resizePending) return;\n if (state.entry) revert(binding, counters, false, \"dormancy-wake\");\n resetGate(state, policy.now());\n armSweep(state.swapper);\n}\n\n/** Binding teardown (dispose / kill switch): drop the `<img>` and release the URL ref. Not counted\n * as a revert — the surface is gone, not returned to its canvas — but the LIVE gauge falls, so it\n * comes back to 0 at teardown. */\nexport function disposeStaticImage(binding: StaticImageSwapBinding): void {\n const state = binding.staticImage;\n if (!state) return;\n if (state.shown && state.counters) state.counters.staticImagesLive--;\n detachImage(state);\n if (state.entry) {\n // A DISPOSE is a release like any other, and under `encode.stillCacheBytes` the last release of\n // a KEYED entry retains it (see the module doc's \"RETAINED STILLS\") — which is exactly the case\n // this mechanism exists for: the surface is gone, its pixels are not, and the next surface to\n // reach that key should not have to re-render them. Nothing is retained for a keyless surface's\n // private key, and nothing at all with the budget at 0, so a consumer without the option takes\n // the revoke it has today.\n release(state.entry, binding, null, true);\n state.entry = null;\n state.claimed = false;\n }\n // A PARKED still is claimable only by THIS surface, which is going away — so its bytes would\n // ordinarily be dead the moment this binding is. That argument holds for the fingerprint claim and\n // NOT for the pixels: if the entry's key is a real one, any surface that reaches that key can\n // still use it, so the still is re-homed as an ownerless RETAINED entry instead of being revoked.\n // A private synthetic key has no such second claimant, and `retainStill` refuses it.\n if (state.parked) {\n const parked = state.parked;\n state.parked = null;\n if (!retainStill(parked, state.swapper)) dropPooledStill(parked);\n }\n state.shown = false;\n state.claimed = false;\n state.stable = 0;\n state.key = null;\n state.swapper.bindings.delete(binding);\n}\n\n/**\n * The ONE writer of both surfaces' `display`. Dormant hides everything (the host's park); otherwise\n * exactly one of the two is visible. Un-hiding restores the value the HOST left on the canvas at the\n * moment this module first hid it — never a blanket `\"\"`, which would resurrect a canvas the host\n * itself had hidden. With no swap state this is byte-identical to the\n * `canvas.style.display = dormant ? \"none\" : \"\"` it replaces.\n */\nexport function applySurfaceVisibility(binding: StaticImageSwapBinding): void {\n const state = binding.staticImage;\n if (!state) {\n binding.canvas.style.display = binding.dormant ? \"none\" : \"\";\n return;\n }\n if (state.img) {\n state.img.style.display = binding.dormant ? \"none\" : \"block\";\n }\n if (binding.dormant || state.shown) {\n if (!state.hidCanvas) {\n state.hostDisplay = binding.canvas.style.display;\n state.hidCanvas = true;\n }\n binding.canvas.style.display = \"none\";\n return;\n }\n if (state.hidCanvas) {\n binding.canvas.style.display = state.hostDisplay;\n state.hidCanvas = false;\n state.hostDisplay = \"\";\n }\n // Never hidden by this module ⇒ the host owns the property; do not touch it.\n}\n\n/** The host's own frame cache dropped this key. Retire the entry: no NEW surface may attach to it,\n * while current holders keep showing it until they release (see the module doc on why this does\n * not revoke out from under a live `<img>`). */\nexport function onStaticFrameEvicted(key: string): void {\n const entry = entriesByKey.get(key);\n if (!entry) return;\n entriesByKey.delete(key);\n if (entry.refs <= 0) {\n // A POOLED entry — parked for one surface, or RETAINED for whatever reaches its key — has no\n // holder but IS still claimable; the host has just said this frame is gone, so the claim goes\n // with the pixels. (An entry still held by a live `<img>` is left alone: it leaves the lookup so\n // nothing NEW attaches, and is revoked by its last holder.)\n unpoolStill(entry);\n revoke(entry);\n }\n}\n\n/** TEST-ONLY: revoke every URL, forget every entry and stand the default context down, so a test\n * starts from a clean registry. */\nexport function __resetStaticImageSwapForTest(): void {\n for (const entry of entriesByKey.values()) revoke(entry);\n entriesByKey.clear();\n // Pooled entries need their own sweep: `onStaticFrameEvicted` can have taken one out of the lookup\n // while it is still pooled and claimable.\n for (const [entry, pooled] of stillPool) {\n if (pooled.owner) pooled.owner.parked = null;\n revoke(entry);\n }\n stillPool.clear();\n pooledBytes = 0;\n encodedKeys.clear();\n liveUrls = 0;\n encodeUnsupported = false;\n soloKeySeq = 0;\n cancelTimers(defaultContext);\n defaultContext.queue.length = 0;\n defaultContext.bindings.clear();\n defaultContext.sliceAt = Number.NEGATIVE_INFINITY;\n defaultContext.sliceCount = 0;\n defaultContext.taskKicks = 0;\n defaultContext.busyDeferSince = NOT_DEFERRING;\n defaultContext.backoffUntil = 0;\n defaultContext.scratch = null;\n defaultContext.lastWatchdogAt = defaultContext.policy.now();\n}\n\n// ---- internals: the gate ----------------------------------------------------------------------\n\nfunction resetGate(state: StaticImageState, now: number): void {\n state.stable = 0;\n state.retryAfter = 0;\n state.lastDrawAt = now;\n // KEY EVIDENCE EXPIRES WITH THE GATE, on the quiet-window side only. Every caller of this is an\n // event that says the surface's pixels can no longer be vouched for (a host invalidate, a re-size,\n // a dormancy wake) — and under a SHORT keyed deadline a surface whose key survived would re-attach\n // its old still on the very next sweep, without ever repainting, on the strength of a key that was\n // reported before whatever just happened. Clearing it puts the surface back on the plain window\n // until it paints again and re-states its key, which costs a keyed surface nothing in practice\n // (the host repaints; that IS the report) and is the whole safety of the short deadline.\n // NOT on the content-key side: there `state.key` is the gate's own state, `stable` is the counter\n // that resets, and clearing it would change behavior for every existing consumer.\n if (state.swapper.policy.quietWindow) state.key = null;\n}\n\n/** The quiet window THIS surface is held to: the keyed one when its last paint named a frame, the\n * plain one otherwise (see the module doc's KEYED-OR-QUIET). One function so the three places that\n * need the deadline — the gate, the sweep's due test and the sweep's ARMING — cannot disagree; a\n * `nextSweepAt` that used the plain window for a keyed surface would simply be late, and the surface\n * would sit eligible-but-unswept until something else woke the swapper. */\nfunction quietWindowFor(\n state: StaticImageState,\n policy: ResolvedPolicy,\n): number {\n return state.key === null ? policy.quietMs : policy.keyedQuietMs;\n}\n\nfunction gateSatisfied(\n binding: StaticImageSwapBinding,\n state: StaticImageState,\n policy: ResolvedPolicy,\n now: number,\n): boolean {\n if (policy.quietWindow) {\n // A pending re-render means the pixels are about to move; wait it out rather than encode them.\n return (\n !binding.dirty && now - state.lastDrawAt >= quietWindowFor(state, policy)\n );\n }\n return state.key !== null && state.stable >= policy.observations;\n}\n\nfunction maybeSwap(\n binding: StaticImageSwapBinding,\n counters: StaticImageSwapCounters,\n): void {\n const state = binding.staticImage;\n if (!state || state.entry || state.blocked) return;\n const policy = state.swapper.policy;\n const now = policy.now();\n if (state.retryAfter > now) return; // a failed encode is pacing its retry\n if (!gateSatisfied(binding, state, policy, now)) return;\n // Parked: nothing is painting, so there is nothing to win and the canvas may be mid-defer.\n // (`dirty` is deliberately NOT checked on the content-key path: the render path calls in with a\n // canvas it has just painted — and `dirty` is only cleared by its caller AFTER the render — while\n // the reconcile path checks `dirty` itself. The invariant either way is \"the canvas holds the\n // frame `key` names\".)\n if (binding.dormant) return;\n // HOST VETO, right beside the dormancy check (see `canFreezeSurface`).\n if (policy.canFreeze && !policy.canFreeze(binding.node, binding.canvas))\n return;\n // A zero-sized backing store has no frame to encode (and `toBlob` of one is not a picture); a\n // canvas that has left the document paints nothing, so freezing it is a wasted encode and a URL\n // held until teardown.\n if (binding.canvas.width < 1 || binding.canvas.height < 1) return;\n if (!binding.canvas.isConnected) return;\n // ZERO-READBACK PATH, checked before the key lookup because it is stronger than a key match: this\n // surface's OWN previous still, re-attached when the canvas still holds the exact pixels it was\n // encoded from (see the module doc's \"PARKED STILLS\").\n if (reclaimParkedStill(binding, state, counters)) return;\n if (encodeUnsupported || !canEncode()) {\n encodeUnsupported = true;\n return;\n }\n // A keyless surface has no identity to share on, so it encodes under a private synthetic key.\n const key = state.key ?? `${SOLO_KEY_PREFIX}${++soloKeySeq}`;\n const existing = entriesByKey.get(key);\n if (existing) {\n if (existing.failed) {\n if (policy.retry) {\n state.retryAfter = now + policy.intervalMs;\n armSweep(state.swapper);\n } else {\n state.blocked = true;\n }\n return;\n }\n // A key hit on a POOLED entry — parked for another surface, or RETAINED by nobody: it has a\n // holder again, so its bytes stop being the pool's to evict and whichever surface parked it\n // loses its claim (the key dedup outranks the private one). This is also the path a surface\n // whose OWN still was retained takes back to its pixels, without any fingerprint at all.\n unpoolStill(existing);\n existing.refs++;\n state.entry = existing;\n if (existing.url) {\n attach(binding, existing, counters);\n } else {\n existing.waiters.add(binding); // an encode is already in flight for this key\n }\n return;\n }\n const entry: StaticImageEntry = {\n key,\n refs: 1,\n url: null,\n failed: false,\n bytes: 0,\n waiters: new Set([binding]),\n bake: false,\n };\n entriesByKey.set(key, entry);\n state.entry = entry;\n // The surface's OWN canvas is the source: it holds exactly the frame `key` names — the same pixels\n // the host's frame cache holds for that key, without this module needing to reach into the cache.\n // Unless the binding supplied a CAPTURE HOOK, in which case the canvas cannot be read at all and\n // the hook re-produces the same frame instead (see the module doc's CAPTURE-HOOK SOURCES). The\n // canvas is still carried: its backing-store size is what the pacing orders the queue by.\n enqueueEncode(state.swapper, {\n entry,\n canvas: binding.canvas,\n capture: binding.captureCanvas ?? null,\n area: binding.canvas.width * binding.canvas.height,\n counters,\n });\n}\n\nfunction canEncode(): boolean {\n return (\n typeof document !== \"undefined\" &&\n typeof URL !== \"undefined\" &&\n typeof URL.createObjectURL === \"function\" &&\n typeof HTMLCanvasElement !== \"undefined\" &&\n typeof HTMLCanvasElement.prototype.toBlob === \"function\"\n );\n}\n\n// ---- internals: parked stills (the zero-readback re-freeze) -------------------------------------\n\n/**\n * May the entry this revert is letting go be PARKED for a later re-attach (`encode.parkedStillBytes`,\n * see the module doc)? The test is the reuse fingerprint, evaluated while the state still carries it:\n *\n * - a frame that was never SHOWN has no fingerprint at all (`drawSeqAtFreeze` is -1 and the frozen\n * size was never captured), so there is nothing to compare a later canvas against;\n * - a paint since the freeze (`drawSeq`) means the canvas no longer holds the encoded pixels, and a\n * re-allocated backing store cleared them outright — those are the reverts a re-encode is FOR;\n * - a decode failure is excluded on its own evidence: the blob would not paint, so re-attaching it\n * later only fails again;\n * - a BLOCKING revert disqualifies the surface for the life of the binding, so nobody is ever\n * coming back for the pixels.\n */\nfunction parkableStill(\n binding: StaticImageSwapBinding,\n state: StaticImageState,\n wasShown: boolean,\n block: boolean,\n cause: StaticImageRevertCause,\n): boolean {\n return (\n wasShown &&\n !block &&\n cause !== \"decode-failure\" &&\n state.swapper.policy.parkedStillBytes > 0 &&\n state.drawSeq === state.drawSeqAtFreeze &&\n binding.canvas.width === state.frozenW &&\n binding.canvas.height === state.frozenH\n );\n}\n\n/** The pool's admission rules, split out so a caller can ask BEFORE it gives anything up (see\n * `parkStill`). A still bigger than the entire budget would evict the pool on admission and then be\n * evicted by the next park anyway — two revokes and a wasted walk to end up where refusing it\n * starts. */\nfunction poolAdmits(entry: StaticImageEntry, budget: number): boolean {\n return (\n budget > 0 && entry.url !== null && !entry.failed && entry.bytes <= budget\n );\n}\n\n/** Admit an unheld entry to the shared still pool and trim to budget — the one insertion point for\n * BOTH kinds (`owner` set = parked for that one surface, `owner` null = retained by key), so there\n * is one byte total and one eviction walk over them. Returns false when the entry cannot be pooled\n * at all, in which case the caller revokes as before.\n * `budget` is the ADMISSION budget of whichever mechanism is asking (`parkedStillBytes` or\n * `stillCacheBytes`); the pool is then trimmed to the policy's `poolBytes`, which is both together. */\nfunction poolStill(\n entry: StaticImageEntry,\n owner: StaticImageState | null,\n ctx: SwapperContext,\n budget: number,\n): boolean {\n if (!poolAdmits(entry, budget)) return false;\n const existing = stillPool.get(entry);\n if (existing) {\n // Already pooled (a retained entry being re-homed to a new owner, or the reverse): its bytes are\n // counted once, and re-inserting would double them. The pooling ORDER is left alone too — an\n // entry nobody has held since it was pooled has not become fresher by being re-labelled.\n existing.owner = owner;\n existing.ctx = ctx;\n } else {\n stillPool.set(entry, { owner, ctx });\n pooledBytes += entry.bytes;\n }\n // Least-recently-pooled first (Map iteration is insertion order, and pooling always inserts\n // fresh). One walk over both kinds: they are the same bytes, and a keyed still is not more\n // evictable than a parked one just because it is claimable by more surfaces.\n const poolBudget = ctx.policy.poolBytes;\n for (const pooled of stillPool.keys()) {\n if (pooledBytes <= poolBudget) break;\n if (pooled === entry) continue; // never evict the admission itself on its own walk\n dropPooledStill(pooled);\n }\n return true;\n}\n\n/** Hold an unheld entry for its last surface instead of revoking it (`encode.parkedStillBytes`).\n * The entry deliberately STAYS in `entriesByKey`: its pixels are still valid for that key, so a\n * different surface reaching the same key should attach to it rather than encode a duplicate\n * (`maybeSwap` unpools it on the way through). */\nfunction parkStill(entry: StaticImageEntry, state: StaticImageState): boolean {\n const ctx = state.swapper;\n const budget = ctx.policy.parkedStillBytes;\n // Asked BEFORE the previous still is given up: a park that cannot happen must not cost this\n // surface the one it is already holding.\n if (!poolAdmits(entry, budget)) return false;\n if (state.parked && state.parked !== entry) dropPooledStill(state.parked);\n if (!poolStill(entry, state, ctx, budget)) return false;\n state.parked = entry;\n state.parkedDrawSeq = state.drawSeq;\n state.parkedW = state.frozenW;\n state.parkedH = state.frozenH;\n return true;\n}\n\n/** Keep an unheld entry claimable BY KEY, by nobody in particular (`encode.stillCacheBytes`, see the\n * module doc's \"RETAINED STILLS\"). Refused for a private synthetic key: no lookup can ever reach\n * one, so retaining it would pin bytes for a hit that cannot happen. */\nfunction retainStill(entry: StaticImageEntry, ctx: SwapperContext): boolean {\n if (isSoloKey(entry.key)) return false;\n return poolStill(entry, null, ctx, ctx.policy.stillCacheBytes);\n}\n\n/** Take an entry out of the pool WITHOUT revoking it — for every way a pooled still gets a holder\n * again (its owner reclaims it, another surface hits its key, or a claim takes it). */\nfunction unpoolStill(entry: StaticImageEntry): void {\n const pooled = stillPool.get(entry);\n if (!pooled) return;\n stillPool.delete(entry);\n pooledBytes -= entry.bytes;\n if (pooled.owner?.parked === entry) pooled.owner.parked = null;\n}\n\n/** Give up on a pooled still: nothing holds it (a pooled entry's refs are 0 by construction), so the\n * URL goes and the key stops being a lookup hit. */\nfunction dropPooledStill(entry: StaticImageEntry): void {\n unpoolStill(entry);\n revoke(entry);\n if (entriesByKey.get(entry.key) === entry) entriesByKey.delete(entry.key);\n}\n\n/** Re-attach the still this surface's previous freeze parked, when the canvas still reports the paint\n * count and backing-store size it was encoded at and no re-render is pending. That is the entire\n * correctness argument for handing back old pixels, and it is the same evidence the watchdog uses to\n * decide a live swap is still legitimate — so a stale still is caught here rather than shown.\n * A disqualified still is revoked ON THE SPOT: it will never match again (the fingerprint only moves\n * forward), so holding its bytes for the LRU to notice later is pure waste. */\nfunction reclaimParkedStill(\n binding: StaticImageSwapBinding,\n state: StaticImageState,\n counters: StaticImageSwapCounters,\n): boolean {\n const entry = state.parked;\n if (!entry) return false;\n const reusable =\n entry.url !== null &&\n !entry.failed &&\n !binding.dirty &&\n state.drawSeq === state.parkedDrawSeq &&\n binding.canvas.width === state.parkedW &&\n binding.canvas.height === state.parkedH;\n if (!reusable) {\n // Disqualified for THIS surface's fingerprint — but if the key is a real one those pixels are\n // still that key's frame, so the entry is re-homed as a RETAINED still rather than revoked. That\n // is only ever a re-labelling: `dropPooledStill` is what happens when nothing can use it.\n state.parked = null;\n if (!retainStill(entry, state.swapper)) dropPooledStill(entry);\n return false;\n }\n unpoolStill(entry);\n entry.refs++;\n state.entry = entry;\n counters.staticImageReuseHits++;\n attach(binding, entry, counters);\n return true;\n}\n\n// ---- internals: encode pacing ------------------------------------------------------------------\n\n/** Would this pass encode a key this document has never encoded, under a policy that exempts those\n * from the deferral apparatus (`encode.primeUnseenKeys`)? Reads the HEAD of the queue, which is\n * what the pass would kick next; costs one `Set.has` and only for a host that asked. */\nfunction primingPass(ctx: SwapperContext): boolean {\n if (!ctx.policy.primeUnseenKeys) return false;\n const head = ctx.queue[0];\n return head !== undefined && !encodedKeys.has(head.entry.key);\n}\n\n/** Remember that a key has been encoded, for `primeUnseenKeys`. Capped, and the cap fails safe: past\n * `PRIMED_KEY_MEMORY` new keys are simply not remembered, so they read as SEEN and the exemption\n * stops applying — the conservative direction. */\nfunction noteEncodedKey(key: string): void {\n if (encodedKeys.size >= PRIMED_KEY_MEMORY) return;\n encodedKeys.add(key);\n}\n\n/**\n * `StaticSurfaceSwapper.bakeStill` — enqueue an encode for a key NOTHING is waiting on (see the\n * module doc's \"BAKING A STILL\", and the public method for the contract).\n *\n * The entry is born `refs: 0, bake: true` and goes through the ORDINARY queue: same pacing, same\n * `perTask`, same `busy`, same capture hook, same `publish`. That reuse is the whole design — a\n * second encode path would be a second place for the readback budget to be wrong — and it is why the\n * three \"everyone let go\" guards downstream had to learn the difference between an entry that lost\n * its holders and one that never had any.\n */\nfunction bakeStillInto(\n ctx: SwapperContext,\n source: StaticStillBakeSource,\n key: string,\n counters: StaticImageSwapCounters,\n onSettled?: (published: boolean) => void,\n): void {\n const settle = (published: boolean): void => onSettled?.(published);\n const canvas = source.canvas;\n // Every refusal settles FALSE and books nothing, so a caller holding a surface open for its pixels\n // is released on the same stack it asked on:\n // - nothing could ever look a synthetic key up, and retention is where a bake LIVES (with it off\n // `publish` would revoke the entry the instant it made it);\n // - a key known in ANY state — live, retained, in flight, or failed — already has its answer:\n // the pixels exist, are coming, or have been proven unobtainable, and baking over it would be\n // a duplicate readback at best and an orphaned live URL at worst;\n // - a zero-sized backing store is not a picture.\n // NOTE the guard that is deliberately ABSENT: `isConnected`. The ordinary freeze path refuses a\n // canvas that has left the document because a surface nobody can see is a wasted encode; here it\n // is the POINT — the pixels are banked precisely because the surface is on its way out.\n if (\n ctx.disposed ||\n isSoloKey(key) ||\n ctx.policy.stillCacheBytes <= 0 ||\n entriesByKey.has(key) ||\n canvas.width < 1 ||\n canvas.height < 1\n ) {\n settle(false);\n return;\n }\n if (encodeUnsupported || !canEncode()) {\n encodeUnsupported = true;\n settle(false);\n return;\n }\n const entry: StaticImageEntry = {\n key,\n refs: 0,\n url: null,\n failed: false,\n bytes: 0,\n waiters: new Set(),\n bake: true,\n };\n entriesByKey.set(key, entry);\n counters.staticStillBakes++;\n enqueueEncode(ctx, {\n entry,\n canvas,\n capture: source.captureCanvas ?? null,\n area: canvas.width * canvas.height,\n counters,\n settle,\n });\n}\n\nfunction enqueueEncode(ctx: SwapperContext, job: EncodeJob): void {\n ctx.queue.push(job);\n if (ctx.policy.smallestFirst) ctx.queue.sort((a, b) => a.area - b.area);\n // `deferHead` trades the inline head away: the caller that made this surface eligible (often a\n // `reconcile()`/`TimerFire` already on a hot path) never runs an `encode()` on its own stack — see\n // `pumpEncodes`'s `allowInline` parameter and the module doc's \"ENCODE PACING\" section.\n pumpEncodes(ctx, !ctx.policy.deferHead);\n}\n\n/**\n * At most `slice` encodes per `intervalMs` WINDOW, counted on the clock rather than per queue\n * flush. That is what actually bounds the synchronous block: surfaces become eligible one at a time\n * (one `reconcile()` can walk a whole set), so a queue-length rule would let every one of them\n * encode inline — which is precisely the 736 ms park this exists to prevent.\n *\n * The head of a burst — up to `slice` surfaces — is therefore encoded INLINE by default\n * (`allowInline`, default `true`), before the queue has anything to sort, so `smallest-first` orders\n * the DEFERRED remainder rather than the whole set. That is the deliberate trade for a lone surface\n * not having to wait a whole interval to swap.\n *\n * `allowInline: false` (only ever passed from `enqueueEncode` under `policy.deferHead`) skips that\n * inline loop entirely and, if the window has not spent any of its budget yet, schedules the drain\n * at the minimum delay (1 ms) rather than at the next `intervalMs` boundary — so the head still goes\n * out promptly, just never on the caller's own stack. Every subsequent call in the burst (queue\n * already non-empty, `drainTimer` already armed) is then a no-op until that timer fires, at which\n * point it re-enters with `allowInline` defaulted back to `true` and normal pacing resumes.\n *\n * THE TWO BUDGETS, stated exactly. At most `slice` encodes per `intervalMs` WINDOW **and** at most\n * `perTask` per TASK, with at least `taskGapMs` between two tasks of the same window. Throughput is\n * governed by the first and is unchanged by the second; what `perTask` moves is GRANULARITY, and with\n * it the two things the window budget could never bound: the longest main-thread park (one readback,\n * not `slice` of them) and how often the deferral predicates get a say (once per encode, not once per\n * slice). `perTask` defaults to `slice`, which collapses the second budget into the first — every\n * existing consumer's drain, unchanged.\n *\n * \"TASK\" is `ctx.taskKicks`, and it is deliberately conservative: it is reset in a TIMER callback and\n * nowhere else, so every inline pump between two timer fires shares one budget. That is what makes\n * the bound real — surfaces become eligible one at a time and each one reaches this function on the\n * CALLER's stack, so a budget scoped to a single call would let one `runSweep` spend the whole slice\n * back-to-back exactly as before. The cost of being conservative is that a lone surface arriving in\n * some later task may wait `taskGapMs` on a timer instead of encoding inline; the cost of being\n * optimistic is the 1,163 ms park.\n */\nfunction pumpEncodes(ctx: SwapperContext, allowInline = true): void {\n if (ctx.disposed) return;\n const policy = ctx.policy;\n const now = policy.now();\n if (now - ctx.sliceAt >= policy.intervalMs) {\n ctx.sliceAt = now;\n ctx.sliceCount = 0;\n }\n // The task budget exists only while it is TIGHTER than the window's. At `perTask >= slice` (the\n // default, where `perTask` resolves to `slice`) the window budget binds first in every case, and\n // tracking the task would actively change behavior: `taskKicks` only clears on a timer while\n // `sliceCount` clears with each window, so a policy that arms no timers — the default one, see\n // `nextSweepAt`'s fast path — would latch its task budget and push every later burst's inline head\n // onto a timer it has never armed.\n const taskBudget =\n policy.perTask < policy.slice ? policy.perTask : Number.POSITIVE_INFINITY;\n // \"Not now\", asked ONCE per pass and only when there is something to encode — which under a pinned\n // `perTask` means once per READBACK, the point of pinning it. A PRIMING pass (the head of the\n // queue is a key this document has never encoded, and `encode.primeUnseenKeys` is on) does not ask\n // at all: see the module doc's \"PRIMING UNSEEN KEYS\" for why WHEN is the wrong lever for that\n // encode. It is the head that is tested because the head is what this pass would kick next; the\n // budgets below are untouched, so a priming pass is still at most one slice.\n const deferring =\n ctx.queue.length > 0 &&\n !primingPass(ctx) &&\n encodeDeferred(ctx, now, allowInline);\n // A deferring pass encodes NOTHING — the inline head included, which is the point: `deferHead`\n // moves the head onto a 1 ms timer, and a 1 ms timer still lands inside the host's burst.\n if (allowInline && !deferring) {\n while (\n ctx.taskKicks < taskBudget &&\n ctx.sliceCount < policy.slice &&\n ctx.queue.length > 0\n ) {\n const job = ctx.queue.shift();\n if (!job) break;\n // Superseded while queued: everyone let go, the key failed, or another holder already encoded\n // it. It costs no readback, so it is charged to NEITHER budget. A BAKE is held by nobody by\n // construction (`entry.bake`), so `refs <= 0` is its resting state and not abandonment.\n if (\n (job.entry.refs <= 0 && !job.entry.bake) ||\n job.entry.failed ||\n job.entry.url !== null\n )\n continue;\n ctx.sliceCount++;\n ctx.taskKicks++;\n encode(ctx, job);\n }\n }\n if (ctx.queue.length > 0 && ctx.drainTimer === null && policy.setT) {\n // Re-read the clock: a readback can outlast the whole pacing window, and a delay measured from\n // BEFORE it would be an interval that has already elapsed. (A no-op under an injected fake clock,\n // which is why the pacing tests are unaffected by it.)\n const after = policy.now();\n const delay = deferring\n ? // Re-ask a whole pacing window later. The queue keeps its order and its slice budget; only\n // the instant moved.\n Math.max(1, policy.intervalMs)\n : ctx.taskKicks >= taskBudget && ctx.sliceCount < policy.slice\n ? // The TASK budget stopped this pass, not the window's: yield for a gap and come back for\n // the rest of the window's budget rather than sitting on it until the window turns over.\n Math.max(1, policy.taskGapMs)\n : !allowInline && ctx.sliceCount === 0\n ? 1\n : Math.max(1, ctx.sliceAt + policy.intervalMs - after);\n ctx.drainTimer = policy.setT(() => {\n ctx.drainTimer = null;\n // The one observable task boundary: whatever the previous task spent, this one starts fresh.\n ctx.taskKicks = 0;\n pumpEncodes(ctx);\n }, delay);\n }\n // Drained: the next deferral is a new run, and gets the whole bound to itself.\n if (ctx.queue.length === 0) ctx.busyDeferSince = NOT_DEFERRING;\n}\n\n/**\n * May this pass spend a readback? Consulted once per drain pass, and the ONLY place either deferral\n * reason is evaluated: the host's `encode.busy` predicate, and this module's own slow-encode backoff\n * (`encode.slowEncodeMs`, see the module doc's stale-signal trap for why one is not enough).\n *\n * ONE SHARED BOUND. `busyMaxDeferMs` caps one unbroken run of deferrals for ANY reason, not one run\n * per reason — so neither the host's predicate nor the module's own measurement, nor the two\n * alternating, can stop the fleet; they can only slow it. Once the bound elapses the pass encodes and\n * the run restarts from that instant, and the forced pass also CLEARS the backoff, or the bound it\n * just overrode would spend the next one immediately. Under a pinned `perTask` a forced pass is one\n * readback rather than a whole slice.\n *\n * FAIL OPEN. A predicate that throws is a host bug; treating it as \"busy\" would turn that bug into a\n * fleet that never freezes, so a throw reads as \"not busy\" and the pass encodes.\n *\n * `kickable` is false for the one pass that could not encode anyway (the `deferHead` enqueue, whose\n * only job is to arm the drain). Such a pass HOLDS the bound rather than spending it, so the forced\n * encode lands on the next pass that can actually kick one instead of being deferred a second bound.\n */\nfunction encodeDeferred(\n ctx: SwapperContext,\n now: number,\n kickable: boolean,\n): boolean {\n const policy = ctx.policy;\n // `busyMaxDeferMs: 0` is the host asking for NO deferral machinery at all: the predicate is not\n // consulted and neither is the backoff.\n if (policy.busyMaxDeferMs <= 0) return false;\n let hold: \"busy\" | \"backoff\" | null = null;\n if (policy.busy) {\n let busy = false;\n try {\n busy = policy.busy() === true;\n } catch {\n busy = false;\n }\n if (busy) hold = \"busy\";\n }\n // The host's word first: it knows things a readback measurement cannot, and attributing a deferral\n // to the louder signal keeps the two counters diagnostic.\n if (hold === null && ctx.backoffUntil > now) hold = \"backoff\";\n if (hold === null) {\n ctx.busyDeferSince = NOT_DEFERRING;\n return false;\n }\n // The swapper's own counters are the pass-level home; a job's are the fallback for the (unreachable\n // in practice) swapper built without one.\n const counters = ctx.counters ?? ctx.queue[0]?.counters ?? null;\n if (ctx.busyDeferSince === NOT_DEFERRING) {\n ctx.busyDeferSince = now;\n } else if (now - ctx.busyDeferSince >= policy.busyMaxDeferMs && kickable) {\n ctx.busyDeferSince = now;\n ctx.backoffUntil = 0;\n if (counters) counters.staticImageBusyForcedEncodes++;\n return false;\n }\n if (counters) {\n if (hold === \"busy\") counters.staticImageBusyDeferrals++;\n else counters.staticImageBackoffDeferrals++;\n }\n return true;\n}\n\n/**\n * Turn one queued job's frame into an object URL and hand it to the surfaces waiting on that key.\n *\n * TWO SOURCES, one tail. Without a capture hook the job's own canvas is read (`toBlob` on it), which\n * is a SYNCHRONOUS GPU→CPU readback and the thing every pacing lever in this file bounds. With one,\n * the pixels arrive asynchronously from the renderer's own readback and land in a throwaway 2D\n * canvas, which is then encoded by the identical tail — see the module doc's CAPTURE-HOOK SOURCES\n * for why the two costs are booked to DIFFERENT counters.\n */\nfunction encode(ctx: SwapperContext, job: EncodeJob): void {\n const policy = ctx.policy;\n const { entry, counters } = job;\n const publish = (blob: Blob | null): void => {\n if (!blob) {\n fail(entry, counters, job.settle);\n return;\n }\n // \"Everyone let go while the encoder ran\" and \"nobody ever held this\" are different events with\n // the same `refs` reading, and only the first is a reason to throw the pixels away. A BAKE is the\n // second: it was enqueued FOR the pool, so it publishes and is retained (below) rather than\n // dropped. Without this distinction `bakeStill` would encode faithfully and then delete itself.\n if (entry.refs <= 0 && !entry.bake) {\n // Publish nothing, and make sure the empty entry isn't left behind as a lookup hit.\n if (entriesByKey.get(entry.key) === entry) entriesByKey.delete(entry.key);\n job.settle?.(false);\n return;\n }\n entry.url = URL.createObjectURL(blob);\n // What a pooled still would COST, taken while the blob is in hand — it is the only moment this\n // module ever sees the encoded size.\n entry.bytes = typeof blob.size === \"number\" ? Math.max(0, blob.size) : 0;\n liveUrls++;\n counters.staticImageEncodes++;\n noteEncodedKey(entry.key);\n if (entry.refs <= 0) {\n // A BAKE, published: held by nobody, so the pool is the only thing that can keep it. Refused\n // (over budget) it is revoked immediately — the alternative is a URL nothing will ever release.\n const retained = retainStill(entry, ctx);\n if (!retained) dropPooledStill(entry);\n job.settle?.(retained);\n return;\n }\n const waiters = [...entry.waiters];\n entry.waiters.clear();\n for (const waiter of waiters) attach(waiter, entry, counters);\n job.settle?.(true);\n };\n // The synchronous `toBlob` tail, shared by both sources. `source` is the job's own canvas on the\n // direct path and the capture hook's throwaway canvas on the other; `encodeSource` may hand back\n // the swapper's `maxDim` scratch instead of either.\n const encodeFrom = (source: HTMLCanvasElement): number => {\n const started = policy.now();\n try {\n encodeSource(ctx, source, counters).toBlob(publish, ENCODE_MIME);\n } catch {\n fail(entry, counters);\n }\n // The SYNCHRONOUS part is what parks the thread: `toBlob` returns once the readback is done and\n // compresses off-thread. Measuring around the call therefore measures the BLOCK, not the codec.\n const cost = Math.max(0, policy.now() - started);\n counters.staticImageEncodeMs += cost;\n if (cost > counters.staticImageEncodeMaxMs)\n counters.staticImageEncodeMaxMs = cost;\n return cost;\n };\n\n if (job.capture === null) {\n // The direct path, unchanged: the surface's own canvas, read synchronously, and the adaptive\n // backoff armed from that park.\n armSlowBackoff(ctx, counters, encodeFrom(job.canvas));\n return;\n }\n captureThenEncode(ctx, job, encodeFrom);\n}\n\n/** The adaptive backoff's one rule (`encode.slowEncodeMs`), applied to whichever cost the source in\n * hand actually measures: the synchronous `toBlob` park on the direct path, the capture's WALL time\n * on a hook path. Split out so the two sources share the rule rather than each re-stating it. */\nfunction armSlowBackoff(\n ctx: SwapperContext,\n counters: StaticImageSwapCounters,\n cost: number,\n): void {\n const policy = ctx.policy;\n if (policy.slowEncodeMs > 0 && cost >= policy.slowEncodeMs) {\n counters.staticImageSlowEncodes++;\n ctx.backoffUntil = policy.now() + policy.slowBackoffMs;\n }\n}\n\n/**\n * The CAPTURE-HOOK source: await the renderer's own readback, then run the ordinary encode tail over\n * what it produced. Everything downstream of `encodeFrom` — dedup, the stand-in, parked stills — is\n * unchanged, because by then the pixels are in an ordinary 2D canvas.\n *\n * THE GUARD mirrors `publish`'s, because the same thing can happen during a capture that can happen\n * during a compression: every holder can let go (a revert, a dispose, a runtime teardown). A dead\n * entry publishes nothing and is taken out of the lookup so it cannot be a hit for the next surface\n * — and books NO failure, because nothing failed; the surface simply stopped wanting a still.\n *\n * A FAILED capture (null, a throw, a degenerate canvas) goes through the SAME `fail()` every other\n * failure does — so it is retried on the encode cadence under `onInvalidate: \"retry\"` and blocks the\n * key under `\"block\"`, and in neither case can it wedge the queue.\n *\n * A BLANK capture (`STATIC_CAPTURE_BLANK`: the capture completed and holds nothing visible, for a\n * surface the producer knows it drew) is that same failure with two differences — it books its own\n * counter, and it is TERMINAL even under `\"retry\"`. See the module doc's BLANK CAPTURES for both,\n * and for why this module never decides it from the pixels itself.\n */\nfunction captureThenEncode(\n ctx: SwapperContext,\n job: EncodeJob,\n encodeFrom: (source: HTMLCanvasElement) => number,\n): void {\n const policy = ctx.policy;\n const { entry, counters } = job;\n const started = policy.now();\n const finish = (source: StaticSurfaceCapture): void => {\n // WALL time, not park time: this is the GPU handing pixels over, and the backoff's whole job is\n // to notice when that has become expensive (see `encode.slowEncodeMs`).\n const cost = Math.max(0, policy.now() - started);\n counters.staticImageCaptureMs += cost;\n if (cost > counters.staticImageCaptureMaxMs)\n counters.staticImageCaptureMaxMs = cost;\n armSlowBackoff(ctx, counters, cost);\n // …with the same exception `publish` makes: a BAKE is held by nobody on purpose, so `refs <= 0`\n // is not the abandonment this guard is looking for.\n if (entry.refs <= 0 && !entry.bake) {\n if (entriesByKey.get(entry.key) === entry) entriesByKey.delete(entry.key);\n releaseCaptureCanvas(source);\n job.settle?.(false);\n return;\n }\n if (source === STATIC_CAPTURE_BLANK) {\n // The capture answered, and answered with nothing. A capture failure like any other — the\n // surface keeps its live canvas — plus its own counter, plus `terminal`: the cause is the\n // device's capture path, which a retry cannot change (see the module doc).\n counters.staticImageBlankCaptures++;\n counters.staticImageCaptureFailures++;\n fail(entry, counters, job.settle, true);\n return;\n }\n if (!source || source.width < 1 || source.height < 1) {\n counters.staticImageCaptureFailures++;\n releaseCaptureCanvas(source);\n fail(entry, counters, job.settle);\n return;\n }\n counters.staticImageCaptures++;\n encodeFrom(source);\n // Released only AFTER `toBlob` has been called on it: Blink snapshots a `toBlob` source\n // synchronously (the same guarantee the `maxDim` scratch's reuse depends on), so zeroing the\n // backing store here cannot race the compression that is still running off-thread.\n releaseCaptureCanvas(source);\n };\n let pending: Promise<StaticSurfaceCapture>;\n try {\n pending = job.capture?.() ?? Promise.resolve(null);\n } catch {\n // A hook that throws synchronously is the same event as one that rejects.\n finish(null);\n return;\n }\n void pending.then(finish, () => finish(null));\n}\n\n/** A capture canvas is this module's to own for exactly one encode. A 0×0 backing store releases its\n * pixels immediately rather than at the next GC — worth stating for a surface fleet whose stills are\n * megabytes each. A verdict rather than a canvas (`STATIC_CAPTURE_BLANK`) owns no pixels and is\n * simply skipped. */\nfunction releaseCaptureCanvas(canvas: StaticSurfaceCapture): void {\n if (!canvas || canvas === STATIC_CAPTURE_BLANK) return;\n canvas.width = 0;\n canvas.height = 0;\n}\n\n/** The canvas the readback actually reads (`encode.maxDim`). Falls back to the SOURCE for anything it\n * cannot do — a clamp that will not run must never be the reason a surface fails to freeze. */\nfunction encodeSource(\n ctx: SwapperContext,\n canvas: HTMLCanvasElement,\n counters: StaticImageSwapCounters,\n): HTMLCanvasElement {\n const maxDim = ctx.policy.maxDim;\n const longest = Math.max(canvas.width, canvas.height);\n if (maxDim <= 0 || longest <= maxDim) return canvas;\n if (typeof document === \"undefined\") return canvas;\n const scale = maxDim / longest;\n const w = Math.max(1, Math.round(canvas.width * scale));\n const h = Math.max(1, Math.round(canvas.height * scale));\n if (!ctx.scratch) ctx.scratch = document.createElement(\"canvas\");\n const scratch = ctx.scratch;\n const g = scratch.getContext(\"2d\");\n if (!g) {\n // No 2d context in this environment (jsdom, a lost context): keep nothing around for a path that\n // cannot run.\n ctx.scratch = null;\n return canvas;\n }\n // A width/height write CLEARS the backing store; a same-size reuse does not, and the previous\n // surface's pixels would otherwise show through anything this one leaves transparent.\n if (scratch.width !== w || scratch.height !== h) {\n scratch.width = w;\n scratch.height = h;\n } else {\n g.clearRect(0, 0, w, h);\n }\n g.imageSmoothingEnabled = true;\n g.imageSmoothingQuality = \"high\";\n g.drawImage(canvas, 0, 0, w, h);\n counters.staticImageClampedEncodes++;\n return scratch;\n}\n\n/** The encode produced nothing. Under `\"block\"` the key is poisoned and every waiter disqualified\n * (today's behavior); under `\"retry\"` the entry is dropped instead — the key stays open — and each\n * waiter is rescheduled one encode interval out, forever.\n *\n * `terminal` overrides that choice and blocks under EITHER policy. Exactly one caller sets it: a\n * BLANK capture, whose cause is the device's capture path rather than this frame, so a retry can\n * only spend a GPU readback per interval per surface on a question already answered (see the module\n * doc's BLANK CAPTURES). */\nfunction fail(\n entry: StaticImageEntry,\n counters: StaticImageSwapCounters,\n settle?: (published: boolean) => void,\n terminal = false,\n): void {\n counters.staticImageFailures++;\n settle?.(false);\n // A BAKE has no waiter to disqualify and nobody asked for this key, so poisoning it would let one\n // speculative readback lock a key out of the ordinary swap path for the life of the document. Drop\n // it instead: the key goes back to unknown, and the next SURFACE that wants it may try for real.\n if (entry.bake && entry.waiters.size === 0) {\n if (entriesByKey.get(entry.key) === entry) entriesByKey.delete(entry.key);\n return;\n }\n const waiters = [...entry.waiters];\n entry.waiters.clear();\n let anyRetry = false;\n for (const waiter of waiters) {\n const state = waiter.staticImage;\n if (!state || state.entry !== entry) continue;\n entry.refs--;\n state.entry = null;\n const ctx = state.swapper;\n if (ctx.policy.retry && !terminal) {\n anyRetry = true;\n state.retryAfter = ctx.policy.now() + ctx.policy.intervalMs;\n armSweep(ctx);\n } else {\n state.blocked = true; // a key that cannot be encoded is not worth retrying per surface\n }\n }\n if (anyRetry) {\n if (entriesByKey.get(entry.key) === entry) entriesByKey.delete(entry.key);\n } else {\n entry.failed = true;\n }\n}\n\n// ---- internals: the stand-in -------------------------------------------------------------------\n\n/** Point the (possibly new) `<img>` at the entry's URL and, once it can actually paint, put it in\n * the canvas's place. The decode gate is what keeps the surface from blinking: showing an `<img>`\n * that has not decoded yet while hiding the canvas is one blank frame on a phone. */\nfunction attach(\n binding: StaticImageSwapBinding,\n entry: StaticImageEntry,\n counters: StaticImageSwapCounters,\n): void {\n const state = binding.staticImage;\n if (!state || state.entry !== entry || !entry.url) return;\n const img = state.img ?? createStandIn(binding);\n state.img = img;\n img.src = entry.url;\n const ready =\n typeof img.decode === \"function\" ? img.decode() : Promise.resolve();\n void ready.then(\n () => finishAttach(binding, entry, counters),\n () => {\n // A frame that will not decode must never hide the canvas.\n const current = binding.staticImage;\n if (current?.entry !== entry) return;\n counters.staticImageFailures++;\n const policy = current.swapper.policy;\n // Under `retry` the GATE is not the thing that failed, so it is restored across the revert and\n // the surface simply re-encodes one interval later — forever, rather than being disqualified.\n const earned = current.stable;\n revert(binding, counters, !policy.retry, \"decode-failure\");\n if (policy.retry) {\n current.stable = earned;\n current.retryAfter = policy.now() + policy.intervalMs;\n armSweep(current.swapper);\n }\n },\n );\n}\n\nfunction finishAttach(\n binding: StaticImageSwapBinding,\n entry: StaticImageEntry,\n counters: StaticImageSwapCounters,\n): void {\n const state = binding.staticImage;\n // Released, churned or re-keyed while the decode ran.\n if (!state || state.entry !== entry || !state.img) return;\n if (!state.img.isConnected) {\n // Immediately BEFORE the canvas: same position in the child list, so the same paint order\n // among the self-layer's positioned children.\n const parent = binding.canvas.parentNode;\n // A canvas already out of the DOM means the node went away mid-decode; leave the swap\n // unfinished (nothing paints either way) and let the pending dispose release the URL.\n if (!parent) return;\n parent.insertBefore(state.img, binding.canvas);\n }\n state.shown = true;\n counters.staticImageSwaps++;\n counters.staticImagesLive++;\n // A CLAIM that made it all the way to the screen (see `claimStaticStill`): counted here rather\n // than at the claim, so the gap between `staticStillCacheHits` and this is exactly the claims\n // undone before their decode landed.\n if (state.claimed) counters.staticStillMounts++;\n applySurfaceVisibility(binding);\n // The watchdog's evidence of a legitimate freeze (see the module doc): the paint count, the\n // backing-store size, and the box the stand-in copied — all read AFTER the hide, which is itself\n // a write to `canvas.style`.\n state.drawSeqAtFreeze = state.drawSeq;\n state.frozenW = binding.canvas.width;\n state.frozenH = binding.canvas.height;\n state.boxCss = binding.canvas.style.cssText;\n armSweep(state.swapper);\n}\n\n/** A stand-in whose box is the canvas's box, exactly. The canvas carries its geometry as INLINE\n * style (`position/left/top/width/height`, `pointer-events`, and for a NinePatch fill a\n * `mask-box-image`), and no gsw stylesheet keys off `[data-godot-shader-canvas]`, so copying\n * `cssText` wholesale reproduces the box AND anything a host set on the canvas without this file\n * having to enumerate it. `mix-blend-mode` is not copied because it does not live here: the host\n * sets it on the NODE (a canvas-level blend cannot reach the DOM painted behind the node), so the\n * swap leaves it alone. */\nfunction createStandIn(binding: StaticImageSwapBinding): HTMLImageElement {\n const img = document.createElement(\"img\");\n img.setAttribute(STATIC_SURFACE_IMAGE_ATTR, \"true\");\n img.alt = \"\";\n copyStandInBox(img, binding.canvas);\n // Hidden until decoded; `applySurfaceVisibility` owns it from then on.\n img.style.display = \"none\";\n return img;\n}\n\nfunction copyStandInBox(\n img: HTMLImageElement,\n canvas: HTMLCanvasElement,\n): void {\n img.style.cssText = canvas.style.cssText;\n // `fill` + `auto` are the canvas's own semantics (the backing store is stretched to the CSS box\n // with default smoothing), pinned explicitly so a host's `img { object-fit }` cannot resample the\n // frame differently than the canvas presented it.\n img.style.objectFit = \"fill\";\n img.style.imageRendering = \"auto\";\n}\n\nfunction revert(\n binding: StaticImageSwapBinding,\n counters: StaticImageSwapCounters,\n block: boolean,\n cause: StaticImageRevertCause,\n): void {\n const state = binding.staticImage;\n if (!state) return;\n const wasShown = state.shown;\n detachImage(state);\n if (state.entry) {\n // PARKED STILLS: a revert that is not evidence of a repaint keeps its encoded frame around for\n // this surface's next freeze instead of revoking it (see `parkableStill`).\n // RETAINED STILLS: a KEYED entry's last release keeps it for whoever reaches that key next,\n // whatever this surface's own fingerprint says — the two are tried in that order inside\n // `release`, and the only revert excluded from retention is a DECODE FAILURE, on its own\n // evidence: those pixels would not paint here and will not paint for the next surface either.\n // A BLOCKING revert is deliberately NOT excluded. Blocking disqualifies this SURFACE (the\n // content-key gate's churn case), and says nothing about the frame the old key named — which\n // another surface on that key may still be showing.\n release(\n state.entry,\n binding,\n parkableStill(binding, state, wasShown, block, cause) ? state : null,\n cause !== \"decode-failure\",\n );\n state.entry = null;\n state.claimed = false;\n }\n state.shown = false;\n state.stable = 0;\n state.drawSeqAtFreeze = -1;\n if (block) state.blocked = true;\n if (wasShown) {\n counters.staticImageReverts++;\n counters.staticImageRevertsByCause[cause]++;\n counters.staticImagesLive--;\n }\n applySurfaceVisibility(binding);\n // LAST, with the revert fully settled (see `StaticSurfacePolicy.onRevert`): the host runtime may\n // need to build and paint the surface this uncovered, and it must find a consistent state to do it\n // from. A re-entrant revert from inside the handler is a no-op — there is no entry left.\n const onRevert = state.swapper.policy.onRevert;\n if (onRevert) {\n try {\n onRevert(binding);\n } catch {\n // A host bug must not leave the surface half-reverted; everything above has already happened.\n }\n }\n}\n\nfunction detachImage(state: StaticImageState): void {\n const img = state.img;\n if (!img) return;\n img.remove();\n // Drop the reference to the blob so the decoded bitmap can go even if the element is retained.\n img.removeAttribute(\"src\");\n state.img = null;\n}\n\n/** Drop one holder's ref. At 0 the URL is revoked and the key stops being a lookup hit — unless the\n * entry can be POOLED instead, in which case it survives unheld:\n * - `retain` (and a real key, and `encode.stillCacheBytes`) keeps it claimable by ANY surface\n * that reaches that key — tried FIRST, because it is strictly the stronger claim: it needs no\n * fingerprint and it outlives this binding entirely;\n * - `parkFor` keeps it claimable by that ONE surface, on the fingerprint (`parkStill`), which is\n * all a keyless surface can ever have.\n * Neither, or both refused ⇒ the revoke this has always done. */\nfunction release(\n entry: StaticImageEntry,\n binding: StaticImageSwapBinding,\n parkFor: StaticImageState | null = null,\n retain = false,\n): void {\n entry.waiters.delete(binding);\n entry.refs--;\n if (entry.refs > 0) return;\n const state = binding.staticImage;\n if (retain && state && retainStill(entry, state.swapper)) {\n // The fingerprint claim is superseded, not kept alongside: the entry is now claimable by key,\n // and two claims on one pooled entry would be two ways to un-pool it.\n if (state.parked === entry) state.parked = null;\n return;\n }\n if (parkFor && parkStill(entry, parkFor)) return;\n revoke(entry);\n if (entriesByKey.get(entry.key) === entry) entriesByKey.delete(entry.key);\n}\n\nfunction revoke(entry: StaticImageEntry): void {\n entry.waiters.clear();\n if (entry.url === null) return;\n URL.revokeObjectURL(entry.url);\n entry.url = null;\n liveUrls--;\n}\n\n// ---- internals: the sweep (quiet windows, retries, the watchdog) --------------------------------\n\nfunction cancelTimers(ctx: SwapperContext): void {\n if (ctx.drainTimer !== null) {\n ctx.policy.clearT(ctx.drainTimer);\n ctx.drainTimer = null;\n }\n if (ctx.sweepTimer !== null) {\n ctx.policy.clearT(ctx.sweepTimer);\n ctx.sweepTimer = null;\n }\n ctx.sweepAt = Number.POSITIVE_INFINITY;\n}\n\n/** The next absolute `now()` at which the swapper has anything to do, or Infinity for \"nothing\". */\nfunction nextSweepAt(ctx: SwapperContext): number {\n const policy = ctx.policy;\n // FAST PATH for the DEFAULT policy: with no quiet window, no retries and no watchdog there is\n // nothing this swapper can ever defer, so the O(bindings) scan below is skipped entirely. It\n // matters because `revertStaticImage` re-arms, and a host re-size reverts every binding in a loop\n // — which would otherwise be O(bindings²) for a mechanism that never arms a timer at all.\n if (!policy.quietWindow && !policy.retry && policy.watchdogMs <= 0) {\n return Number.POSITIVE_INFINITY;\n }\n let next = Number.POSITIVE_INFINITY;\n let anyShown = false;\n for (const binding of ctx.bindings) {\n const state = binding.staticImage;\n if (!state) continue;\n if (state.shown) {\n anyShown = true;\n continue;\n }\n if (state.entry || state.blocked) continue;\n if (policy.quietWindow) {\n next = Math.min(\n next,\n Math.max(\n state.lastDrawAt + quietWindowFor(state, policy),\n state.retryAfter,\n ),\n );\n } else if (state.retryAfter > 0) {\n next = Math.min(next, state.retryAfter);\n }\n }\n if (anyShown && policy.watchdogMs > 0) {\n next = Math.min(next, ctx.lastWatchdogAt + policy.watchdogMs);\n }\n return next;\n}\n\nfunction armSweep(ctx: SwapperContext): void {\n const policy = ctx.policy;\n if (ctx.disposed || !policy.setT) return;\n const at = nextSweepAt(ctx);\n if (at === Number.POSITIVE_INFINITY) {\n if (ctx.sweepTimer !== null) {\n policy.clearT(ctx.sweepTimer);\n ctx.sweepTimer = null;\n ctx.sweepAt = Number.POSITIVE_INFINITY;\n }\n return;\n }\n if (ctx.sweepTimer !== null && ctx.sweepAt <= at) return; // an earlier wakeup is already coming\n if (ctx.sweepTimer !== null) policy.clearT(ctx.sweepTimer);\n const now = policy.now();\n // Never 0: a sweep that finds nothing due must not be able to spin on the same instant.\n const delay = Math.max(1, at - now);\n ctx.sweepAt = now + delay;\n ctx.sweepTimer = policy.setT(() => runSweep(ctx), delay);\n}\n\n/** `armSweep` for the per-draw hot path: an already-armed sweep is never too late for a deadline\n * that only moved OUT, and skipping the arm skips the O(bindings) deadline scan. */\nfunction armSweepIfIdle(ctx: SwapperContext): void {\n if (ctx.sweepTimer === null) armSweep(ctx);\n}\n\nfunction runSweep(ctx: SwapperContext): void {\n ctx.sweepTimer = null;\n ctx.sweepAt = Number.POSITIVE_INFINITY;\n if (ctx.disposed) return;\n // A task boundary, like the drain timer's: this sweep may make many surfaces eligible, and the\n // first of them is entitled to the fresh task budget `pumpEncodes` bounds them by.\n ctx.taskKicks = 0;\n const policy = ctx.policy;\n const now = policy.now();\n if (policy.watchdogMs > 0 && now - ctx.lastWatchdogAt >= policy.watchdogMs) {\n ctx.lastWatchdogAt = now;\n runWatchdog(ctx, now);\n }\n for (const binding of [...ctx.bindings]) {\n const state = binding.staticImage;\n if (!state || state.shown || state.entry || state.blocked) continue;\n if (state.retryAfter > now) continue;\n state.retryAfter = 0;\n if (policy.quietWindow) {\n // (`windowMs`, not `window`: shadowing the global in a DOM module is a trap for the next\n // reader, not a name.)\n const windowMs = quietWindowFor(state, policy);\n if (now - state.lastDrawAt < windowMs) continue;\n maybeSwap(binding, countersFor(state, ctx));\n // DECLINED (dirty, dormant, host-vetoed, zero-sized, off-document, no encoder). Its window is\n // already elapsed, so without a back-off the next arm would land 1 ms later and every one\n // after that too — a spin for as long as the surface stays ineligible. One attempt per window.\n // A retry that something else already scheduled (a failed encode paces itself on the encode\n // interval) is left alone.\n // The back-off is the PLAIN window even for a keyed surface, and deliberately: the keyed\n // deadline says how fresh these pixels are, while a DECLINE is about the surface's\n // circumstances (parked, host-vetoed, unsized, off-document), which do not change on that\n // clock. A host pinning `keyedQuietMs: 0` would otherwise turn one vetoed surface into a 1 ms\n // sweep spin for as long as the veto stands.\n if (!state.entry && !state.shown && state.retryAfter === 0) {\n state.retryAfter = now + Math.max(1, policy.quietMs);\n }\n } else if (state.stable >= policy.observations) {\n // A `retry` policy's rescheduled encode: the gate is still satisfied, only the encode failed.\n maybeSwap(binding, countersFor(state, ctx));\n }\n }\n armSweep(ctx);\n}\n\n/**\n * THE WATCHDOG (see the module doc). Every swapped surface must still be legitimately frozen; the\n * observable proxies for \"it is not\" are: the canvas left the DOM, a re-render is pending, the\n * backing store was re-allocated (which CLEARS it), or a paint was reported since the freeze. A\n * canvas that merely MOVED is not stale — its stand-in is just at the old box, so that is re-synced\n * rather than reverted.\n */\nfunction runWatchdog(ctx: SwapperContext, now: number): void {\n for (const binding of [...ctx.bindings]) {\n const state = binding.staticImage;\n if (!state?.shown) continue;\n const canvas = binding.canvas;\n const unexplained =\n !canvas.isConnected ||\n binding.dirty ||\n state.drawSeq !== state.drawSeqAtFreeze ||\n canvas.width !== state.frozenW ||\n canvas.height !== state.frozenH;\n if (unexplained) {\n revert(binding, countersFor(state, ctx), false, \"watchdog\");\n // The watchdog fires on exactly the evidence a KEY cannot survive: the backing store was\n // re-allocated (and therefore cleared), a repaint is owed, or a paint landed that this module\n // never accepted. So the surface goes back to keyless until it paints and says otherwise —\n // without this, a short `keyedQuietMs` would re-attach the same stale still on the next sweep.\n // (Same rule as `resetGate`, applied here because the watchdog resets the clock by hand.)\n state.key = null;\n state.lastDrawAt = now;\n continue;\n }\n if (state.img && canvas.style.cssText !== state.boxCss) {\n copyStandInBox(state.img, canvas);\n applySurfaceVisibility(binding);\n state.boxCss = canvas.style.cssText;\n }\n }\n}\n\n/** The counters a TIMER-driven path bumps: the ones this surface was last noted with, else the\n * swapper's own. The throwaway is unreachable in practice (a registered surface always has one of\n * the two) and exists so no timer path can be null-checked into skipping its revert. */\nfunction countersFor(\n state: StaticImageState,\n ctx: SwapperContext,\n): StaticImageSwapCounters {\n return state.counters ?? ctx.counters ?? createStaticImageSwapCounters();\n}\n","import {\n normalizeParticleCurve,\n type ParticleCurvePoint,\n type ParticleGradientStop,\n sampleParticleCurve,\n sampleParticleGradient,\n} from \"@godot-scene-web/effects/particles\";\n\n// Bakes Godot procedural texture resources (the sampler inputs of a ShaderMaterial)\n// into raw RGBA pixels the WebGL runtime can upload directly — no canvas/DOM, so it\n// runs and is unit-testable in plain node. Two kinds today, matching the procedural\n// sampler resources a ShaderMaterial may carry:\n// - GradientTexture1D (a `Gradient` color ramp) -> exact.\n// - NoiseTexture2D (a `FastNoiseLite` field) -> a compact Perlin/FBM\n// equivalent (Godot's FastNoiseLite uses different permutation tables, so the\n// bytes differ, but the type/frequency/FBM params and look match).\n//\n// `material.ts` serializes the spec (pixel-free, numbers only) onto the node; the\n// runtime calls `bakeTexture` to realize the pixels lazily and caches the result.\n\nexport interface GradientStop extends ParticleGradientStop {}\n\nexport interface GradientBakeSpec {\n kind: \"gradient\";\n /** Output width (GradientTexture1D default 256). */\n width: number;\n stops: GradientStop[];\n /**\n * Godot `Gradient.interpolation_mode`: 0/absent = LINEAR (default), 1 = CONSTANT\n * (hold each stop's color until the next — a stepped ramp), 2 = CUBIC (approximated\n * as linear). See `sampleGradient`.\n */\n interpolationMode?: number;\n}\n\n/** Godot `Gradient.GradientInterpolationMode.CONSTANT` — hold each stop, no blend. */\nexport const GRADIENT_INTERPOLATE_CONSTANT = 1;\n\nexport interface NoiseBakeSpec {\n kind: \"noise\";\n /** Output size (NoiseTexture2D default 512×512). */\n width: number;\n height: number;\n /** Godot `seamless` (edge-tiling). Not reproduced; recorded for fidelity. */\n seamless: boolean;\n /** Godot `FastNoiseLite.NoiseType` (3 = Perlin — the only type modelled exactly). */\n noiseType: number;\n /** Per-texel coordinate scale, before fractal octaves. */\n frequency: number;\n /** Godot `FastNoiseLite.FractalType` (0 = none, 1 = FBM, others approximated as FBM). */\n fractalType: number;\n octaves: number;\n lacunarity: number;\n gain: number;\n seed: number;\n}\n\nexport interface CurvePoint extends ParticleCurvePoint {}\n\nexport interface CurveBakeSpec {\n kind: \"curve\";\n width: number;\n channels: CurvePoint[][];\n}\n\nexport type TextureBakeSpec = GradientBakeSpec | NoiseBakeSpec | CurveBakeSpec;\n\nexport interface BakedPixels {\n width: number;\n height: number;\n /** RGBA8, row-major, top-left origin. */\n data: Uint8ClampedArray;\n}\n\n// Godot enum values referenced above (FastNoiseLite).\nexport const NOISE_TYPE_PERLIN = 3;\nexport const FRACTAL_NONE = 0;\n\nexport function bakeTexture(spec: TextureBakeSpec): BakedPixels {\n if (spec.kind === \"gradient\") return bakeGradient(spec);\n if (spec.kind === \"curve\") return bakeCurve(spec);\n return bakeNoise(spec);\n}\n\n// ---- gradient --------------------------------------------------------------\n\nexport function bakeGradient(spec: GradientBakeSpec): BakedPixels {\n const width = Math.max(1, Math.round(spec.width) || 1);\n const data = new Uint8ClampedArray(width * 4);\n const stops = [...spec.stops].sort((a, b) => a.offset - b.offset);\n for (let x = 0; x < width; x += 1) {\n const t = width === 1 ? 0 : x / (width - 1);\n const [r, g, b, a] = sampleGradient(stops, t, spec.interpolationMode);\n const i = x * 4;\n data[i] = r * 255;\n data[i + 1] = g * 255;\n data[i + 2] = b * 255;\n data[i + 3] = a * 255;\n }\n return { width, height: 1, data };\n}\n\n// Godot's default Gradient interpolation is linear between adjacent stops, with the\n// endpoints held constant outside the stop range. Exported so the particle runtime\n// samples color ramps over particle lifetime with the SAME math the bake path uses.\n//\n// `interpolationMode` mirrors Godot's `Gradient.interpolation_mode`: 1 (CONSTANT) holds\n// each stop's color until the NEXT stop instead of blending — STS2's VFX color LUTs are\n// authored that way (a two-stop grey→white step), and blending them turns a hard edge\n// into a gradient. Anything else (0 LINEAR, 2 CUBIC) uses the linear path.\nexport const sampleGradient = sampleParticleGradient;\n\n// ---- curve -----------------------------------------------------------------\n\nexport function bakeCurve(spec: CurveBakeSpec): BakedPixels {\n const width = Math.max(1, Math.round(spec.width) || 1);\n const sourceChannels = spec.channels.length > 0 ? spec.channels : [[]];\n const channels = sourceChannels.map(\n (points) => normalizeParticleCurve(points) ?? [],\n );\n const data = new Uint8ClampedArray(width * 4);\n for (let x = 0; x < width; x += 1) {\n const t = width === 1 ? 0 : x / (width - 1);\n const r = channels[0] ? sampleParticleCurve(channels[0], t) : 0;\n const g = channels[1] ? sampleParticleCurve(channels[1], t) : r;\n const b = channels[2] ? sampleParticleCurve(channels[2], t) : r;\n const a = channels[3] ? sampleParticleCurve(channels[3], t) : 1;\n const i = x * 4;\n data[i] = clamp01(r) * 255;\n data[i + 1] = clamp01(g) * 255;\n data[i + 2] = clamp01(b) * 255;\n data[i + 3] = clamp01(a) * 255;\n }\n return { width, height: 1, data };\n}\n\n// Exported so the particle runtime samples scale/alpha curves over lifetime with the\n// SAME (linear, endpoint-clamped) math the bake path uses.\nexport function sampleCurve(points: CurvePoint[], t: number): number {\n return sampleParticleCurve(normalizeParticleCurve(points) ?? [], t);\n}\n\n// ---- noise -----------------------------------------------------------------\n\nexport function bakeNoise(spec: NoiseBakeSpec): BakedPixels {\n const width = Math.max(1, Math.round(spec.width) || 1);\n const height = Math.max(1, Math.round(spec.height) || 1);\n const octaves =\n spec.fractalType === FRACTAL_NONE\n ? 1\n : Math.max(1, Math.round(spec.octaves) || 1);\n const lacunarity = spec.lacunarity || 2;\n const gain = spec.gain || 0.5;\n const frequency = spec.frequency || 0.01;\n const baseSeed = (spec.seed | 0) >>> 0;\n\n // One permutation table per octave (seed + octave), built once up front — Godot's\n // FastNoiseLite increments the seed per fractal octave to decorrelate them.\n const perms: Uint8Array[] = [];\n for (let o = 0; o < octaves; o += 1) {\n perms.push(makePerm((baseSeed + o) >>> 0));\n }\n // Normalize the FBM sum so it stays ~[-1,1] (mirrors FastNoiseLite's fractal\n // bounding = 1 / Σ gain^o, with weighted_strength left at its default 0).\n let ampSum = 0;\n for (let o = 0, amp = 1; o < octaves; o += 1, amp *= gain) ampSum += amp;\n const bounding = ampSum > 0 ? 1 / ampSum : 1;\n\n const data = new Uint8ClampedArray(width * height * 4);\n for (let y = 0; y < height; y += 1) {\n for (let x = 0; x < width; x += 1) {\n let sum = 0;\n let amp = 1;\n let freqMul = 1;\n for (let o = 0; o < octaves; o += 1) {\n const sx = x * frequency * freqMul;\n const sy = y * frequency * freqMul;\n sum += perlin2(perms[o], sx, sy) * amp;\n amp *= gain;\n freqMul *= lacunarity;\n }\n // FastNoiseLite returns ~[-1,1]; NoiseTexture2D remaps to [0,1] for the image.\n const v = clamp01(sum * bounding * 0.5 + 0.5);\n const g = v * 255;\n const i = (y * width + x) * 4;\n data[i] = g;\n data[i + 1] = g;\n data[i + 2] = g;\n data[i + 3] = 255;\n }\n }\n return { width, height, data };\n}\n\n// Seeded Fisher–Yates shuffle of 0..255, doubled to 512 (classic Perlin permutation).\nfunction makePerm(seed: number): Uint8Array {\n const p = new Uint8Array(256);\n for (let i = 0; i < 256; i += 1) p[i] = i;\n let s = seed >>> 0 || 0x9e3779b9; // xorshift32, avoid the 0 fixed point\n for (let i = 255; i > 0; i -= 1) {\n s ^= s << 13;\n s ^= s >>> 17;\n s ^= s << 5;\n s >>>= 0;\n const j = s % (i + 1);\n const tmp = p[i];\n p[i] = p[j];\n p[j] = tmp;\n }\n const perm = new Uint8Array(512);\n for (let i = 0; i < 512; i += 1) perm[i] = p[i & 255];\n return perm;\n}\n\n// Improved-Perlin 2D gradient noise, output ~[-1,1].\nfunction perlin2(perm: Uint8Array, x: number, y: number): number {\n const xi = Math.floor(x) & 255;\n const yi = Math.floor(y) & 255;\n const xf = x - Math.floor(x);\n const yf = y - Math.floor(y);\n const u = fade(xf);\n const v = fade(yf);\n const aa = perm[perm[xi] + yi];\n const ab = perm[perm[xi] + yi + 1];\n const ba = perm[perm[xi + 1] + yi];\n const bb = perm[perm[xi + 1] + yi + 1];\n const x1 = lerp(grad2(aa, xf, yf), grad2(ba, xf - 1, yf), u);\n const x2 = lerp(grad2(ab, xf, yf - 1), grad2(bb, xf - 1, yf - 1), u);\n return lerp(x1, x2, v);\n}\n\n// 4 diagonal gradients — the standard 2D Perlin simplification.\nfunction grad2(hash: number, x: number, y: number): number {\n switch (hash & 3) {\n case 0:\n return x + y;\n case 1:\n return -x + y;\n case 2:\n return x - y;\n default:\n return -x - y;\n }\n}\n\nfunction fade(t: number): number {\n return t * t * t * (t * (t * 6 - 15) + 10);\n}\n\nfunction lerp(a: number, b: number, t: number): number {\n return a + t * (b - a);\n}\n\nfunction clamp01(value: number): number {\n return value < 0 ? 0 : value > 1 ? 1 : value;\n}\n","// Shared WebGL2 plumbing used by BOTH live runtimes (the ShaderMaterial runtime in\n// `runtime.ts` and the particle runtime in `../particles/runtime.ts`). Keeping it in\n// one module is load-bearing: both runtimes MUST share\n// - ONE WebGL2 context (an offscreen canvas), so we never hit the browser's ~16\n// live-context limit even with many cards + particle systems on a page, and\n// - ONE module-scoped texture cache, so identical textures upload once, and\n// - ONE monotonic clock origin, so `TIME` is continuous across attach/detach and\n// across the two runtimes (a per-runtime origin would reset animations to 0).\n// Duplicating any of these would defeat those guarantees.\n\nimport {\n createWebglFullscreenQuad,\n createWebglPlaceholderTexture,\n createWebglTexture,\n uploadWebglTexture,\n} from \"@godot-scene-web/canvas-effects/webgl\";\nimport { bakeTexture, type TextureBakeSpec } from \"./bake-texture\";\n\nexport interface SharedGl {\n canvas: HTMLCanvasElement;\n gl: WebGL2RenderingContext;\n /** Full-screen clip-space [-1,1] quad as a TRIANGLE_STRIP (the shader runtime). */\n quad: WebGLBuffer;\n}\n\nexport interface TextureEntry {\n texture: WebGLTexture | null;\n width: number;\n height: number;\n loaded: boolean;\n listeners: Set<() => void>;\n}\n\n// Module-scope shared state, persisted across attach/detach cycles.\nlet shared: SharedGl | null | undefined; // undefined = not tried; null = unavailable\nconst textureCache = new Map<string, TextureEntry>();\n// The shared canvas's ACTUAL drawing-buffer ceiling, discovered at runtime (see\n// `ensureSharedDrawSize`). Infinity until a grow comes back smaller than requested; latched so a\n// doomed realloc isn't re-attempted (and the buffer re-cleared) on every draw. Module-scoped like\n// the canvas itself — both runtimes share the one buffer, so they must share its one ceiling.\nlet sharedMaxWidth = Number.POSITIVE_INFINITY;\nlet sharedMaxHeight = Number.POSITIVE_INFINITY;\n// The `TIME` clock origin — module-scoped (set once) so it is MONOTONIC across\n// attach/detach cycles and across both runtimes. A re-render (e.g. focusing a\n// creature adds tooltip nodes) tears down and re-attaches a runtime; a per-attach\n// origin would reset TIME to 0 and visibly restart every animation. Persisting it\n// keeps TIME continuous, like Godot's `TIME` (seconds since start).\nlet clockOrigin: number | undefined;\n\n// Renderer strings that mean WebGL is being rasterized on the CPU (no usable GPU):\n// SwiftShader (Chromium's software fallback, incl. the ANGLE/Vulkan-Subzero variant),\n// Mesa's llvmpipe/softpipe, and Windows' Basic Render Driver. Running our full-screen\n// fragment shaders (doom-bar FBM noise, card-ripple SDF) on these pegs the CPU for no\n// visual gain over the CSS/SVG fallback — so we decline WebGL and let the fallback render.\nconst SOFTWARE_RENDERER_RE =\n /swiftshader|llvmpipe|softpipe|\\bsoftware\\b|basic render|paravirtual/i;\n\n// Read the unmasked GL renderer string, if the browser exposes it. Returns \"\" when the\n// `WEBGL_debug_renderer_info` extension is unavailable (e.g. privacy-masked) — callers\n// must treat \"\" as UNKNOWN, not software, so a real GPU is never disabled on uncertainty.\nfunction readRendererString(\n gl: WebGLRenderingContext | WebGL2RenderingContext,\n): string {\n try {\n const ext = gl.getExtension(\"WEBGL_debug_renderer_info\");\n if (!ext) return \"\";\n return String(gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) ?? \"\");\n } catch {\n return \"\";\n }\n}\n\nexport interface GpuInfo {\n /** The unmasked GL renderer string, or \"\" when masked/unknown/unavailable. */\n renderer: string;\n /** True only when `renderer` POSITIVELY matches a software rasterizer — never on \"\" (unknown). */\n software: boolean;\n /** True when WebGL is unavailable at all (no context). */\n unavailable: boolean;\n}\n\nlet gpuInfo: GpuInfo | undefined;\n\n// Probe the GPU once for a consumer's device-tier heuristic. Uses its OWN throwaway context (then\n// drops it) so it works even on software renderers, where `getShared` returns null + loses its\n// context. Latched. Returns `software:false` on an unknown/masked string so a real GPU is never\n// mis-classified as software on uncertainty (callers fall back to other low-end signals).\nexport function describeGpu(): GpuInfo {\n if (gpuInfo) return gpuInfo;\n try {\n if (typeof document === \"undefined\") {\n gpuInfo = { renderer: \"\", software: false, unavailable: true };\n return gpuInfo;\n }\n const canvas = document.createElement(\"canvas\");\n const gl = (canvas.getContext(\"webgl2\") || canvas.getContext(\"webgl\")) as\n | WebGLRenderingContext\n | WebGL2RenderingContext\n | null;\n if (!gl) {\n gpuInfo = { renderer: \"\", software: false, unavailable: true };\n return gpuInfo;\n }\n const renderer = readRendererString(gl);\n gpuInfo = {\n renderer,\n software: SOFTWARE_RENDERER_RE.test(renderer),\n unavailable: false,\n };\n gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n return gpuInfo;\n } catch {\n gpuInfo = { renderer: \"\", software: false, unavailable: true };\n return gpuInfo;\n }\n}\n\nexport function getShared(): SharedGl | null {\n if (shared !== undefined) {\n return shared;\n }\n try {\n const canvas = document.createElement(\"canvas\");\n const gl = canvas.getContext(\"webgl2\", {\n // THE ALPHA CONTRACT OF THIS CANVAS, and the one line every fragment that draws here must\n // agree with. `premultipliedAlpha` does not change a single byte in the drawing buffer — it\n // tells the browser how to READ them, on every `drawImage` out of this canvas and on every\n // page composite. Declare it wrong and nothing errors; the picture is simply wrong, and no\n // readback can see it, because the declaration is not in the buffer.\n //\n // TRUE, i.e. every consumer writes `(rgb·a, a)`:\n // - `./transpile.ts` emits `fragColor = vec4(COLOR.rgb * COLOR.a, COLOR.a);` and\n // `./shader-backend.ts` draws with BLEND off, so COLOR lands premultiplied;\n // - core's `particles/render-webgl.ts` emits a premultiplied fragment under\n // `blendFuncSeparate(ONE, ONE_MINUS_SRC_ALPHA, ONE, ONE_MINUS_SRC_ALPHA)`, and its\n // additive resolve presents the accumulated total as `(light, cov)`.\n // That is byte-for-byte the WebGPU contract (`../webgpu/device.ts` configures\n // `alphaMode: \"premultiplied\"`, both WGSL fragments return `rgb*a`), which is the point: ONE\n // statement of the rule for both backends instead of two that have to be kept in sync.\n //\n // It used to be FALSE, and the MIX particle path was wrong for it: a straight declaration\n // over `blendFuncSeparate(SRC_ALPHA, …)` — which is itself a premultiplying operation over a\n // cleared buffer — made the blit into each node canvas multiply by alpha a SECOND time, so\n // MIX particles composited at roughly a² instead of a. `shared-gl.test.ts` pins this line and\n // `test-harness`'s `webglCompositeXvfb.test.ts` measures the result on a real page.\n premultipliedAlpha: true,\n alpha: true,\n antialias: false,\n preserveDrawingBuffer: false,\n });\n if (!gl) {\n shared = null;\n return null;\n }\n // Software-rendered WebGL (SwiftShader/llvmpipe/…) runs every shader on the CPU; treat it\n // as \"no GPU available\" and fall back to CSS/SVG — UNLESS positively a hardware renderer,\n // or the string is masked/unknown (default ON so a real GPU is never penalised). Escape\n // hatch: set `globalThis.__gswForceWebglShaders = true` to run shaders even on software\n // (e.g. to exercise the shader path on a headless/CI box).\n const forceWebgl =\n (globalThis as Record<string, unknown>).__gswForceWebglShaders === true;\n if (!forceWebgl && SOFTWARE_RENDERER_RE.test(readRendererString(gl))) {\n gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n shared = null;\n return null;\n }\n const quad = createWebglFullscreenQuad(gl);\n if (!quad) {\n shared = null;\n return null;\n }\n shared = { canvas, gl, quad };\n return shared;\n } catch {\n shared = null;\n return null;\n }\n}\n\n// A reported drawing-buffer dimension, or `fallback` (the canvas attribute) when the context\n// doesn't report one (test fakes / non-browser contexts) — trusting the attribute there keeps the\n// pre-verification behavior exactly.\nfunction actualBufferDim(reported: unknown, fallback: number): number {\n return typeof reported === \"number\" &&\n Number.isFinite(reported) &&\n reported > 0\n ? reported\n : fallback;\n}\n\n/** The size a caller can REALLY draw + blit for a w×h node render on the shared canvas:\n * `vw`×`vh` (≤ w×h) and the buffer height `bufH` the bottom-left blit's source rows must be\n * measured against. On a healthy context this is exactly {w, h, canvas.height}. */\nexport interface SharedDrawSize {\n vw: number;\n vh: number;\n bufH: number;\n}\n\n// Grow the shared canvas (grow-only, as both runtimes always did — reallocating per node per\n// frame was a realloc storm) so a w×h draw fits, and VERIFY the drawing buffer actually reached\n// the requested size. Setting `canvas.width/height` only REQUESTS a realloc: the GL\n// implementation may come back SMALLER — the GPU's max texture/renderbuffer size, or an\n// allocation failure that keeps the previous buffer (Chrome restores the last-known-good size).\n// Trusting the attribute is the UNDERDOCKS widened-background black-band bug: the viewport/\n// scissor spanned the attribute size and the blit's source rect read columns/rows past the real\n// buffer → transparent → a band at exactly oldBuffer/newAttribute of the node canvas. When the\n// buffer comes back short, the attribute is snapped DOWN to it (so canvas-as-image dimensions\n// match the pixels that exist), the ceiling is latched (no per-draw realloc retry; the retry\n// would also CLEAR the buffer every draw), and the caller renders at vw×vh and scales its blit\n// up to the node's full backing — complete content at reduced resolution, never a clipped band.\nexport function ensureSharedDrawSize(\n sharedGl: SharedGl,\n w: number,\n h: number,\n): SharedDrawSize {\n const { gl, canvas } = sharedGl;\n const wantW = Math.min(w, sharedMaxWidth);\n const wantH = Math.min(h, sharedMaxHeight);\n if (canvas.width < wantW) canvas.width = wantW;\n if (canvas.height < wantH) canvas.height = wantH;\n let bufW = actualBufferDim(gl.drawingBufferWidth, canvas.width);\n let bufH = actualBufferDim(gl.drawingBufferHeight, canvas.height);\n if (bufW < canvas.width || bufH < canvas.height) {\n if (bufW < canvas.width) sharedMaxWidth = bufW;\n if (bufH < canvas.height) sharedMaxHeight = bufH;\n // Snap at the achievable size (a realloc the implementation just proved it can hold; it\n // clears the buffer, which is fine — every caller draws immediately after).\n if (canvas.width !== bufW) canvas.width = bufW;\n if (canvas.height !== bufH) canvas.height = bufH;\n bufW = actualBufferDim(gl.drawingBufferWidth, canvas.width);\n bufH = actualBufferDim(gl.drawingBufferHeight, canvas.height);\n }\n return { vw: Math.min(w, bufW), vh: Math.min(h, bufH), bufH };\n}\n\nexport function getTexture(\n gl: WebGL2RenderingContext,\n url: string,\n repeat = false,\n maxDim?: number,\n): TextureEntry {\n return getImageTexture(gl, url, { repeat, maxDim });\n}\n\n// Downscale a loaded image to ≤ maxDim on its longest edge BEFORE uploading, so a huge source (e.g. a\n// full-screen background ~4096²) doesn't cost a ~250ms main-thread `texImage2D` upload. Aspect is preserved\n// (so uvFit is unchanged); the shader samples UV [0,1] either way, just at a lower internal resolution. A no-op\n// when maxDim is unset/0, the image already fits, or there's no DOM (SSR). Returns the upload source + its dims.\nexport function downscaleForUpload(\n image: HTMLImageElement,\n maxDim: number | undefined,\n): { source: TexImageSource; width: number; height: number } {\n const w = image.naturalWidth || 1;\n const h = image.naturalHeight || 1;\n if (\n !maxDim ||\n maxDim <= 0 ||\n (w <= maxDim && h <= maxDim) ||\n typeof document === \"undefined\"\n ) {\n return { source: image, width: w, height: h };\n }\n const scale = maxDim / Math.max(w, h);\n const dw = Math.max(1, Math.round(w * scale));\n const dh = Math.max(1, Math.round(h * scale));\n const canvas = document.createElement(\"canvas\");\n canvas.width = dw;\n canvas.height = dh;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return { source: image, width: w, height: h };\n ctx.drawImage(image, 0, 0, dw, dh);\n return { source: canvas, width: dw, height: dh };\n}\n\n// Godot CanvasItem.TextureRepeat: 2 = Enabled, 3 = Mirror -> the texture wraps; 0\n// (ParentNode) / 1 (Disabled) clamp. A shader that scrolls the node TEXTURE past\n// [0,1] via TIME (e.g. the affliction smoke) only animates if the texture wraps;\n// clamped, the scroll freezes at the edge as a motionless, edge-coloured patch.\nexport function nodeTextureRepeats(node: HTMLElement): boolean {\n const value = node.getAttribute(\"data-godot-texture-repeat\");\n return value === \"2\" || value === \"3\";\n}\n\nexport function getImageTexture(\n gl: WebGL2RenderingContext,\n url: string,\n opts: { repeat: boolean; maxDim?: number },\n): TextureEntry {\n return loadUploadedTexture(\n gl,\n `${opts.repeat ? \"repeat\" : \"clamp\"}:${url}`,\n url,\n opts.repeat,\n (image) => downscaleForUpload(image, opts.maxDim),\n );\n}\n\n// Like `getImageTexture` but uploads only the atlas SUB-RECT `region` (page px, top-left origin) — for a\n// WebGL shader on an atlas-region sprite (map-node icon, relic). Without it the runtime binds the whole atlas\n// PAGE, so the shader's implicit `COLOR = texture(TEXTURE, UV)` samples page padding → opaque white (the\n// recolor shaders pass white through). Cropping makes the sprite fill UV [0,1], so `uvFit` /\n// `TEXTURE_PIXEL_SIZE` (both derived from the uploaded texture's dims) come out right with NO shader change.\nexport function getRegionTexture(\n gl: WebGL2RenderingContext,\n url: string,\n region: { x: number; y: number; width: number; height: number },\n opts: { repeat: boolean; maxDim?: number },\n): TextureEntry {\n const key =\n `${opts.repeat ? \"repeat\" : \"clamp\"}:region` +\n `:${Math.round(region.x)},${Math.round(region.y)},${Math.round(region.width)},${Math.round(region.height)}:${url}`;\n return loadUploadedTexture(gl, key, url, opts.repeat, (image) =>\n cropRegionForUpload(image, region, opts.maxDim),\n );\n}\n\n// Shared texture loader: a 1x1 transparent placeholder now, then async-load `url`, transform it via `toUpload`\n// (full-image downscale, or atlas-region crop) and upload UN-flipped: image row 0 lands at texture V=0, i.e.\n// top-left origin — matching the transpiled prelude's Godot-convention `UV`/`GODOT_UV` (UV.y=0 = top) and\n// `getBakedTexture`'s top-left uploads. (A FLIP_Y upload here double-flips against `1.0 - v_uv.y` in\n// transpile.ts and renders base textures upside-down.) Alpha stays un-premultiplied. One code path for every\n// cached texture.\nfunction loadUploadedTexture(\n gl: WebGL2RenderingContext,\n cacheKey: string,\n url: string,\n repeat: boolean,\n toUpload: (image: HTMLImageElement) => {\n source: TexImageSource;\n width: number;\n height: number;\n },\n): TextureEntry {\n const cached = textureCache.get(cacheKey);\n if (cached) return cached;\n const texture = createWebglPlaceholderTexture(gl, repeat);\n const entry: TextureEntry = {\n texture,\n width: 1,\n height: 1,\n loaded: false,\n listeners: new Set(),\n };\n textureCache.set(cacheKey, entry);\n if (!texture) return entry;\n const image = new Image();\n image.crossOrigin = \"anonymous\";\n image.onload = () => {\n // Top-left-origin upload (no FLIP_Y): the shader prelude samples with Godot-convention\n // UVs (UV.y=0 = top), so the image must keep row 0 at V=0.\n const up = toUpload(image);\n uploadWebglTexture(gl, texture, up.source, undefined, undefined, {\n repeat,\n });\n entry.width = up.width;\n entry.height = up.height;\n markTextureLoaded(entry);\n };\n image.src = url;\n return entry;\n}\n\n// Crop the atlas sub-rect `region` (page px, top-left origin) to an offscreen canvas, downscaled to `maxDim`\n// on its longest edge (the region size, not the page). Mirrors `downscaleForUpload`'s canvas-draw; a no-op\n// fallback to the whole image when there's no DOM / 2D context.\nfunction cropRegionForUpload(\n image: HTMLImageElement,\n region: { x: number; y: number; width: number; height: number },\n maxDim: number | undefined,\n): { source: TexImageSource; width: number; height: number } {\n const rw = Math.max(1, Math.round(region.width));\n const rh = Math.max(1, Math.round(region.height));\n const fallback = {\n source: image as TexImageSource,\n width: image.naturalWidth || rw,\n height: image.naturalHeight || rh,\n };\n if (typeof document === \"undefined\") return fallback;\n const scale =\n maxDim && maxDim > 0 ? Math.min(1, maxDim / Math.max(rw, rh)) : 1;\n const dw = Math.max(1, Math.round(rw * scale));\n const dh = Math.max(1, Math.round(rh * scale));\n const canvas = document.createElement(\"canvas\");\n canvas.width = dw;\n canvas.height = dh;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return fallback;\n ctx.drawImage(image, region.x, region.y, rw, rh, 0, 0, dw, dh);\n return { source: canvas, width: dw, height: dh };\n}\n\nfunction markTextureLoaded(entry: TextureEntry): void {\n entry.loaded = true;\n for (const listener of [...entry.listeners]) {\n listener();\n }\n entry.listeners.clear();\n}\n\n/** The subset of a texture entry the load hook needs: whether the real pixels have arrived, and who\n * to tell when they do. Structural rather than `TextureEntry` so an entry from a non-WebGL texture\n * cache — which has no `WebGLTexture` to offer — can be waited on through the same hook. */\nexport interface TextureLoadState {\n loaded: boolean;\n listeners: Set<() => void>;\n}\n\nexport function onTextureLoaded(\n entry: TextureLoadState,\n listener: () => void,\n): () => void {\n if (entry.loaded) {\n listener();\n return () => {};\n }\n entry.listeners.add(listener);\n return () => entry.listeners.delete(listener);\n}\n\nexport function getSolidTexture(\n gl: WebGL2RenderingContext,\n key: string,\n rgba: [number, number, number, number],\n): TextureEntry {\n const cacheKey = `solid:${key}`;\n const cached = textureCache.get(cacheKey);\n if (cached) return cached;\n const texture = createWebglTexture(gl);\n if (texture) uploadWebglTexture(gl, texture, new Uint8Array(rgba), 1, 1);\n const entry: TextureEntry = {\n texture,\n width: 1,\n height: 1,\n loaded: true,\n listeners: new Set(),\n };\n textureCache.set(cacheKey, entry);\n return entry;\n}\n\n// Upload (or refresh) a 2D canvas into a caller-OWNED GL texture — the SCREEN_TEXTURE\n// capture path re-uploads its per-binding screen composite here. Unlike the url-keyed\n// `textureCache` entries these textures are volatile (re-captured, per-binding) and are\n// created/deleted by the caller. Top-left-origin upload (no FLIP_Y), matching the\n// transpiled prelude's Godot-convention UVs (SCREEN_UV.y = 0 is the top).\nexport const uploadCanvasTexture = uploadWebglTexture;\n\n// A procedural sampler texture (NoiseTexture2D/GradientTexture1D) baked from its\n// spec to RGBA bytes and uploaded once. Cached by a stable spec key so identical\n// samplers across nodes (e.g. every enemy's doom bar) share one GL texture.\nexport function getBakedTexture(\n gl: WebGL2RenderingContext,\n key: string,\n bake: () => { width: number; height: number; data: Uint8ClampedArray },\n opts: { repeat: boolean; nearest?: boolean },\n): TextureEntry {\n const cached = textureCache.get(key);\n if (cached) return cached;\n const baked = bake();\n const texture = createWebglTexture(gl);\n // `repeat_enable` -> REPEAT so an unbounded TIME scroll keeps wrapping (CLAMP\n // would freeze the animation at the texture edge); else CLAMP_TO_EDGE.\n // NEAREST for a CONSTANT-interpolation ramp: the bake already produced hard steps, and\n // LINEAR would smear each step boundary back across a texel. LINEAR otherwise.\n if (texture)\n uploadWebglTexture(\n gl,\n texture,\n new Uint8Array(\n baked.data.buffer,\n baked.data.byteOffset,\n baked.data.byteLength,\n ),\n baked.width,\n baked.height,\n opts,\n );\n const entry: TextureEntry = {\n texture,\n width: baked.width,\n height: baked.height,\n loaded: true,\n listeners: new Set(),\n };\n textureCache.set(key, entry);\n return entry;\n}\n\nexport type { TextureBakeSpec };\n// Re-export so callers that bake spec textures don't also need to import bake-texture.\nexport { bakeTexture };\n\n// ---- clock + environment shims (guarded for non-browser / test envs) --------\n\n// Seconds since the shared (monotonic) clock origin. Lazily initialises the origin\n// on first call, so whichever runtime attaches first sets it and both then agree.\nexport function nowSeconds(): number {\n if (clockOrigin === undefined) clockOrigin = performanceNow();\n return (performanceNow() - clockOrigin) / 1000;\n}\n\n/** TEST-ONLY: reset the memoized shared GL + clock so a test can install a stubbed\n * `getContext`/WebGL2 and re-probe deterministically (getShared latches its result). */\nexport function __resetSharedForTest(): void {\n shared = undefined;\n clockOrigin = undefined;\n sharedMaxWidth = Number.POSITIVE_INFINITY;\n sharedMaxHeight = Number.POSITIVE_INFINITY;\n}\n\nexport function performanceNow(): number {\n return typeof performance !== \"undefined\" && performance.now\n ? performance.now()\n : 0;\n}\n\nexport function devicePixelRatio(): number {\n return typeof window !== \"undefined\" && window.devicePixelRatio\n ? window.devicePixelRatio\n : 1;\n}\n\n// The backing-store pixel ratio a runtime should size its canvases at: `devicePixelRatio` scaled by\n// an optional `renderScale` (clamped to (0, 1]). <1 renders the effect at a lower internal resolution\n// (browser upscales the CSS-sized canvas) — the low-end GPU-fill saving knob. Shared by both runtimes\n// so a particle system and a shader on the same device agree on their pixel density.\nexport function effectivePixelRatio(renderScale?: number): number {\n const scale =\n typeof renderScale === \"number\" && renderScale > 0\n ? Math.min(renderScale, 1)\n : 1;\n return devicePixelRatio() * scale;\n}\n\n// Longest-edge ceiling (backing-store px) for a PINNED static backing store — see\n// `staticShaderPixelRatio` / `staticParticlePixelRatio` in `../types`. It bounds only the PINNED\n// path: the live path is sized by `devicePixelRatio × renderScale`, which the device itself bounds,\n// and has never been clamped.\n//\n// 2048 because that is the WebGL2 (GLES 3.0) guaranteed minimum `MAX_TEXTURE_SIZE`, i.e. the largest\n// dimension every conformant context can be relied on to allocate. Two things push back on going\n// higher: the ONE shared drawing buffer is grow-only and shared by every node in both runtimes, so a\n// single oversized pinned node permanently grows it (and if the grow comes back short,\n// `ensureSharedDrawSize` LATCHES a lower ceiling for every other node); and each frozen frame is also\n// snapshotted into the 64-entry static-frame cache, whose bitmaps scale with the pinned size. A pin\n// is a consumer's guess at \"what fullscreen would be on this device\" — this is the guard rail that\n// keeps a wrong guess (a windowed 4K desktop asking for 4K-class frames) from becoming an allocation\n// problem, at the cost of a softer frame on displays that really are that large.\nexport const MAX_PINNED_BACKING_DIM = 2048;\n\n/** Backing-store size (px) for a `cssW`×`cssH` surface at `ratio`, plus the ratio ACTUALLY applied —\n * which differs from `ratio` only when `maxDim` bit, and is what a caller that draws geometry in\n * CSS px × ratio (the particle runtime) must scale by so its sprites still land inside the canvas.\n *\n * With `maxDim` the result is scaled down ASPECT-PRESERVINGLY (the `downscaleForUpload` idiom) so\n * its longest edge fits. Per-axis clamping is not an option: the shader runtime's `uvFit` derives\n * contain/cover from the canvas aspect, so a squashed backing store would re-fit the texture\n * wrongly. Without `maxDim` (or under it) this is exactly the `Math.max(1, Math.round(css * ratio))`\n * both runtimes have always done, and `ratio` comes back untouched. */\nexport function backingStoreSize(\n cssW: number,\n cssH: number,\n ratio: number,\n maxDim?: number,\n): { w: number; h: number; ratio: number } {\n const w = Math.max(1, Math.round(cssW * ratio));\n const h = Math.max(1, Math.round(cssH * ratio));\n const longest = Math.max(w, h);\n if (!maxDim || maxDim <= 0 || longest <= maxDim) return { w, h, ratio };\n const scale = maxDim / longest;\n return {\n w: Math.max(1, Math.round(w * scale)),\n h: Math.max(1, Math.round(h * scale)),\n ratio: ratio * scale,\n };\n}\n\n/** The self-layer attribute that carries a surface's PER-BINDING backing-density multiplier — the\n * factor by which the device pixels this surface really covers exceed its own CSS box.\n *\n * Both fx runtimes size a canvas from `clientWidth × (devicePixelRatio × renderScale)`, and\n * `clientWidth` is blind to ancestor CSS transforms: a node under a `transform: scale(1.41)`\n * ancestor lays out at its untransformed width, so its backing store is sized for 1/1.41 of the\n * device pixels it is magnified onto and the surface is visibly soft. Nothing the runtime can read\n * off its own element tells it that — the transform belongs to an ancestor the runtime does not\n * own, and finding it would be a `getBoundingClientRect()` walk per surface per frame, i.e. exactly\n * the forced layout both runtimes are built around avoiding.\n *\n * So the HOST states it. It already composed that transform to write it; this attribute is that\n * number, handed down. It is a MULTIPLIER on the density term, not a replacement for it: the\n * device ratio, `renderScale` and any frozen-mode pin all still apply, and this rides on top.\n *\n * IT IS AN AXIS SCALE, NOT A BOUNDING-BOX RATIO, and a host that confuses the two will over-allocate\n * every rotated surface it owns. `getBoundingClientRect()` returns the axis-aligned bounding box of a\n * transformed element, so a square rotated θ measures `|cos θ| + |sin θ|` wider than it is — √2 at\n * 45° — while covering exactly as many device pixels as before. Rotation is rigid. The number this\n * attribute wants is the transform's column norm (or the mean of the two under a non-uniform scale).\n *\n * Named `data-godot-shader-*` and read by the PARTICLE runtime as well, deliberately — one name\n * for one question (\"how magnified is this surface?\") that both fx families ask identically, so a\n * host stamps it the same way whichever runtime picks the node up. */\nexport const SURFACE_PIXEL_RATIO_ATTR = \"data-godot-shader-pixel-ratio\";\n\n/** Ceiling on the attribute above. The multiplier is the one density input that arrives as a STRING\n * from outside the runtime — `renderScale` is clamped to ≤ 1 and the static pin is bounded by\n * `MAX_PINNED_BACKING_DIM` — so a host mid-animation, or a host with a bug, could otherwise turn\n * one attribute write into a quadratic backing-store allocation on a surface the runtime has no\n * other reason to distrust. 4 is 16× the area, past any magnification a UI plausibly applies to a\n * live effect, and a surface asking for more is far likelier to be wrong than under-resolved. */\nexport const MAX_SURFACE_PIXEL_RATIO = 4;\n\n/** Parse `SURFACE_PIXEL_RATIO_ATTR` into a density MULTIPLIER.\n *\n * Absent, empty, unparseable, non-finite or non-positive ⇒ exactly `1`, i.e. the density term is\n * the product it has always been and the surface is sized byte-for-byte as it was before this\n * attribute existed. That is the whole off-switch: a host that never writes the attribute cannot\n * tell this feature is here.\n *\n * Values BELOW 1 are honoured (a surface minified by an ancestor really does cover fewer device\n * pixels than its box). Above `MAX_SURFACE_PIXEL_RATIO` they are capped, not rejected — a too-large\n * value is still evidence the surface is magnified, so clamping keeps most of the fix while\n * refusing the allocation. */\nexport function parseSurfacePixelRatio(\n attr: string | null | undefined,\n): number {\n if (attr === null || attr === undefined) return 1;\n const value = Number.parseFloat(attr);\n if (!Number.isFinite(value) || value <= 0) return 1;\n return Math.min(value, MAX_SURFACE_PIXEL_RATIO);\n}\n\n/** Normalize a pinned static backing ratio (`staticShaderPixelRatio`/`staticParticlePixelRatio`):\n * a finite ratio > 0, else `undefined` = NOT pinned (the runtime keeps sizing every binding at\n * `devicePixelRatio × renderScale`, exactly as before the option existed). Unlike `renderScale`\n * it is NOT clamped to ≤ 1 — the whole point is to allow a backing store denser than the current\n * fit — only bounded later by `MAX_PINNED_BACKING_DIM` on the resulting size. */\nexport function normalizeStaticPixelRatio(\n value: number | undefined,\n): number | undefined {\n return typeof value === \"number\" && Number.isFinite(value) && value > 0\n ? value\n : undefined;\n}\n\n/** TEST-ONLY: reset the memoized GPU probe so a test can re-run `describeGpu` with a fresh stub. */\nexport function __resetGpuInfoForTest(): void {\n gpuInfo = undefined;\n}\n","// The WebGPU device gate: ONE page-wide `GPUDevice` for every WebGPU surface both runtimes\n// create, acquired at most once. Ported from the S7 probe\n// (`packages/perf-harness/src/scenarios/webgpu/renderer.ts`, `acquireGpu`) with its FAIL-LOUD\n// contract inverted: a probe that cannot measure must abort the run, but a PRODUCT that cannot\n// render on WebGPU must fall back to WebGL and say nothing to the user. So nothing here ever\n// throws; every failure resolves `null` and LATCHES a `WebgpuFallbackReason` the runtimes report\n// as a stat, which is the only way a silent fallback stays diagnosable.\n//\n// Why memoized rather than per-runtime: the same reason `shared-gl.ts` holds ONE WebGL2 context.\n// Two devices would double every driver-side allocation, and a texture cache can only be shared\n// by surfaces that share a device.\n\n/** Why the WebGPU path declined, latched at its FIRST occurrence (later ones cannot un-explain it). */\nimport { configureWebgpuSurface } from \"@godot-scene-web/canvas-effects/webgpu\";\n\nexport type WebgpuFallbackReason =\n | \"no-navigator-gpu\"\n | \"no-adapter\"\n | \"fallback-adapter\"\n | \"acquire-timeout\"\n | \"device-lost\"\n | \"context-refused\"\n | \"pipeline-error\";\n\nexport interface WebgpuShared {\n device: GPUDevice;\n format: GPUTextureFormat;\n /** `device.limits`, hoisted: sizing law (`maxTextureDimension2D`) and the uniform ring read it. */\n limits: GPUSupportedLimits;\n counters: {\n /** `uncapturederror` events. Non-zero means a frame was silently wrong. */\n gpuErrors: number;\n /** Resolutions of `device.lost`. A lost device stops producing frames; the surfaces are dead. */\n deviceLosses: number;\n };\n}\n\n/** How long acquisition waits for an adapter/device before declaring WebGPU unavailable. A hung\n * `requestAdapter` is a driver/flag problem on the host that no amount of waiting fixes, and the\n * runtimes have a working WebGL path to adopt instead. */\nconst ACQUIRE_TIMEOUT_MS = 8000;\n\n/**\n * WebGPU's usage / visibility / map BIT FLAGS, spelled out from the specification.\n *\n * TypeScript 6's `lib.dom.d.ts` ships every WebGPU *interface* but NOT the flag namespace OBJECTS\n * (`GPUBufferUsage`, `GPUShaderStage`, `GPUTextureUsage`, `GPUMapMode`) — only their\n * `…Flags = number` aliases. Reading the real globals instead is not an option: these modules are\n * import-reachable from the node CLIs that import `@godot-scene-web/html`, node has no such\n * globals, and a module-scope `GPUBufferUsage.VERTEX` would throw AT IMPORT and take the whole\n * entry point down — on a machine that was never going to render anything anyway. The values are\n * normative constants in the WebGPU specification, so stating them is safe in a way that copying\n * an implementation detail would not be.\n */\nexport const BUFFER_USAGE = {\n MAP_READ: 0x0001,\n COPY_SRC: 0x0004,\n COPY_DST: 0x0008,\n VERTEX: 0x0020,\n UNIFORM: 0x0040,\n} as const;\nexport const SHADER_STAGE = { VERTEX: 0x1, FRAGMENT: 0x2 } as const;\nexport const TEXTURE_USAGE = {\n COPY_SRC: 0x01,\n COPY_DST: 0x02,\n TEXTURE_BINDING: 0x04,\n RENDER_ATTACHMENT: 0x10,\n} as const;\nexport const MAP_MODE = { READ: 0x1 } as const;\n\n// Module-scope acquisition state, persisted across attach/detach cycles like `shared-gl`'s.\n// `memo` is the page-wide promise; `settled` is its resolved value for the SYNC peek the runtimes\n// use to skip the async gate entirely when the answer is already known.\nlet memo: Promise<WebgpuShared | null> | undefined;\nlet settled: WebgpuShared | null | undefined;\nlet latchedReason: WebgpuFallbackReason | null = null;\nconst lostListeners = new Set<() => void>();\n// Bumped by `__resetWebgpuForTest`, so an in-flight acquisition or a `device.lost` handler from a\n// PREVIOUS test cannot poison the memo a later test just built.\nlet epoch = 0;\n\n/** Latch the first fallback reason. Exported for `pipeline.ts` (a shader/pipeline that fails\n * validation is a WebGPU failure the runtime reports through the same stat), not for consumers. */\nexport function latchWebgpuFallbackReason(reason: WebgpuFallbackReason): void {\n if (latchedReason === null) latchedReason = reason;\n}\n\n/** The FIRST reason WebGPU was declined, or null while nothing has gone wrong. */\nexport function webgpuFallbackReason(): WebgpuFallbackReason | null {\n return latchedReason;\n}\n\n/**\n * The page-wide device promise. Never rejects: `null` means \"render on WebGL\", with\n * `webgpuFallbackReason()` saying why.\n *\n * Memoized on the FIRST call, so N bindings created in one reconcile share one `requestAdapter`.\n * Once a device is lost the memo is POISONED (replaced with a resolved `null`) rather than\n * cleared: a device that died once will usually die again, and re-probing per binding would turn\n * a rare failure into a stall on every rebuild.\n */\nexport function acquireWebgpuDevice(): Promise<WebgpuShared | null> {\n if (memo) return memo;\n memo = acquireOnce();\n return memo;\n}\n\n/**\n * The SYNC view of the gate, for factories that must decide without awaiting:\n * `undefined` = never tried or still pending, `null` = tried and unavailable (or poisoned by a\n * device loss), otherwise the shared device.\n */\nexport function peekWebgpuDevice(): WebgpuShared | null | undefined {\n return settled;\n}\n\n/** Subscribe to device loss (the runtimes rebuild every binding onto WebGL). Returns unsubscribe. */\nexport function onWebgpuDeviceLost(callback: () => void): () => void {\n lostListeners.add(callback);\n return () => {\n lostListeners.delete(callback);\n };\n}\n\nasync function acquireOnce(): Promise<WebgpuShared | null> {\n const started = epoch;\n const shared = await tryAcquire();\n // A reset (or a loss) landed while we were awaiting: this result belongs to a page that no\n // longer exists, so it must not overwrite the current `settled`.\n if (started !== epoch) return null;\n settled = shared;\n return shared;\n}\n\nasync function tryAcquire(): Promise<WebgpuShared | null> {\n const gpu = (globalThis.navigator as (Navigator & { gpu?: GPU }) | undefined)\n ?.gpu;\n if (!gpu) return decline(\"no-navigator-gpu\");\n\n let adapter: GPUAdapter | null;\n try {\n const raced = await withDeadline(gpu.requestAdapter());\n if (raced === TIMED_OUT) return decline(\"acquire-timeout\");\n adapter = raced;\n } catch {\n return decline(\"no-adapter\");\n }\n if (!adapter) return decline(\"no-adapter\");\n\n // A fallback adapter is a CPU implementation wearing the API's name — the WebGPU twin of\n // `shared-gl`'s SwiftShader/llvmpipe decline: running our fragment work on it pegs the CPU for\n // no visual gain over the WebGL path, which is also the Godot-parity reference. Escape hatch:\n // set `globalThis.__gswForceWebgpuEffects = true` to render on it anyway (exercising the WebGPU\n // path on a headless/CI box is exactly what SwiftShader is for), mirroring\n // `__gswForceWebglShaders` in `../webgl/shared-gl.ts`.\n if (isFallbackAdapter(adapter) && !forcedOn())\n return decline(\"fallback-adapter\");\n\n let device: GPUDevice;\n try {\n const raced = await withDeadline(adapter.requestDevice());\n if (raced === TIMED_OUT) return decline(\"acquire-timeout\");\n device = raced;\n } catch {\n // `requestDevice` resolves or rejects — it never yields null — so a rejection is \"we asked for\n // a device and hold none\", which is the lost-device state at t=0 and reported as such.\n return decline(\"device-lost\");\n }\n\n const counters = { gpuErrors: 0, deviceLosses: 0 };\n const shared: WebgpuShared = {\n device,\n format: preferredFormat(gpu),\n limits: device.limits,\n counters,\n };\n wireDeviceEvents(device, counters);\n return shared;\n}\n\nfunction decline(reason: WebgpuFallbackReason): null {\n latchWebgpuFallbackReason(reason);\n return null;\n}\n\n// A lost device does not throw anywhere — it just stops working, so the loss has to be OBSERVED to\n// be survivable. `uncapturederror` is the same shape of silence: the frame is wrong and nothing\n// says so. Both are wrapped because a partial implementation missing either member must degrade to\n// \"no telemetry\", never to a throw out of acquisition.\nfunction wireDeviceEvents(\n device: GPUDevice,\n counters: WebgpuShared[\"counters\"],\n): void {\n const started = epoch;\n try {\n void device.lost.then(() => {\n counters.deviceLosses += 1;\n if (started !== epoch) return;\n latchWebgpuFallbackReason(\"device-lost\");\n // POISON: later acquires resolve null immediately, without touching the adapter again.\n memo = Promise.resolve(null);\n settled = null;\n for (const listener of [...lostListeners]) listener();\n });\n device.addEventListener(\"uncapturederror\", () => {\n counters.gpuErrors += 1;\n });\n } catch {\n // No device telemetry available; the counters simply stay at 0.\n }\n}\n\nfunction isFallbackAdapter(adapter: GPUAdapter): boolean {\n // `info` is a live accessor on modern Chrome; the optional chain is for a build where it is not.\n const info = (\n adapter as GPUAdapter & { info?: { isFallbackAdapter?: boolean } }\n ).info;\n return Boolean(info?.isFallbackAdapter);\n}\n\nfunction forcedOn(): boolean {\n return (\n (globalThis as Record<string, unknown>).__gswForceWebgpuEffects === true\n );\n}\n\nfunction preferredFormat(gpu: GPU): GPUTextureFormat {\n try {\n return gpu.getPreferredCanvasFormat();\n } catch {\n // Every implementation prefers one of bgra8unorm/rgba8unorm; bgra8unorm is the desktop default\n // and is only ever reached when the accessor itself is missing (a stub, or a partial build).\n return \"bgra8unorm\";\n }\n}\n\nconst TIMED_OUT: unique symbol = Symbol(\"gsw-webgpu-acquire-timeout\");\n\n// `Promise.race` with a cleared timer: the probe's `withDeadline`, resolving a sentinel instead of\n// rejecting. Clearing in `finally` is load-bearing — a live 8 s timer per acquisition would keep\n// the event loop (and, in node, the process) awake long after the answer arrived.\nasync function withDeadline<T>(\n promise: Promise<T>,\n): Promise<T | typeof TIMED_OUT> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const deadline = new Promise<typeof TIMED_OUT>((resolve) => {\n timer = setTimeout(() => resolve(TIMED_OUT), ACQUIRE_TIMEOUT_MS);\n });\n try {\n return await Promise.race([promise, deadline]);\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n }\n}\n\n/**\n * `canvas.getContext(\"webgpu\")` — THE ONE CAST.\n *\n * `lib.dom.d.ts` ships every WebGPU interface but gives `getContext` no `\"webgpu\"` overload, so the\n * call lands on the `(contextId: string) => RenderingContext | null` signature. Narrowing it here,\n * once, is preferable to sprinkling casts at the call sites or adding a `@webgpu/types` dependency\n * the type system does not need.\n *\n * Null (never a throw) when the canvas already holds a context of another TYPE — a canvas gets one\n * context for its whole life, so this is a permanent property of that element, not a retryable error.\n */\nexport function webgpuContext(\n canvas: HTMLCanvasElement,\n): GPUCanvasContext | null {\n try {\n const context = canvas.getContext(\"webgpu\") as GPUCanvasContext | null;\n if (!context) {\n latchWebgpuFallbackReason(\"context-refused\");\n return null;\n }\n return context;\n } catch {\n latchWebgpuFallbackReason(\"context-refused\");\n return null;\n }\n}\n\n/** Configure one canvas to present premultiplied frames from the shared device, or null on refusal.\n *\n * `alphaMode` has only two values and \"opaque\" would flatten the effect onto black and stop it\n * compositing over the page at all — so \"premultiplied\" is the only usable one, and every fragment\n * this package writes must therefore return `vec4f(rgb * a, a)` under a `one / one-minus-src-alpha`\n * blend. That pairing is load-bearing in both directions and neither half raises an error on its\n * own: a premultiplied fragment under src-alpha blend double-multiplies, a straight one under this\n * blend halos. */\nexport function configureCanvas(\n canvas: HTMLCanvasElement,\n shared: WebgpuShared,\n): GPUCanvasContext | null {\n const context = webgpuContext(canvas);\n if (!context) return null;\n try {\n configureWebgpuSurface(context, shared.device, shared.format);\n return context;\n } catch {\n latchWebgpuFallbackReason(\"context-refused\");\n return null;\n }\n}\n\n/** TEST-ONLY: clear the memo, the latched reason and the loss subscribers so a test can install a\n * stubbed `navigator.gpu` and re-probe deterministically (acquisition latches its result). */\nexport function __resetWebgpuForTest(): void {\n epoch += 1;\n memo = undefined;\n settled = undefined;\n latchedReason = null;\n lostListeners.clear();\n}\n","// Turning captured WebGPU pixels into something an ENCODER can read.\n//\n// WHY THIS FILE EXISTS. The frozen-surface image swap (`../surface-image-swap`) stands an `<img>` in\n// for a canvas that has stopped changing, and it makes that image with `canvas.toBlob()`. On a\n// WebGPU binding there is no canvas to call that on: reading a WebGPU canvas back — `drawImage`,\n// `toDataURL`, `toBlob` — goes through the PRESENTATION path, which is blank under SwiftShader,\n// returns nothing at all in headless Chrome (it never composites a WebGPU canvas), and is\n// pathologically slow on Android (S7 measured the blit-shaped arm at 23 Hz against 87). The\n// sanctioned path is `../webgpu/readback`: re-render the frame into an offscreen `rgba8unorm`\n// texture and `copyTextureToBuffer` it back. What comes out of that is a tightly packed, top-down,\n// PREMULTIPLIED RGBA byte array — and `putImageData` wants STRAIGHT alpha. These two functions are\n// that seam, and nothing else.\n//\n// PREMULTIPLIED → STRAIGHT, AND WHY IT IS SOUND HERE. `packages/test-harness/src/webgpu-parity/\n// pixels.ts` argues the opposite direction — that a parity comparison must move to PREMULTIPLIED\n// space, because un-premultiplying divides by alpha and \"the colour of an alpha-0 pixel does not\n// exist\", so whatever bytes two renderers happen to leave there would read as a difference. That\n// argument is about COMPARISON, where an arbitrary value is a false failure. It does not block this\n// ENCODE path, for two reasons:\n//\n// - the ambiguity is resolved by CONVENTION, not by guessing: alpha 0 → (0,0,0), the canonical\n// invisible pixel, which is exactly what the PNG encoder would have to store anyway and exactly\n// what a browser's own `putImageData`/`getImageData` round trip produces;\n// - the trip is a ROUND TRIP, and it ends where it started. These bytes are unpremultiplied,\n// drawn into a 2D canvas, encoded to PNG, decoded by the `<img>`, and RE-premultiplied by the\n// compositor before anything is shown. Composed, the error is ≤ ~1 byte per channel in\n// premultiplied space (one rounding at each end), which is why the desktop swap-parity test\n// compares in premultiplied space at a per-channel tolerance of ~2 rather than asserting\n// equality on the straight bytes.\n//\n// The clamp below is not defensive decoration. A readback can legitimately carry c > a: the GPU\n// blends and rounds in float and stores 8-bit, so a fully saturated channel at low alpha can land\n// one ULP above its own alpha. `c * 255 / a` then exceeds 255, and an unclamped write into a\n// `Uint8ClampedArray` would be saved by the array's own clamp while a `Uint8Array` would WRAP — so\n// the clamp is stated here, once, rather than left to whichever buffer type a caller passes.\n//\n// THE BLANK GUARD, and the two ways this conversion can produce a picture of nothing.\n//\n// A frozen WebGPU surface publishes whatever comes out of here, and the canvas underneath it is\n// HIDDEN by then — so an invisible result is not a degraded image, it is a surface that has\n// disappeared with every counter in `../surface-image-swap` reporting success. That happened for\n// real on one launch mode of this box (headed Chromium under Xvfb on the default ANGLE backend,\n// NVIDIA RTX 2060; docs/perf-harness.md, S8), where all 12 WebGPU surfaces of both effect arms\n// swapped and the screenshot showed none of them. Two things can do it, and only one of them was:\n//\n// 1. THE READBACK CAME BACK EMPTY. The premise of everything below is that `pixels` are the frame.\n// A device whose `copyTextureToBuffer` hands back zeros breaks it silently. Caught by a scan\n// FOLDED INTO the unpremultiply loop, which is already walking every byte: a comparison per\n// pixel, once per frozen surface. NOT what that rung does — measured 2026-08-21, the readback\n// there is fine (827 painted px of 15,376 on the `textured` fixture).\n// 2. THE 2D CANVAS DID NOT TAKE THE PIXELS — what actually happens there, and the reason this\n// second check exists at all. `putImageData` into an ACCELERATED 2D canvas does not land:\n// reading any pixel of it back answers alpha 0, and the `toBlob` that follows encodes the\n// nothing that is really in the canvas (in the more broken of the two launchers measured, a\n// PNG decoded OUTSIDE the browser — the in-page read cannot be trusted to measure itself —\n// holds 0 of 16,384 painted px). The same page's `willReadFrequently: true` canvas, which\n// Chrome keeps on the CPU, takes the identical write and answers 255. Caught by reading ONE\n// pixel back — the most opaque one, whose alpha this function knows — because a canvas that\n// answers 0 where we just wrote 255 is answering about a write it lost.\n// WHY THIS IS A FAIR TEST OF THE ENCODE, and why it does not generalise to a canvas painted by\n// someone else: it exercises the same CPU→canvas→CPU trip the encode depends on, on the same\n// canvas, with bytes this function chose. On the SAME rung, a canvas the host painted with\n// `drawImage` still encodes correctly through `toBlob` — so the shipped WebGL swap, which reads\n// such a canvas directly, is unaffected there and must not be judged by this check (see\n// `../surface-image-swap`'s WHY THE DIRECT PATH IS NOT GUARDED).\n//\n// NEITHER IS DECIDED FROM THE PIXELS ALONE. An invisible frame is also what a surface with nothing\n// to draw legitimately produces, and refusing that one would hold a live canvas in the composite\n// forever. Both verdicts are therefore reached only when the CALLER passes `expectCoverage` — its\n// assertion that the draw it just encoded must have put pixels somewhere — and both answer\n// `STATIC_CAPTURE_BLANK`, which the swap treats as a capture failure (`../surface-image-swap`'s\n// BLANK CAPTURES).\n//\n// FAIL OPEN, ALWAYS. The write-back check refuses ONLY on a positive reading of alpha 0 at a pixel\n// it knows it wrote. An environment that cannot answer at all — no `getImageData` (jsdom), a context\n// that throws — is never a refusal: this guard exists to stop a blank publish, not to become a new\n// way for a surface to fail to freeze.\n\nimport {\n STATIC_CAPTURE_BLANK,\n type StaticSurfaceCapture,\n} from \"../surface-image-swap\";\n\n/** What `unpremultiplyRgba` reports about the bytes it walked, for a caller that must tell an empty\n * frame from an empty CAPTURE. Mutated in place — an out-param rather than a returned pair — so the\n * scan rides along the existing loop instead of allocating or walking a second time. Build one\n * zeroed (`{ anyAlpha: false, witness: -1 }`); this function only ever writes it. */\nexport interface RgbaAlphaScan {\n /** At least one pixel carried a non-zero alpha byte. `false` after a whole buffer ⇒ the frame is\n * entirely invisible: every pixel composites to nothing, whatever its colour channels say. */\n anyAlpha: boolean;\n /** Index — in PIXELS, not bytes — of the MOST OPAQUE pixel seen, or -1 for none. The witness the\n * write-back check reads: the one pixel whose alpha this loop can state from the source bytes, so\n * a canvas answering 0 there is answering about a write that was definitely made. Most opaque\n * rather than first, because alpha survives a canvas round trip exactly at 255 and only that\n * reading needs no argument about precision. */\n witness: number;\n}\n\n/**\n * Premultiplied RGBA → straight (\"unassociated\") RGBA, in a NEW buffer (the input is untouched — a\n * capture buffer may be handed to more than one consumer).\n *\n * Alpha 0 → `(0, 0, 0, 0)`: the canonical invisible pixel (see the module doc on why this convention\n * is sound for an encode path and not for a comparison one). Otherwise each channel is\n * `round(c * 255 / a)`, CLAMPED to 255 — a readback can carry `c > a` from GPU rounding.\n *\n * `scan`, when given, collects the blank guard's two facts on the walk this loop was making anyway:\n * `anyAlpha` the moment a pixel with a non-zero alpha is seen, and `witness` at the most opaque\n * pixel of the buffer (see the module doc).\n */\nexport function unpremultiplyRgba(\n premultiplied: Uint8Array,\n scan?: RgbaAlphaScan,\n): Uint8Array<ArrayBuffer> {\n // The buffer type is stated (rather than left as the default `ArrayBufferLike`) so the caller\n // below can build a `Uint8ClampedArray` VIEW over these same bytes instead of copying them: a\n // capture is a whole backing store, and `ImageData` only accepts the clamped view.\n const out = new Uint8Array(new ArrayBuffer(premultiplied.length));\n let witnessAlpha = 0;\n for (let index = 0; index + 3 < premultiplied.length; index += 4) {\n const alpha = premultiplied[index + 3];\n out[index + 3] = alpha;\n if (alpha === 0) {\n // Already zero from the allocation; stated so the invariant is readable rather than implied.\n out[index] = 0;\n out[index + 1] = 0;\n out[index + 2] = 0;\n continue;\n }\n // Past the alpha-0 branch ⇒ this pixel composites to something. Two stores on a branch the\n // frame's own opacity bounds: `anyAlpha` settles once, and `witness` only climbs.\n if (scan) {\n scan.anyAlpha = true;\n if (alpha > witnessAlpha) {\n witnessAlpha = alpha;\n scan.witness = index >> 2;\n }\n }\n out[index] = Math.min(\n 255,\n Math.round((premultiplied[index] * 255) / alpha),\n );\n out[index + 1] = Math.min(\n 255,\n Math.round((premultiplied[index + 1] * 255) / alpha),\n );\n out[index + 2] = Math.min(\n 255,\n Math.round((premultiplied[index + 2] * 255) / alpha),\n );\n }\n return out;\n}\n\n/**\n * Captured premultiplied RGBA → a 2D canvas holding those pixels, ready for `toBlob`. Null when the\n * conversion cannot be done at all: no `document` (SSR), a byte count that does not match\n * `width * height * 4`, a degenerate size, or an environment with no 2D context and no `ImageData`\n * (jsdom is both). A null here is an ordinary capture FAILURE upstream — the surface stays on its\n * canvas — never a throw.\n *\n * `expectCoverage` is the CALLER's assertion that the draw behind these bytes must have painted\n * something — the particle runtime's packed instance count, the shader runtime's full-viewport quad.\n * Under it this function refuses, as `STATIC_CAPTURE_BLANK` rather than encoding, BOTH ways a blank\n * still is produced (see the module doc's BLANK GUARD, and `../surface-image-swap`'s BLANK CAPTURES\n * for what the swap does with the verdict): a readback in which every pixel's alpha is 0, and a\n * canvas that reads back alpha 0 at the pixel this call just wrote its most opaque one to.\n * WITHOUT it — the default — an all-transparent readback is an ordinary frame and is encoded as one,\n * because a surface with nothing to draw is legitimately invisible and must still be allowed to\n * freeze.\n *\n * A FRESH canvas per call, deliberately — unlike the swap module's `maxDim` scratch, which is reused\n * because `toBlob` snapshots its source synchronously. Captures are ASYNCHRONOUS and can be in\n * flight concurrently (one per frozen surface), so a shared canvas would let the second capture\n * overwrite the first's pixels before its `toBlob` ever ran. The caller releases the canvas\n * (`width = 0; height = 0`) once its encode has been kicked.\n */\nexport function canvasFromPremultipliedRgba(\n pixels: Uint8Array,\n width: number,\n height: number,\n expectCoverage = false,\n): StaticSurfaceCapture {\n if (typeof document === \"undefined\" || typeof ImageData === \"undefined\") {\n return null;\n }\n const w = Math.floor(width);\n const h = Math.floor(height);\n if (w < 1 || h < 1) return null;\n if (pixels.length !== w * h * 4) return null;\n const canvas = document.createElement(\"canvas\");\n canvas.width = w;\n canvas.height = h;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return null;\n const scan: RgbaAlphaScan = { anyAlpha: false, witness: -1 };\n const straight = unpremultiplyRgba(pixels, scan);\n /** Give the canvas back its pixels now rather than at the next GC, and report the verdict. */\n const refuse = (): StaticSurfaceCapture => {\n canvas.width = 0;\n canvas.height = 0;\n return STATIC_CAPTURE_BLANK;\n };\n // (1) Drawn, and read back invisible.\n if (expectCoverage && !scan.anyAlpha) return refuse();\n // `ImageData` needs a `Uint8ClampedArray` over the SAME bytes; `straight` is freshly allocated and\n // never referenced again, so the view is safe to hand over without another copy.\n ctx.putImageData(\n new ImageData(\n new Uint8ClampedArray(\n straight.buffer,\n straight.byteOffset,\n straight.length,\n ),\n w,\n h,\n ),\n 0,\n 0,\n );\n // (2) Written, and the canvas kept nothing. One pixel, read back where the most opaque one was\n // just written — and only under the caller's assertion, so a legitimately invisible frame (which\n // has no witness to read) is never asked the question.\n if (expectCoverage && !writeLanded(ctx, scan.witness, w)) return refuse();\n return canvas;\n}\n\n/**\n * Did the 2D canvas actually take the pixels? Reads back the ONE pixel `scan.witness` names, whose\n * alpha the conversion above knows it just wrote as the frame's most opaque.\n *\n * FAILS OPEN on everything that is not a positive \"alpha 0 where a solid pixel was written\": no\n * witness, no `getImageData` (jsdom), a context that throws, an answer that is not a pixel. This\n * check exists to stop a blank publish, and must never become a new reason a healthy surface cannot\n * freeze.\n *\n * COSTS one 1×1 readback per frozen surface. On an accelerated canvas that forces the pixels to be\n * flushed — which the `toBlob` a moment later forces anyway, so what moves is WHEN, not whether; and\n * it moves into the capture's own wall time (`staticImageCaptureMs`), never into the encode park\n * that `staticImageEncodeMs` means.\n */\nfunction writeLanded(\n ctx: CanvasRenderingContext2D,\n witness: number,\n width: number,\n): boolean {\n if (witness < 0) return true;\n try {\n const probe = ctx.getImageData(\n witness % width,\n Math.floor(witness / width),\n 1,\n 1,\n );\n const alpha = probe?.data?.[3];\n return typeof alpha === \"number\" ? alpha !== 0 : true;\n } catch {\n return true;\n }\n}\n","// HOW BIG THE OVERLAY CANVAS HAS TO BE — the whole sizing law for a particle system, in one pure\n// module (no DOM, no GL, no clock), so `./runtime` can size a canvas without re-deriving it and a\n// unit test can pin it.\n//\n// A particle system's node box says almost nothing about where its pixels land: a `GPUParticles2D`\n// is a POINT (a zero-size box), and the spray happens entirely outside it. So the canvas is the box\n// grown by a MARGIN on each side, and the margin is this module's subject.\n//\n// THE MARGIN USED TO BE ONE SYMMETRIC NUMBER: `spriteExtentPad + emissionExtentPad`, i.e. how big\n// one sprite is plus how far apart they are BORN. That is the whole of it — it models no movement\n// at all, so every system got a square canvas centred on its node origin, and anything that\n// TRAVELS was cropped at the square's edge. The visible symptom this was written for: a chest's\n// gold-coin burst (a ~700px square canvas over particles that fly ~1200px sideways and fall ~2500px)\n// looked as if a rectangle had been cut out of the screen.\n//\n// SO THE MARGIN IS NOW FOUR NUMBERS, one per side, and it includes a BALLISTIC TRAVEL term derived\n// from the same fields `./simulate` integrates: initial velocity over the direction/spread arc,\n// gravity, linear/radial/tangential acceleration, damping, orbit, and the lifetime the whole thing\n// runs for. Two rules keep it honest:\n//\n// * CONSERVATIVE, NOT EXACT. Every term is an upper bound of the real integral — the arc maxima\n// are per-axis, the position-dependent forces (radial/tangential) are treated as isotropic, and\n// an orbit is treated as \"any reach on one axis can appear on any other\" (it rotates the whole\n// position vector about the origin, so it genuinely can). Being generous costs canvas pixels;\n// being tight costs a visible crop, which is the bug.\n// * IT CAN ONLY GROW. The final margin is floored at the symmetric pad it replaces, so no system\n// ever gets a SMALLER canvas than it had before this existed, whatever the travel math or the\n// visible-rect clamp say. That is what makes the change safe to default on: the failure mode of\n// a wrong number here is \"wasted pixels\", never \"cropped pixels\".\n//\n// AND THE PIXELS ARE CAPPED BY WHAT CAN BE SEEN. Travel bounds alone are unbounded in principle (a\n// 2.5s fall under gravity 800 is 2500px), so a host that knows which part of the element's own local\n// space is actually on screen passes it in (`ParticleLocalRect` → `visibleAllowance`) and the margin\n// is clamped to that. Without one the margin is capped at `PAD_CAP` per side, exactly as before.\n\nimport type { ParticleTextureHandle } from \"./render-backend\";\nimport type { ParticleSpecConfig } from \"./spec\";\n\n// Sprite size (px) for an untextured system (soft round dot), pre-scale.\nconst DEFAULT_DOT = 16;\n\n/** Upper bound on the per-side canvas margin, so a pathological scale/texture/travel can't allocate\n * an enormous canvas. Applies to every side independently, so the worst-case canvas is the node box\n * plus `2 * PAD_CAP` on each axis — the same worst case the symmetric pad always had. */\nexport const PAD_CAP = 1024;\n\nconst DEG2RAD = Math.PI / 180;\nconst TAU = Math.PI * 2;\n\n/** Per-side canvas margin in CSS px, measured OUTWARD from the node box's corresponding edge. All\n * four are >= 0. `left`/`top` are also the draw's origin offset (see `./runtime`'s `packBinding`). */\nexport interface ParticleExtents {\n left: number;\n right: number;\n top: number;\n bottom: number;\n}\n\n/** A rect in the element's OWN local CSS px space — the space the node box lives in, so `{x: 0, y: 0}`\n * is the box's top-left corner and the axes are the node's local axes (pre-transform: an ancestor\n * scale/rotation does not enter here, because the canvas is drawn inside that transform). */\nexport interface ParticleLocalRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport const ZERO_EXTENTS: ParticleExtents = {\n left: 0,\n right: 0,\n top: 0,\n bottom: 0,\n};\n\n// A `flipbookCropOnly` sheet keeps the FULL texture size: its grid comes from a SHADER that\n// only crops UV, so Godot draws the quad undivided and magnifies one cell over it (config.ts).\nexport function frameSize(\n cfg: ParticleSpecConfig,\n texture: ParticleTextureHandle | null,\n): { frameW: number; frameH: number } {\n const texW =\n cfg.textureWidth > 0 ? cfg.textureWidth : (texture?.width ?? DEFAULT_DOT);\n const texH =\n cfg.textureHeight > 0\n ? cfg.textureHeight\n : (texture?.height ?? DEFAULT_DOT);\n if (cfg.flipbookCropOnly) {\n return { frameW: texW, frameH: texH };\n }\n return { frameW: texW / cfg.hframes, frameH: texH / cfg.vframes };\n}\n\n// The largest value a scale curve reaches (default 1 when there is no curve), so the\n// canvas margin accounts for scale-over-life that grows the sprite past its base size.\nfunction curveMax(points: { y: number }[] | undefined): number {\n if (!points || points.length === 0) return 1;\n let max = 0;\n for (const point of points) if (point.y > max) max = point.y;\n return max > 0 ? max : 1;\n}\n\n// Half-extent (px) a particle sprite can reach beyond the emitter box, so the overlay\n// canvas can be grown to contain it instead of clipping it to the (often tiny) node\n// box. Uses the diagonal so it's correct under any rotation, and the max base scale ×\n// the scale-curve peak. Pure (no DOM/GL) — unit-tested.\nexport function spriteExtentPad(\n cfg: ParticleSpecConfig,\n texture: ParticleTextureHandle | null,\n): number {\n const { frameW, frameH } = frameSize(cfg, texture);\n const scaleMax = Math.max(cfg.scaleMin, cfg.scaleMax);\n const curvePeak = Math.max(\n curveMax(cfg.scaleCurveX ?? cfg.scaleCurve),\n curveMax(cfg.scaleCurveY ?? cfg.scaleCurve),\n );\n const half = (scaleMax * curvePeak * Math.hypot(frameW, frameH)) / 2;\n return Math.min(PAD_CAP, Math.max(0, Math.ceil(half)));\n}\n\n// The emission shape's reach from the shape's own centre, per axis, BEFORE the shape offset.\n// Mirrors `sampleEmission` in `./simulate` (extents x scale; ring treated as its outer radius).\nfunction emissionReach(cfg: ParticleSpecConfig): { x: number; y: number } {\n switch (cfg.emissionShape) {\n case 3: // box\n return {\n x: Math.abs(cfg.emissionBoxExtents[0] * cfg.emissionScale[0]),\n y: Math.abs(cfg.emissionBoxExtents[1] * cfg.emissionScale[1]),\n };\n case 1: // sphere (disk)\n case 2: // sphere surface (ring)\n case 6: {\n // ring\n const r = Math.max(cfg.emissionSphereRadius, cfg.emissionRingRadius);\n return {\n x: r * Math.abs(cfg.emissionScale[0]),\n y: r * Math.abs(cfg.emissionScale[1]),\n };\n }\n default: // 0 point / 4 points -> no spread\n return { x: 0, y: 0 };\n }\n}\n\n// Half-extent (px) the EMISSION SHAPE reaches from the node origin, so the overlay canvas\n// is grown to contain particles spawned ACROSS a box/sphere/ring (not just at a point).\n// Added to `spriteExtentPad` in `syncCanvasSize`: without it a tiny sprite with a large\n// emission area (e.g. the card-sparkles box ~120x170) would be clipped to a small canvas.\n// Mirrors `sampleEmission` in `./simulate` (extents x scale, plus the shape offset). Pure\n// (no DOM/GL) — unit-tested.\n//\n// SYMMETRIC, and kept exactly as it was: it is the FLOOR the directional law can never go below\n// (see `particleCanvasExtents`), so the old number has to stay computable.\nexport function emissionExtentPad(cfg: ParticleSpecConfig): number {\n const reach = emissionReach(cfg);\n const half = Math.max(\n Math.abs(cfg.emissionOffset[0]) + reach.x,\n Math.abs(cfg.emissionOffset[1]) + reach.y,\n );\n return Math.min(PAD_CAP, Math.max(0, Math.ceil(half)));\n}\n\n/** The emission shape's reach from the NODE ORIGIN, per side: the shape's own half-extents about\n * its (possibly offset) centre. An offset shape reaches further on one side than the other, which\n * the symmetric `emissionExtentPad` had to round up to the larger of the two. Pure. */\nexport function emissionExtents(cfg: ParticleSpecConfig): ParticleExtents {\n const reach = emissionReach(cfg);\n const ox = cfg.emissionOffset[0];\n const oy = cfg.emissionOffset[1];\n return {\n left: Math.max(0, reach.x - ox),\n right: Math.max(0, reach.x + ox),\n top: Math.max(0, reach.y - oy),\n bottom: Math.max(0, reach.y + oy),\n };\n}\n\n// The maximum of cos(theta - phi) over theta in [a, b] — i.e. how far a unit vector confined to\n// that arc can reach along the +phi axis. 1 when the arc CONTAINS phi (the peak is inside), else\n// whichever endpoint is closer to it. The workhorse behind the four per-axis maxima below.\nfunction maxCosOverArc(a: number, b: number, phi: number): number {\n if (b - a >= TAU) return 1;\n // Where phi sits inside the arc, measured from `a` and wrapped into [0, TAU).\n let offset = (phi - a) % TAU;\n if (offset < 0) offset += TAU;\n if (offset <= b - a) return 1;\n return Math.max(Math.cos(a - phi), Math.cos(b - phi));\n}\n\n// A projection factor below this is TAKEN AS ZERO. `Math.cos(Math.PI / 2)` is 6.1e-17, not 0, so an\n// axis-aligned emitter — much the commonest kind — reaches its two PERPENDICULAR sides by ~1e-14px,\n// which `resolveSide`'s `Math.ceil` then rounds up to a whole wasted pixel of canvas per side (and\n// splits `padX` from `padY` in the frozen-frame key for no reason). The threshold is ~13 orders of\n// magnitude above that noise and ~10 below any projection a real spread produces, so it can only\n// ever discard the noise: at this factor even a 1,000,000 px/s·s travel term contributes 1e-6 px.\nconst AXIS_EPSILON = 1e-12;\n\n// Per-side maxima of a UNIT vector whose angle is confined to [a, b], in screen axes (+x right,\n// +y DOWN — Godot 2D's convention, which is also CSS's). Each is clamped at 0: an arc that cannot\n// reach a side at all contributes nothing to it rather than a negative margin.\nfunction arcAxisMaxima(a: number, b: number): ParticleExtents {\n return {\n right: axisMax(maxCosOverArc(a, b, 0)),\n left: axisMax(maxCosOverArc(a, b, Math.PI)),\n bottom: axisMax(maxCosOverArc(a, b, Math.PI / 2)),\n top: axisMax(maxCosOverArc(a, b, -Math.PI / 2)),\n };\n}\n\nfunction axisMax(value: number): number {\n return value > AXIS_EPSILON ? value : 0;\n}\n\nfunction maxAbs(a: number, b: number): number {\n return Math.max(Math.abs(a), Math.abs(b));\n}\n\n// How far an initial speed `v0` can travel in `t` seconds under Godot's damping (`./simulate`'s\n// `integrate`), using the SMALLEST damping any particle in the system can draw — the one that\n// travels furthest. Damping only ever REMOVES speed, so ignoring it would also be a valid bound;\n// folding it in just makes the bound tighter (and the canvas smaller) for the systems that use it.\nfunction dampedDistance(\n v0: number,\n t: number,\n damping: number,\n asFriction: boolean,\n): number {\n if (v0 <= 0 || t <= 0) return 0;\n if (damping <= 0) return v0 * t;\n if (asFriction) {\n // dec = cur * damping * 0.05 * dt ⇒ exponential decay at rate k.\n const k = damping * 0.05;\n return (v0 / k) * (1 - Math.exp(-k * t));\n }\n // dec = damping * dt ⇒ linear decay to a full stop at v0/damping.\n const stop = v0 / damping;\n const te = Math.min(t, stop);\n return v0 * te - 0.5 * damping * te * te;\n}\n\n/**\n * The BALLISTIC TRAVEL bound: how far, per side, a particle's POSITION can get from where it was\n * born, over one full lifetime. Pure, closed-form (this runs once per system, not per frame) and\n * deliberately an over-estimate — see the module header.\n *\n * The terms, each mapped onto the sides it can actually push toward:\n * * INITIAL VELOCITY over the `direction` ± `spread` arc, damped (`dampedDistance`).\n * * GRAVITY — a fixed vector, so `0.5 * g * t^2` lands on exactly one side per axis.\n * * LINEAR ACCEL — along the velocity, i.e. inside the same arc (a NEGATIVE one along the\n * REVERSED arc, since it can flip the particle around).\n * * RADIAL + TANGENTIAL ACCEL — both point along/across the particle's own position vector, which\n * can be anywhere, so they are added ISOTROPICALLY to all four sides.\n *\n * `lifetimeRandomness` is not folded in: Godot draws `lifetime * (1 - rand * randomness)`, so it\n * only ever SHORTENS a particle's life. Nor is `speedScale`, which scales the sim's clock and not\n * its distances — a particle still dies after `lifetime` seconds of its OWN time, having covered\n * the same ground faster or slower.\n */\nexport function travelExtents(cfg: ParticleSpecConfig): ParticleExtents {\n const t = Math.max(0, cfg.lifetime);\n if (!(t > 0)) return { ...ZERO_EXTENTS };\n const halfTT = 0.5 * t * t;\n\n // The spawn arc. `atan2(0, 0)` is 0, i.e. a zero `direction` reads as +x — which is what\n // `./simulate`'s `restartParticle` does with it too.\n const dir = Math.atan2(cfg.direction[1], cfg.direction[0]);\n const spread = Math.min(180, Math.abs(cfg.spread)) * DEG2RAD;\n const arc = arcAxisMaxima(dir - spread, dir + spread);\n const back = arcAxisMaxima(dir - spread + Math.PI, dir + spread + Math.PI);\n\n const v0 = maxAbs(cfg.initialVelocityMin, cfg.initialVelocityMax);\n const damping = Math.max(0, Math.min(cfg.dampingMin, cfg.dampingMax));\n const speedDist = dampedDistance(v0, t, damping, cfg.dampingAsFriction);\n\n const accelFwd = Math.max(0, cfg.linearAccelMax) * halfTT;\n const accelBack = Math.max(0, -cfg.linearAccelMin) * halfTT;\n\n const isotropic =\n (maxAbs(cfg.radialAccelMin, cfg.radialAccelMax) +\n maxAbs(cfg.tangentialAccelMin, cfg.tangentialAccelMax)) *\n halfTT;\n\n const gx = cfg.gravity[0] * halfTT;\n const gy = cfg.gravity[1] * halfTT;\n\n return {\n left:\n arc.left * speedDist +\n arc.left * accelFwd +\n back.left * accelBack +\n Math.max(0, -gx) +\n isotropic,\n right:\n arc.right * speedDist +\n arc.right * accelFwd +\n back.right * accelBack +\n Math.max(0, gx) +\n isotropic,\n top:\n arc.top * speedDist +\n arc.top * accelFwd +\n back.top * accelBack +\n Math.max(0, -gy) +\n isotropic,\n bottom:\n arc.bottom * speedDist +\n arc.bottom * accelFwd +\n back.bottom * accelBack +\n Math.max(0, gy) +\n isotropic,\n };\n}\n\n/**\n * Turn a host-supplied VISIBLE RECT (in the element's own local px space) into the per-side margin\n * it allows, given where the node box sits inside that space. Pure.\n *\n * The canvas spans `[boxOffsetX - left, boxOffsetX + boxW + right]` horizontally (see\n * `measureCanvasGeometry`), so \"stay inside the visible rect\" is one subtraction per side. A\n * negative result (the box is entirely off the visible rect on that side) clamps to 0.\n */\nexport function visibleAllowance(\n rect: ParticleLocalRect,\n box: { width: number; height: number; offsetX: number; offsetY: number },\n): ParticleExtents {\n return {\n left: Math.max(0, box.offsetX - rect.x),\n right: Math.max(0, rect.x + rect.width - box.offsetX - box.width),\n top: Math.max(0, box.offsetY - rect.y),\n bottom: Math.max(0, rect.y + rect.height - box.offsetY - box.height),\n };\n}\n\n/**\n * THE SIZING LAW: the per-side canvas margin for one system. Pure.\n *\n * `allowance` is the visible-rect budget (`visibleAllowance`) or null when the host has not said\n * what can be seen — in which case the only ceiling is `PAD_CAP`, which is what this always had.\n *\n * The floor is the SYMMETRIC pad this replaces (`spriteExtentPad + emissionExtentPad`, capped), so\n * the result is never smaller than the canvas the same system got before directional extents\n * existed — including when the allowance is tiny or zero.\n */\nexport function particleCanvasExtents(\n cfg: ParticleSpecConfig,\n texture: ParticleTextureHandle | null,\n allowance: ParticleExtents | null,\n): ParticleExtents {\n const sprite = spriteExtentPad(cfg, texture);\n const floor = Math.min(PAD_CAP, sprite + emissionExtentPad(cfg));\n const emission = emissionExtents(cfg);\n const travel = travelExtents(cfg);\n\n let left = emission.left + travel.left + sprite;\n let right = emission.right + travel.right + sprite;\n let top = emission.top + travel.top + sprite;\n let bottom = emission.bottom + travel.bottom + sprite;\n\n // ORBIT rotates the whole position vector about the node origin at `orbit` revolutions/second\n // (`./simulate`'s `integrate`), preserving its length — so over a full turn any reach on one axis\n // shows up on every other. Bound it radially: one number, applied to all four sides. Deliberately\n // not \"is a full turn reached within the lifetime\": a partial turn still sweeps an arc this has\n // no cheap closed form for, and the whole point is to be safely generous.\n if (maxAbs(cfg.orbitVelocityMin, cfg.orbitVelocityMax) > 0) {\n const radial = Math.max(left, right, top, bottom);\n left = radial;\n right = radial;\n top = radial;\n bottom = radial;\n }\n\n return {\n left: resolveSide(left, floor, allowance?.left),\n right: resolveSide(right, floor, allowance?.right),\n top: resolveSide(top, floor, allowance?.top),\n bottom: resolveSide(bottom, floor, allowance?.bottom),\n };\n}\n\n// One side: the computed reach, capped by the visible allowance and by `PAD_CAP`, then floored at\n// the symmetric pad so this can only ever GROW a canvas. Integral, because it is a css-px canvas\n// coordinate and a fractional one would put the draw origin between pixels.\nfunction resolveSide(\n reach: number,\n floor: number,\n allowance: number | undefined,\n): number {\n let side = Math.min(PAD_CAP, Math.max(0, Math.ceil(reach)));\n if (allowance !== undefined) side = Math.min(side, Math.ceil(allowance));\n return Math.max(floor, side);\n}\n\n/** The symmetric margin this module replaced, still computed exactly as it was — the shape the\n * runtime uses when `particleTravelExtents` is off (its kill switch). */\nexport function symmetricCanvasExtents(\n cfg: ParticleSpecConfig,\n texture: ParticleTextureHandle | null,\n): ParticleExtents {\n const pad = Math.min(\n PAD_CAP,\n spriteExtentPad(cfg, texture) + emissionExtentPad(cfg),\n );\n return { left: pad, right: pad, top: pad, bottom: pad };\n}\n\n/**\n * Parse a host's `data-godot-particle-visible-rect` attribute: four comma-separated numbers\n * `x,y,width,height` in the element's own local px space. Null for absent/empty/malformed, and for\n * a degenerate (non-positive) size — \"nothing is visible\" is not a canvas budget any caller wants\n * to act on, and treating it as absent keeps the uncapped-but-`PAD_CAP`ed path.\n */\nexport function parseLocalVisibleRect(\n attr: string | null,\n): ParticleLocalRect | null {\n if (!attr) return null;\n const parts = attr.split(\",\");\n if (parts.length !== 4) return null;\n const x = Number(parts[0]);\n const y = Number(parts[1]);\n const width = Number(parts[2]);\n const height = Number(parts[3]);\n if (!Number.isFinite(x) || !Number.isFinite(y)) return null;\n if (!(width > 0) || !(height > 0)) return null;\n return { x, y, width, height };\n}\n","// The RENDERER SEAM of the particle runtime: everything in `./runtime` that depends on HOW a\n// binding's pixels are produced, behind one interface. The runtime keeps the simulation, the\n// bindings, the canvases, the loop and the frozen/dormant/image-swap machinery — all of which are\n// renderer-agnostic — and hands a packed `InstanceBuffer` plus the draw's inputs to a backend.\n//\n// The WebGL backend below is `drawBinding`'s GL half MOVED, not rewritten: the same call order, the\n// same bottom-left source-rect math, the same profiling buckets. A WebGPU peer lands against this\n// same interface later, and it renders STRAIGHT into each node's canvas — no shared canvas, no\n// blit — which is what the two unusual shapes here exist for:\n//\n// - `ParticleSurface.ctx2d` is NULL on such a backend. The runtime's two 2D-CANVAS-ONLY features\n// — the static-frame cache and the surface image swap, both of which re-read a canvas through a\n// 2D context — key off exactly that, rather than off a renderer name.\n// - the draw options carry texture HANDLES (`ParticleTextureHandle`), not `WebGLTexture`s, so a\n// GPU texture entry (which has no `WebGLTexture` to offer) can satisfy them unchanged.\n//\n// `beginFrame`/`endFrame` bracket one tick's worth of draws. They are no-ops on WebGL, whose\n// submits are independent; a WebGPU backend records the tick into ONE command encoder and submits\n// it once at `endFrame` (the measured reason it is fast — see docs/perf-harness.md S7).\n\nimport {\n clearWebglSurface,\n disposeParticleInstanceBuffer,\n drawParticles,\n getParticleProgram,\n} from \"@godot-scene-web/canvas-effects/webgl\";\nimport type { InstanceBuffer } from \"@godot-scene-web/effects/particles\";\nimport {\n bakeGradient,\n GRADIENT_INTERPOLATE_CONSTANT,\n type GradientBakeSpec,\n} from \"../webgl/bake-texture\";\nimport {\n ensureSharedDrawSize,\n getBakedTexture,\n getImageTexture,\n performanceNow,\n type SharedGl,\n type TextureEntry,\n} from \"../webgl/shared-gl\";\nimport type { ParticleProfile } from \"./runtime\";\nimport type { ParticleSpecConfig } from \"./spec\";\n\n/** What one binding draws INTO. The canvas is the runtime's (it owns its DOM life); the context is\n * the backend's, because a canvas can only ever hold ONE context type — which is why the choice is\n * made once, at create, and why a backend swap means a new canvas element. */\nexport interface ParticleSurface {\n canvas: HTMLCanvasElement;\n /** The 2D blit target on the WebGL backend (the shared GL canvas is copied onto it), and NULL on a\n * backend that renders into `canvas` directly. Everything that needs to READ this surface's\n * pixels back — the static-frame cache, the surface image swap — is gated on it being non-null. */\n ctx2d: CanvasRenderingContext2D | null;\n}\n\n/** The structural subset of a texture entry the runtime and the draw carry around: enough to size a\n * sprite (`width`/`height`), to know whether the real pixels have arrived (`loaded`, the frozen\n * frame's cache gate) and to be told when they do (`listeners`, see `onTextureLoaded`). Deliberately\n * NOT `TextureEntry`: the GPU resource itself is the backend's business, and a non-WebGL entry\n * carries no `WebGLTexture`. */\nexport interface ParticleTextureHandle {\n width: number;\n height: number;\n loaded: boolean;\n listeners: Set<() => void>;\n}\n\n/** One binding's draw inputs: the packed instances are the buffer, everything else is here. Mirrors\n * `DrawParticlesOptions` (the fragment feature set) with the viewport quartet replaced by the node\n * canvas's own backing-store size — how that maps onto a viewport, a clamped target rect and a blit\n * is a backend's private business (see the WebGL implementation). */\nexport interface ParticleDrawOptions {\n /** The node canvas's BACKING-STORE size (px): the coordinate domain the packed instance positions\n * are already expressed in, and the rect a blitting backend copies into. */\n width: number;\n height: number;\n texture: ParticleTextureHandle | null;\n textured: boolean;\n /** Baked per-texel color LUT (see `ParticleSpecConfig.colorLut`), or null. */\n lutTexture: ParticleTextureHandle | null;\n /** Quad-shaped coverage mask (see `ParticleSpecConfig.maskUrl`), or null. */\n maskTexture: ParticleTextureHandle | null;\n hframes: number;\n vframes: number;\n blendMode: number;\n /** `ParticleSpecConfig.alphaFromRed`: coverage from the source RED channel, pre-LUT. */\n alphaFromRed?: boolean;\n /** `ParticleSpecConfig.alphaErode`: constant-erosion smoothstep applied to coverage. */\n erode?: { threshold: number; softness: number } | null;\n /** `ParticleSpecConfig.uvPolar`: sample the sheet through Godot's polar_coordinates remap. */\n uvPolar?: boolean;\n}\n\n/** The three texture handles one system draws with, resolved from its spec by the backend that will\n * sample them (see `ParticleRenderBackend.resolveTextures`). The runtime holds them for reasons that\n * have nothing to do with sampling — the sprite's decoded size drives the canvas pad, `loaded` gates\n * the frozen-frame cache, `listeners` is how a late decode kicks a redraw — which is why they come\n * back as handles rather than staying private to the backend. */\nexport interface ParticleTextureSet {\n texture: ParticleTextureHandle | null;\n lut: ParticleTextureHandle | null;\n mask: ParticleTextureHandle | null;\n}\n\nexport interface ParticleRenderBackend {\n readonly kind: \"webgl\" | \"webgpu\";\n /** Give `canvas` its rendering context (and whatever per-surface GPU state the backend needs).\n * NULL means this canvas cannot be rendered to at all, and the binding is refused — today's\n * `getContext(\"2d\")` returning null. */\n createSurface(\n canvas: HTMLCanvasElement,\n config: ParticleSpecConfig,\n ): ParticleSurface | null;\n /** Resolve `config`'s sprite / colour-LUT / mask into handles this backend can sample.\n *\n * A texture belongs to the API that will bind it — a `WebGLTexture` is meaningless to a WebGPU\n * pass and vice versa — so the CACHE is the backend's, not the runtime's. Both implementations key\n * their cache with the SAME strings for the same inputs (`clamp:`/`repeat:` + url, and\n * `particle-lut:${JSON}` from `particleLutBake`), which is what lets a page that falls back from\n * WebGPU to WebGL mid-run re-derive the same entry for the same texture. */\n resolveTextures(config: ParticleSpecConfig): ParticleTextureSet;\n /** Release every GPU resource this binding owned, the instance buffer included. The canvas element\n * itself belongs to the runtime, which removes it. */\n disposeSurface(surface: ParticleSurface, buffer: InstanceBuffer): void;\n /** Longest-edge ceiling this backend can back a canvas with, folded into the sizing law, or\n * UNDEFINED for no limit of its own — which is what WebGL reports (the shared drawing buffer\n * discovers its ceiling at draw time instead, see `ensureSharedDrawSize`). */\n maxBackingDim(): number | undefined;\n /** Called once around each tick's draws (see the module doc). */\n beginFrame(): void;\n endFrame(): void;\n /** Blank `surface` over `w`x`h` — the live tick's no-live-instances frame and the expired-burst\n * retire. A REAL frame, cheap but not free, so it books to `blitMs` on a backend where it is\n * 2D-canvas fill. */\n clear(\n surface: ParticleSurface,\n w: number,\n h: number,\n prof: ParticleProfile | null,\n ): void;\n /** Draw `buffer`'s instances onto `surface`. `prof` is the caller's cost attribution or null (the\n * frozen path draws once, off the per-frame buckets — see `ParticleProfile`), and every bracket\n * must sit behind that one null check so an unprofiled draw takes no clock reading at all. */\n draw(\n surface: ParticleSurface,\n buffer: InstanceBuffer,\n opts: ParticleDrawOptions,\n prof: ParticleProfile | null,\n ): void;\n /** Frames this backend has SUBMITTED to the GPU, or absent where the question is meaningless (a\n * WebGL backend submits per draw, through a context it does not own). Read as a stat, never by a\n * render decision — it is how a probe checks that a tick really cost ONE submit and not N. */\n submits?(): number;\n /** Re-render `surface`'s current frame into an offscreen target and read it back as tightly-packed\n * RGBA (premultiplied, top-down) at the surface's backing size — see\n * `ParticleRuntime.captureNodePixels`, which is the only caller.\n *\n * ABSENT on the WebGL backend, deliberately: its canvas already holds readable 2D pixels, so a\n * consumer that wants them uses `getImageData`. It exists at all because a WebGPU canvas has no\n * such path (`drawImage`/`toDataURL` from one are blank headless and pathological on Android —\n * see `../webgpu/readback`), so the pixels must be produced a second time into a texture that can\n * be copied. */\n captureSurface?(\n surface: ParticleSurface,\n buffer: InstanceBuffer,\n opts: ParticleDrawOptions,\n ): Promise<Uint8Array | null>;\n}\n\n/** Godot's `GradientTexture1D` default width — the resolution the game's own LUT samplers are baked\n * at, so the browser samples the same quantization. */\nconst LUT_WIDTH = 256;\n\n/**\n * The colour-LUT bake for a system — its cache KEY, the gradient to bake, and whether it must be\n * sampled with NEAREST — or null when the spec carries no `colorLut`.\n *\n * Pure, and shared by both backends ON PURPOSE: the key is a cross-cache invariant (`particle-lut:`\n * + the serialized spec), so the WebGL and WebGPU caches hand out the same LUT for the same stops\n * and a fallback mid-run does not re-bake a subtly different ramp.\n */\nexport function particleLutBake(\n cfg: ParticleSpecConfig,\n): { key: string; spec: GradientBakeSpec; nearest: boolean } | null {\n const stops = cfg.colorLut;\n if (!stops || stops.length === 0) return null;\n const mode = cfg.colorLutInterpolation ?? 0;\n const spec: GradientBakeSpec = {\n kind: \"gradient\",\n width: LUT_WIDTH,\n stops,\n interpolationMode: mode,\n };\n return {\n key: `particle-lut:${JSON.stringify(spec)}`,\n spec,\n // CONSTANT stops are hard steps; LINEAR filtering would smear each boundary back.\n nearest: mode === GRADIENT_INTERPOLATE_CONSTANT,\n };\n}\n\n/** The baked colour-LUT texture for a system on the WEBGL backend, or null when the spec carries no\n * `colorLut`. Cached (by value, in shared-gl's texture cache) so the many nodes that share one VFX\n * material — every hit-streak burst in a combat — share ONE 256x1 GL texture. Re-exported from\n * `./runtime`, where it used to live. */\nexport function lutTextureFor(\n gl: WebGL2RenderingContext,\n cfg: ParticleSpecConfig,\n): TextureEntry | null {\n const bake = particleLutBake(cfg);\n if (!bake) return null;\n return getBakedTexture(gl, bake.key, () => bakeGradient(bake.spec), {\n repeat: false,\n nearest: bake.nearest,\n });\n}\n\n// Under this backend a texture handle IS a shared-gl `TextureEntry` — the runtime resolves them from\n// the shared cache — and the interface types them structurally only so a GPU entry can satisfy it\n// later. Narrow back here, at the one place that needs the resource itself.\nfunction glTextureOf(\n handle: ParticleTextureHandle | null,\n): WebGLTexture | null {\n return (handle as TextureEntry | null)?.texture ?? null;\n}\n\n/**\n * The WebGL particle backend: one shared offscreen WebGL2 context for every system on the page\n * (`../webgl/shared-gl`), rendered into a viewport sub-rect of its grow-only canvas and blitted onto\n * each node's own 2D canvas — the pipeline this runtime has always had.\n *\n * NULL when the instanced particle program does not compile or link, which is the runtime's\n * long-standing \"no particles at all\" gate: the caller returns a no-op handle and every opted-in node\n * stays on its static `<span>` preview.\n */\nexport function createWebglParticleBackend(\n shared: SharedGl,\n): ParticleRenderBackend | null {\n const { gl } = shared;\n const program = getParticleProgram(gl);\n if (!program) return null;\n return {\n kind: \"webgl\",\n createSurface(canvas: HTMLCanvasElement): ParticleSurface | null {\n // A PLAIN 2D context, with no options: this canvas is the blit target for the shared GL\n // canvas, and `drawImage` between canvases needs nothing declared here (the alpha contract\n // lives on the SOURCE — `../webgl/shared-gl.ts` declares `premultipliedAlpha: true`, and\n // every 2D canvas is premultiplied by definition, so the copy is a straight copy).\n //\n // The additive path resolves in-canvas to `(light, coverage)`, which is a source-over-\n // complete frame: source-over then contributes `light + dst*(1 - coverage)`. That is the\n // closest source-over gets to Godot's `light + dst`, not an equal — the node ALSO carries\n // `mix-blend-mode: plus-lighter` when its material says BLEND_MODE_ADD (`../material.ts`),\n // which is what supplies the missing `dst` term at the page level. The in-canvas resolve is\n // what makes overlapping particles inside ONE system stack correctly; the CSS blend is what\n // makes the system stack correctly on the page.\n const ctx2d = canvas.getContext(\"2d\");\n if (!ctx2d) return null;\n return { canvas, ctx2d };\n },\n // The shared image-texture cache: the many nodes of one VFX family share each entry, and a\n // still-loading one is the 1x1 TRANSPARENT placeholder — so a masked system draws nothing rather\n // than flashing an unmasked square, which is what the game shows too.\n resolveTextures(config: ParticleSpecConfig) {\n return {\n texture: config.textureUrl\n ? getImageTexture(gl, config.textureUrl, { repeat: false })\n : null,\n lut: lutTextureFor(gl, config),\n mask: config.maskUrl\n ? getImageTexture(gl, config.maskUrl, { repeat: false })\n : null,\n };\n },\n disposeSurface(_surface: ParticleSurface, buffer: InstanceBuffer): void {\n disposeParticleInstanceBuffer(gl, buffer);\n },\n // The shared drawing buffer is grow-only and VERIFIES what it really got, so this backend needs\n // no size law of its own (see `ensureSharedDrawSize`): a canvas larger than the buffer can hold\n // is drawn at a reduced target rect and scaled back up by the blit, never clamped up front.\n maxBackingDim(): number | undefined {\n return undefined;\n },\n // No-ops: each system's draw is submitted on its own, exactly as before this seam existed.\n beginFrame(): void {},\n endFrame(): void {},\n clear(\n surface: ParticleSurface,\n w: number,\n h: number,\n prof: ParticleProfile | null,\n ): void {\n // The CLEAR is 2D-canvas fill, the same cost class as the blit that usually follows it, so it\n // is booked to `blitMs` — on the clear-only path too, which is a real (if cheap) frame.\n const clearStart = prof ? performanceNow() : 0;\n surface.ctx2d?.clearRect(0, 0, w, h);\n if (prof) prof.blitMs += performanceNow() - clearStart;\n },\n draw(\n surface: ParticleSurface,\n buffer: InstanceBuffer,\n opts: ParticleDrawOptions,\n prof: ParticleProfile | null,\n ): void {\n const ctx2d = surface.ctx2d;\n const w = opts.width;\n const h = opts.height;\n const clearStart = prof ? performanceNow() : 0;\n ctx2d?.clearRect(0, 0, w, h);\n if (prof) prof.blitMs += performanceNow() - clearStart;\n\n // Grow-only: never shrink the shared GL canvas. Re-assigning canvas.width/height reallocates the drawing\n // buffer (very expensive) — and with MANY differently-sized particle systems (screen-filling background\n // emitters can be 1500–2048px) each setting the shared canvas to its own size every frame, that realloc\n // thrash dominated the frame. Keep the shared canvas at the max ever needed and render into a viewport\n // sub-rect. The grow is VERIFIED against the actual drawing buffer (see `ensureSharedDrawSize`):\n // drawing/blitting at the attribute size when the buffer came back smaller reads out-of-bounds — the\n // shader runtime's black-band bug, same shared canvas. vw×vh ≤ w×h is what can really be drawn; the blit\n // scales it back up to the node canvas.\n const { vw, vh, bufH } = ensureSharedDrawSize(shared, w, h);\n clearWebglSurface(gl, vw, vh);\n\n // GL SUBMIT bucket — issue cost only (the GPU runs async; see `ParticleProfile.glMs`). The\n // options object is inside the bracket because building it IS part of submitting the draw.\n const glStart = prof ? performanceNow() : 0;\n drawParticles(shared, program, buffer, {\n texture: glTextureOf(opts.texture),\n textured: opts.textured,\n lutTexture: glTextureOf(opts.lutTexture),\n maskTexture: glTextureOf(opts.maskTexture),\n hframes: opts.hframes,\n vframes: opts.vframes,\n blendMode: opts.blendMode,\n // The px coordinate DOMAIN stays the node's full backing (the vertex NDC mapping divides by\n // it); the clamped `target` is where the pixels land — together they scale the draw down.\n viewportW: w,\n viewportH: h,\n targetW: vw,\n targetH: vh,\n alphaFromRed: opts.alphaFromRed,\n erode: opts.erode,\n uvPolar: opts.uvPolar,\n });\n if (prof) prof.glMs += performanceNow() - glStart;\n // The viewport rendered into the framebuffer's bottom-left (GL origin) = the BOTTOM `vh` rows of the\n // (possibly taller, grow-only) shared canvas image; copy that sub-region rather than the whole canvas. The\n // dest covers the node's FULL w×h backing: a buffer-capped draw scales up (reduced resolution, never a\n // clipped band).\n //\n // A PURE COPY, in bytes: the shared canvas declares `premultipliedAlpha: true` and a 2D\n // canvas is premultiplied, so `drawImage` converts nothing. It did once — the source was\n // declared STRAIGHT while the MIX fragment wrote premultiplied content, and this line was\n // where the browser dutifully multiplied by alpha a second time, landing MIX particles at\n // `(c·a², a)`. That is why the alpha contract is stated on the context, not here.\n const blitStart = prof ? performanceNow() : 0;\n ctx2d?.drawImage(shared.canvas, 0, bufH - vh, vw, vh, 0, 0, w, h);\n if (prof) prof.blitMs += performanceNow() - blitStart;\n },\n };\n}\n","// The WebGPU texture cache — the twin of `../webgl/shared-gl.ts`'s `textureCache`, deliberately\n// built to be PROVABLY parallel to it:\n//\n// - the cache KEYS are the same strings for the same inputs (`clamp:`/`repeat:` + url, and the\n// caller-supplied `particle-lut:{…}` for baked ones). Both caches are module-scoped, so a page\n// that falls back from WebGPU to WebGL mid-run re-derives the same key for the same texture and\n// nothing in the runtimes needs to know which cache answered.\n// - entries carry `width`/`height`/`loaded`/`listeners`, the quartet the particle runtime reads\n// off a WebGL `TextureEntry` (frame-grid sizing, sprite extent, the \"redraw when it decodes\"\n// subscription). Keeping the field names identical is what lets the runtime stay\n// renderer-agnostic over both.\n// - uploads keep STRAIGHT alpha (`premultipliedAlpha: false`), exactly as the GL path's\n// `UNPACK_PREMULTIPLY_ALPHA_WEBGL false`. Premultiplication happens ONCE, in WGSL, at fragment\n// output — premultiplying here as well would darken every edge texel by its own alpha twice.\n// - uploads are top-left-origin (no flip), so image row 0 is V=0, matching the Godot-convention\n// UVs the fragment code samples with.\n//\n// The one thing WebGL does NOT force on the runtime: a `GPUTexture`'s size is fixed at creation, so\n// the 1×1 placeholder cannot be re-uploaded into at the decoded size — the entry's `texture` AND\n// `view` are REPLACED when the image lands. Any bind group built from the placeholder view is\n// stale from that moment; rebuilding it is the job of the `loaded` listener, which is exactly the\n// callback the GL path already fires for its own reasons (a redraw).\n\nimport {\n createWebgpuRgbaTexture,\n createWebgpuSampler,\n destroyWebgpuTexture,\n uploadWebgpuExternalImage,\n uploadWebgpuRgba,\n} from \"@godot-scene-web/canvas-effects/webgpu\";\nimport { onWebgpuDeviceLost, TEXTURE_USAGE, type WebgpuShared } from \"./device\";\n\nexport interface GpuTextureEntry {\n /** REPLACED when an async image decodes — never captured across a `loaded` notification. */\n texture: GPUTexture;\n /** REPLACED with `texture`; the handle bind groups are built from. */\n view: GPUTextureView;\n sampler: GPUSampler;\n width: number;\n height: number;\n loaded: boolean;\n listeners: Set<() => void>;\n}\n\nexport interface GpuImageTextureOptions {\n repeat?: boolean;\n}\n\nexport interface GpuBakedTextureOptions {\n /** NEAREST for a CONSTANT-interpolation ramp: the bake already produced hard steps, and LINEAR\n * would smear each step boundary back across a texel. */\n nearest?: boolean;\n repeat?: boolean;\n}\n\n/** Baked pixel data. Structurally satisfied by `ImageData` and by `bakeTexture`'s\n * `{width, height, data}`, so a caller can hand either to `getBakedTextureGpu`. */\nexport interface BakedPixels {\n width: number;\n height: number;\n data: Uint8ClampedArray;\n}\n\n// Every texture a `copyExternalImageToTexture` writes into must declare RENDER_ATTACHMENT as well\n// as COPY_DST (the copy is implemented as a render pass), and TEXTURE_BINDING because that is what\n// the whole thing is for.\nconst _UPLOADABLE_USAGE =\n TEXTURE_USAGE.TEXTURE_BINDING |\n TEXTURE_USAGE.COPY_DST |\n TEXTURE_USAGE.RENDER_ATTACHMENT;\n\n// Module-scope caches, keyed by the same strings as the GL cache. `cacheDevice` is the generation\n// marker: entries belong to ONE device, so a new device (after a loss + re-acquire) starts clean\n// rather than handing out textures the new device cannot bind.\nconst textureCache = new Map<string, GpuTextureEntry>();\nconst samplerCache = new Map<string, GPUSampler>();\nlet cacheDevice: GPUDevice | null = null;\nlet unsubscribeLost: (() => void) | null = null;\n\n/** The cache key for an image texture — byte-identical to `getImageTexture`'s in\n * `../webgl/shared-gl.ts` (`${opts.repeat ? \"repeat\" : \"clamp\"}:${url}`, shared-gl.ts:318). If\n * that line ever changes, this one must change with it. */\nexport function imageTextureCacheKey(url: string, repeat: boolean): string {\n return `${repeat ? \"repeat\" : \"clamp\"}:${url}`;\n}\n\n/**\n * A sprite/mask texture from `url`, returned IMMEDIATELY as a 1×1 transparent placeholder and\n * filled in when the image decodes. Same contract as the GL path: until it loads, a masked system\n * draws nothing (the placeholder's red is 0) rather than flashing an unmasked square.\n */\nexport function getImageTextureGpu(\n shared: WebgpuShared,\n url: string,\n opts: GpuImageTextureOptions = {},\n): GpuTextureEntry {\n const repeat = opts.repeat === true;\n return loadImageTexture(\n shared,\n imageTextureCacheKey(url, repeat),\n url,\n repeat,\n null,\n );\n}\n\n/** The cache key for an atlas SUB-RECT texture — byte-identical to `getRegionTexture`'s in\n * `../webgl/shared-gl.ts`, for the same reason `imageTextureCacheKey` is. */\nexport function regionTextureCacheKey(\n url: string,\n region: AtlasRegion,\n repeat: boolean,\n): string {\n return (\n `${repeat ? \"repeat\" : \"clamp\"}:region` +\n `:${Math.round(region.x)},${Math.round(region.y)},${Math.round(region.width)},${Math.round(region.height)}:${url}`\n );\n}\n\n/** An atlas sprite's own sub-rect of the atlas PAGE, in page pixels, top-left origin. */\nexport interface AtlasRegion {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * Like `getImageTextureGpu` but uploads ONLY the atlas sub-rect `region` — the twin of\n * `getRegionTexture`, and needed for the same reason: bound the whole page instead and a recolour\n * shader's implicit `COLOR = texture(TEXTURE, UV)` samples the page's padding (opaque white) rather\n * than the sprite. Cropping makes the sprite fill UV [0,1], so `uvFit` and `TEXTURE_PIXEL_SIZE`\n * (both derived from the uploaded size) come out right with no shader change.\n *\n * The crop is the copy's own `origin`, so it costs no intermediate canvas — WebGPU can express\n * directly what the GL path has to draw into a scratch 2D canvas to achieve.\n */\nexport function getRegionTextureGpu(\n shared: WebgpuShared,\n url: string,\n region: AtlasRegion,\n opts: GpuImageTextureOptions = {},\n): GpuTextureEntry {\n const repeat = opts.repeat === true;\n return loadImageTexture(\n shared,\n regionTextureCacheKey(url, region, repeat),\n url,\n repeat,\n region,\n );\n}\n\n// The shared image loader behind both getters: a 1×1 transparent placeholder now, the decoded image\n// (whole, or cropped to `region`) uploaded when it lands.\nfunction loadImageTexture(\n shared: WebgpuShared,\n key: string,\n url: string,\n repeat: boolean,\n region: AtlasRegion | null,\n): GpuTextureEntry {\n ensureCacheDevice(shared);\n const cached = textureCache.get(key);\n if (cached) return cached;\n\n const { device } = shared;\n const entry: GpuTextureEntry = {\n texture: placeholderTexture(device, key),\n view: undefined as unknown as GPUTextureView,\n sampler: samplerFor(shared, { nearest: false, repeat }),\n width: 1,\n height: 1,\n loaded: false,\n listeners: new Set(),\n };\n entry.view = entry.texture.createView();\n textureCache.set(key, entry);\n\n const image = new Image();\n image.crossOrigin = \"anonymous\";\n image.onload = () => {\n // The entry may have been dropped by a device loss between `src` and `onload`; uploading into\n // a dead device is pointless and the listeners belong to surfaces that are gone.\n if (textureCache.get(key) !== entry || cacheDevice !== device) return;\n const naturalW = Math.max(1, image.naturalWidth || 1);\n const naturalH = Math.max(1, image.naturalHeight || 1);\n // A region is clamped to the decoded page: a stale/oversized region descriptor must crop to\n // something rather than make `copyExternalImageToTexture` throw out of an image callback.\n const width = region\n ? Math.max(1, Math.min(Math.round(region.width), naturalW))\n : naturalW;\n const height = region\n ? Math.max(1, Math.min(Math.round(region.height), naturalH))\n : naturalH;\n const originX = region\n ? Math.max(0, Math.min(Math.round(region.x), naturalW - width))\n : 0;\n const originY = region\n ? Math.max(0, Math.min(Math.round(region.y), naturalH - height))\n : 0;\n try {\n const uploaded = createWebgpuRgbaTexture(device, width, height, key);\n uploadWebgpuExternalImage(device, uploaded, {\n source: image,\n origin: [originX, originY],\n });\n const texture = uploaded.texture;\n entry.texture.destroy();\n entry.texture = texture;\n entry.view = texture.createView();\n entry.width = width;\n entry.height = height;\n } catch {\n // Upload refused (a tainted cross-origin image, a size past the device limit): the entry\n // stays the transparent placeholder, which draws nothing — the same visible outcome as the\n // GL path's failed `texImage2D`.\n return;\n }\n markLoaded(entry);\n };\n image.src = url;\n return entry;\n}\n\n/**\n * A procedurally baked texture (the particle colour LUT: a 256×1 gradient), uploaded SYNCHRONOUSLY\n * — the pixels already exist, so there is no placeholder state and `loaded` is true on return.\n * `key` is the caller's stable spec key, the same one it hands `getBakedTexture` on the GL side\n * (`particle-lut:${JSON.stringify(spec)}`), so identical samplers across nodes share one texture.\n */\nexport function getBakedTextureGpu(\n shared: WebgpuShared,\n key: string,\n bake: () => HTMLCanvasElement | BakedPixels,\n opts: GpuBakedTextureOptions = {},\n): GpuTextureEntry {\n ensureCacheDevice(shared);\n const cached = textureCache.get(key);\n if (cached) return cached;\n\n const { device } = shared;\n const baked = bake();\n const isCanvas = typeof (baked as BakedPixels).data === \"undefined\";\n const width = Math.max(1, Math.round(baked.width) || 1);\n const height = Math.max(1, Math.round(baked.height) || 1);\n const uploaded = createWebgpuRgbaTexture(device, width, height, key);\n const texture = uploaded.texture;\n if (isCanvas) {\n uploadWebgpuExternalImage(device, uploaded, {\n source: baked as HTMLCanvasElement,\n });\n } else {\n const pixels = (baked as BakedPixels).data;\n uploadWebgpuRgba(device, uploaded, pixels);\n }\n const entry: GpuTextureEntry = {\n texture,\n view: texture.createView(),\n sampler: samplerFor(shared, {\n nearest: opts.nearest === true,\n repeat: opts.repeat === true,\n }),\n width,\n height,\n loaded: true,\n listeners: new Set(),\n };\n textureCache.set(key, entry);\n return entry;\n}\n\n/** Module-scoped samplers: filtering + wrap is a two-bit space, so four objects serve every entry\n * and a per-texture sampler would only add driver-side state. */\nfunction samplerFor(\n shared: WebgpuShared,\n opts: { nearest: boolean; repeat: boolean },\n): GPUSampler {\n const key = `${opts.nearest ? \"nearest\" : \"linear\"}:${opts.repeat ? \"repeat\" : \"clamp\"}`;\n const cached = samplerCache.get(key);\n if (cached) return cached;\n const sampler = createWebgpuSampler(shared.device, opts);\n samplerCache.set(key, sampler);\n return sampler;\n}\n\nfunction placeholderTexture(device: GPUDevice, label: string): GPUTexture {\n const uploaded = createWebgpuRgbaTexture(device, 1, 1, label);\n uploadWebgpuRgba(device, uploaded, new Uint8Array([0, 0, 0, 0]));\n return uploaded.texture;\n}\n\nfunction markLoaded(entry: GpuTextureEntry): void {\n entry.loaded = true;\n for (const listener of [...entry.listeners]) listener();\n entry.listeners.clear();\n}\n\n// The cache lives and dies with the device: a lost device invalidates every texture in it, and a\n// re-acquire hands out a NEW `GPUDevice` whose bind groups cannot reference the old one's objects.\nfunction ensureCacheDevice(shared: WebgpuShared): void {\n if (cacheDevice === shared.device) return;\n clearCache();\n cacheDevice = shared.device;\n unsubscribeLost = onWebgpuDeviceLost(clearCache);\n}\n\nfunction clearCache(): void {\n for (const entry of textureCache.values()) {\n try {\n destroyWebgpuTexture(entry.texture);\n } catch {\n // Destroying against a lost device is a no-op that some implementations still refuse.\n }\n }\n textureCache.clear();\n samplerCache.clear();\n cacheDevice = null;\n unsubscribeLost?.();\n unsubscribeLost = null;\n}\n\n/** TEST-ONLY: the live cache keys, for asserting they match the GL cache's for the same inputs. */\nexport function __webgpuTextureCacheKeysForTest(): string[] {\n return [...textureCache.keys()];\n}\n\n/** TEST-ONLY: drop every cached texture/sampler so a suite can re-probe with a fresh stub device. */\nexport function __resetWebgpuTextureCacheForTest(): void {\n clearCache();\n}\n","export { INSTANCE_STRIDE } from \"@godot-scene-web/effects\";\n\nimport {\n createWebgpuParticleRenderer,\n peekWebgpuParticleRenderer,\n type WebgpuParticleRenderer,\n type WebgpuParticleRendererOptions,\n type WebgpuParticleSurfaceState,\n} from \"@godot-scene-web/canvas-effects/webgpu\";\n\nexport {\n __resetWebgpuParticleProgramForTest,\n ADDITIVE_BLEND,\n ADDITIVE_RESOLVE_WGSL,\n INSTANCE_STRIDE_BYTES,\n PARTICLE_FS_ADDITIVE_ENTRY,\n PARTICLE_FS_ENTRY,\n PARTICLE_VERTEX_BUFFERS,\n PARTICLE_VS_ENTRY,\n PARTICLE_WGSL,\n PREMULTIPLIED_BLEND,\n RESOLVE_FS_ENTRY,\n RESOLVE_VS_ENTRY,\n} from \"@godot-scene-web/canvas-effects/webgpu\";\n\nimport { bakeGradient } from \"../webgl/bake-texture\";\nimport { onTextureLoaded, performanceNow } from \"../webgl/shared-gl\";\nimport {\n configureCanvas,\n latchWebgpuFallbackReason,\n type WebgpuShared,\n} from \"../webgpu/device\";\nimport {\n type GpuTextureEntry,\n getBakedTextureGpu,\n getImageTextureGpu,\n} from \"../webgpu/textures\";\nimport {\n type ParticleRenderBackend,\n type ParticleSurface,\n type ParticleTextureSet,\n particleLutBake,\n} from \"./render-backend\";\nimport type { ParticleSpecConfig } from \"./spec\";\n\ntype HostSurface = ParticleSurface & { gpu: WebgpuParticleSurfaceState };\nfunction textures(shared: WebgpuShared, config: ParticleSpecConfig) {\n const bake = particleLutBake(config);\n return {\n sprite: config.textureUrl\n ? getImageTextureGpu(shared, config.textureUrl, { repeat: false })\n : null,\n lut: bake\n ? getBakedTextureGpu(shared, bake.key, () => bakeGradient(bake.spec), {\n repeat: false,\n nearest: bake.nearest,\n })\n : null,\n mask: config.maskUrl\n ? getImageTextureGpu(shared, config.maskUrl, { repeat: false })\n : null,\n };\n}\nfunction state(surface: ParticleSurface): HostSurface {\n return surface as HostSurface;\n}\nfunction options(shared: WebgpuShared): WebgpuParticleRendererOptions {\n return {\n device: shared.device,\n format: shared.format,\n onPipelineError: () => latchWebgpuFallbackReason(\"pipeline-error\"),\n };\n}\nexport async function createWebgpuParticleBackend(\n shared: WebgpuShared,\n): Promise<ParticleRenderBackend | null> {\n const renderer = await createWebgpuParticleRenderer(options(shared));\n return renderer ? backend(shared, renderer) : null;\n}\nexport function peekWebgpuParticleBackend(\n shared: WebgpuShared,\n): ParticleRenderBackend | null | undefined {\n const renderer = peekWebgpuParticleRenderer(options(shared));\n return renderer === undefined\n ? undefined\n : renderer === null\n ? null\n : backend(shared, renderer);\n}\nfunction backend(\n shared: WebgpuShared,\n renderer: WebgpuParticleRenderer,\n): ParticleRenderBackend {\n return {\n kind: \"webgpu\",\n createSurface(canvas, config) {\n const context = configureCanvas(canvas, shared);\n if (!context) return null;\n canvas.setAttribute(\"data-godot-effects-backend\", \"webgpu\");\n const ts = textures(shared, config);\n let listeners: Array<() => void> = [];\n const gpu = renderer.createSurface({\n context,\n textures: ts,\n onTexturesChanged(callback) {\n listeners = [ts.sprite, ts.lut, ts.mask]\n .filter((x): x is GpuTextureEntry => x !== null)\n .map((x) => onTextureLoaded(x, callback));\n return () => {\n for (const dispose of listeners) dispose();\n listeners = [];\n };\n },\n });\n return { canvas, ctx2d: null, gpu } as HostSurface;\n },\n resolveTextures(config): ParticleTextureSet {\n const ts = textures(shared, config);\n return { texture: ts.sprite, lut: ts.lut, mask: ts.mask };\n },\n disposeSurface(surface, _buffer) {\n const s = state(surface);\n renderer.disposeSurface(s.gpu);\n try {\n s.gpu.context.unconfigure();\n } catch {}\n },\n maxBackingDim: () => shared.limits.maxTextureDimension2D,\n beginFrame: () => renderer.beginFrame(),\n endFrame: () => renderer.endFrame(),\n clear(surface, _w, _h, prof) {\n const start = prof ? performanceNow() : 0;\n renderer.clear(state(surface).gpu);\n if (prof) prof.glMs += performanceNow() - start;\n },\n draw(surface, buffer, opts, prof) {\n const start = prof ? performanceNow() : 0;\n renderer.draw(state(surface).gpu, buffer, opts);\n if (prof) prof.glMs += performanceNow() - start;\n },\n submits: () => renderer.submits(),\n captureSurface: (surface, buffer, opts) =>\n renderer.captureSurface(state(surface).gpu, buffer, opts),\n };\n}\n","import {\n normalizeParticleRenderConfig,\n type ParticleRenderConfig,\n} from \"@godot-scene-web/effects/particles\";\n\n/** HTML attribute transport: portable parameters plus DOM placement and resource URLs. */\nexport interface ParticleSpecConfig extends ParticleRenderConfig {\n originX: number;\n originY: number;\n boxOffsetX: number;\n boxOffsetY: number;\n textureUrl: string | null;\n maskUrl?: string | null;\n}\n\nexport function normalizeParticleSpecConfig(\n raw: Partial<ParticleSpecConfig> | null | undefined,\n): ParticleSpecConfig {\n const value = raw ?? {};\n return {\n ...normalizeParticleRenderConfig(value),\n originX: finite(value.originX),\n originY: finite(value.originY),\n boxOffsetX: finite(value.boxOffsetX),\n boxOffsetY: finite(value.boxOffsetY),\n textureUrl: typeof value.textureUrl === \"string\" ? value.textureUrl : null,\n maskUrl:\n typeof value.maskUrl === \"string\" && value.maskUrl ? value.maskUrl : null,\n };\n}\n\n/** Decode the DOM attribute without putting its transport contract in the simulator. */\nexport function parseParticleSpecConfig(\n json: string | null | undefined,\n): ParticleSpecConfig | null {\n if (!json) return null;\n try {\n const raw = JSON.parse(json) as Partial<ParticleSpecConfig>;\n return raw && typeof raw === \"object\"\n ? normalizeParticleSpecConfig(raw)\n : null;\n } catch {\n return null;\n }\n}\n\nfunction finite(value: unknown): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n}\n","// Static-frame cache for FROZEN particle surfaces — the particle sibling of the shader runtime's\n// `staticFrameCache` (`../webgl/runtime.ts`), and it exists for the same reason: in frozen\n// (`staticParticles`) mode a system is warmed to a representative state, drawn ONCE, and then never\n// touched again, so two systems whose frozen frame is the same bitmap can share ONE render — one\n// warm + one instanced GL draw + N cheap 2D blits — instead of paying both per node.\n//\n// WHY THIS IS SOUND: the CPU simulation in this directory is fully DETERMINISTIC. Every random it\n// consumes comes from the seeded MINSTD/Park-Miller LCG in `./simulate` (`randFromSeed` /\n// `hash01(i, cfg.seed)`); there is no `Math.random()`, no `Date.now()` and no `performance.now()`\n// anywhere under `src/particles/`. `warmStaticParticles` is therefore a pure function of the\n// (config, particle-count) pair, and two frozen systems built from the same spec are already\n// pixel-identical twins on screen today — deduping them changes nothing visible.\n//\n// The caller (`./runtime`) is responsible for only consulting this on the path where that purity\n// actually holds: a binding whose simulation state is still the deterministic post-create state.\n// A system frozen mid-flight (the runtime was LIVE and `setStaticParticles(true)` flipped it) warms\n// from whatever phase it happened to be in, which is NOT a function of the config, so the runtime\n// marks those bindings non-pristine and never keys them. See `staticFrameKeyFor` there.\n//\n// Module-scoped and LRU-bounded, like `programCache`/`textureCache`/the shader frame cache: it\n// SURVIVES a runtime `dispose()` (a remount/re-render re-uses the frames it already rendered) and\n// is never cleared except by eviction (or the test-only reset below).\n\n// Entry-count ceiling, matching the shader runtime's `STATIC_FRAME_CACHE_LIMIT`.\nconst STATIC_FRAME_CACHE_LIMIT = 64;\n// TOTAL backing-store ceiling, in pixels, across all live entries — a DELIBERATE divergence from the\n// shader cache, which bounds entry COUNT only. Particle canvases are grown by `pad` on every side and\n// a screen-filling ambient emitter is routinely 1500–2048px per edge, so 64 large entries would be\n// ~1GB of RGBA on exactly the low-end phones frozen mode exists for. 16M px ≈ 64MB of canvas backing.\nconst STATIC_FRAME_CACHE_PIXEL_LIMIT = 16 * 1024 * 1024;\n\n// Insertion-ordered (JS `Map`) = LRU order: a read re-inserts (see `getStaticParticleFrame`), so\n// `keys().next()` is always the least-recently-used entry.\nconst staticFrameCache = new Map<string, HTMLCanvasElement>();\n// Running sum of `width * height` over `staticFrameCache`, so the pixel ceiling costs no iteration.\nlet cachedPixels = 0;\n\n/** The BINDING-LIFETIME-CONSTANT half of a frozen frame's identity (see `particleStaticFrameKeyBase`). */\nexport interface ParticleStaticFrameKeyBase {\n /** The RAW `data-godot-particle-specs` attribute string — never a re-serialization of the parsed\n * config. A round-trip through `JSON.parse`/`JSON.stringify` would reorder keys and reformat\n * floats, so two nodes carrying byte-identical attributes could produce different keys. */\n specJson: string;\n /** The EFFECTIVE particle count — `amount` after the runtime's `particleMaxInstances` clamp. NOT\n * derivable from `specJson`: the clamp is a per-runtime option, and this cache is module-scoped,\n * so two runtimes configured with different caps share it. */\n count: number;\n /** `CanvasItemMaterial` blend mode (0 mix, 1 add). Part of the key because it changes the CACHED\n * BITMAP, not merely how that bitmap composites: additive is a whole second pass in\n * `./render-webgl.ts` (sum raw light into an FBO, then resolve the total to\n * `(light, peak-coverage)`), so the two modes put different pixels in the canvas for the same\n * spec. (The node ALSO carries `mix-blend-mode: plus-lighter` under an additive material — see\n * `../material.ts` — which is a page-level fact this cache never sees.) */\n blendMode: number;\n /** `cfg.seed`. A constant today, and included anyway: if per-node seeds are ever introduced they\n * must SPLIT the cache instead of silently aliasing one node's spray onto another's. */\n seed: number;\n /** Sprite texture identity. */\n textureUrl: string | null;\n /** Coverage-mask identity (`maskUrl`). Multiplies coverage, so it changes the pixels. */\n maskUrl: string | null;\n}\n\n/** The half that MOVES over a binding's life: canvas geometry and the (async) texture dimensions. */\nexport interface ParticleStaticFrameGeometry {\n /** Canvas backing-store size. EXACT (never quantized): it is geometry — which pixels the frame\n * covers — and the shader cache carries a precedent comment about a widened-background band bug\n * caused by quantizing exactly this kind of term. */\n width: number;\n height: number;\n /** The ratio `drawBinding` scales its particle geometry by. EXACT for the same reason: it\n * multiplies every position and sprite size, so a 1%-different ratio is a different picture.\n * (It is also not a jittery streamed value — it is `devicePixelRatio × renderScale`, or a pin.) */\n drawRatio: number;\n /** The LEFT and TOP canvas margins, in css px. EXACT: together they ARE the draw's origin offset\n * inside the canvas (`./runtime`'s `packBinding`), so two frames that differ in either are\n * different pictures. Both, not one symmetric `pad`, because the margin is directional — see\n * `./extents`; with `particleTravelExtents` off they are always equal and this key reduces to the\n * one it replaced with the number repeated. */\n padX: number;\n padY: number;\n /** Per-particle sprite size in px. EXACT (geometry). */\n frameW: number;\n frameH: number;\n /** The sprite texture's CURRENT dimensions — 1x1 while it is still the placeholder, its real size\n * once decoded (there is no attribute change to hang that transition off, which is why it lives\n * in the moving half). The runtime refuses to key an unloaded binding at all; this is the belt. */\n textureWidth: number;\n textureHeight: number;\n}\n\n// LENGTH-PREFIXED free-form strings. `specJson`, `textureUrl` and `maskUrl` are host-supplied and\n// may contain the `|`/`@`/`:` separators, so a plain join could let one field's content impersonate\n// the next field's — and an aliased key serves the WRONG bitmap, the one failure mode this cache\n// must not have. A length prefix makes each string self-delimiting.\nfunction tagged(value: string | null): string {\n return value === null ? \"-1:\" : `${value.length}:${value}`;\n}\n\n/**\n * The constant prefix of a frozen frame's key, computed ONCE per binding (the shader runtime's\n * `paramsKey` memo, same trick — see `ParticleBinding.staticKeyBase` in `./runtime`). Verbatim:\n *\n * `${len}:${specJson}|n${count}|b${blendMode}|s${seed}|t${len}:${textureUrl}|k${len}:${maskUrl}`\n */\nexport function particleStaticFrameKeyBase(\n base: ParticleStaticFrameKeyBase,\n): string {\n return (\n `${tagged(base.specJson)}|n${base.count}|b${base.blendMode}|s${base.seed}` +\n `|t${tagged(base.textureUrl)}|k${tagged(base.maskUrl)}`\n );\n}\n\n/**\n * The full identity of one frozen particle frame: the memoized base plus the geometry that moves.\n * Verbatim (the whole key, base expanded):\n *\n * `${len}:${specJson}|n${count}|b${blendMode}|s${seed}|t${len}:${textureUrl}|k${len}:${maskUrl}` +\n * `|${width}x${height}|r${drawRatio}|p${padX}x${padY}|f${frameW}x${frameH}|@${texW}x${texH}`\n *\n * NOTHING here is quantized. The shader key quantizes its params/modulate because those are\n * per-delta streamed floats that jitter; a particle system has no such term — its only free-form\n * input is the spec ATTRIBUTE STRING (already a stable string, and a change to it re-creates the\n * binding anyway), and everything else in this key is geometry, which the shader key keeps exact too.\n */\nexport function particleStaticFrameKey(\n base: string,\n geometry: ParticleStaticFrameGeometry,\n): string {\n return (\n `${base}|${geometry.width}x${geometry.height}|r${geometry.drawRatio}` +\n `|p${geometry.padX}x${geometry.padY}|f${geometry.frameW}x${geometry.frameH}` +\n `|@${geometry.textureWidth}x${geometry.textureHeight}`\n );\n}\n\n/** The cached frame for `key`, or undefined. A hit is bumped to the most-recently-used end. */\nexport function getStaticParticleFrame(\n key: string,\n): HTMLCanvasElement | undefined {\n const hit = staticFrameCache.get(key);\n if (!hit) return undefined;\n staticFrameCache.delete(key);\n staticFrameCache.set(key, hit); // LRU bump\n return hit;\n}\n\n/**\n * Snapshot a just-drawn particle canvas into the cache under `key`. The copy is a FRESH canvas, not\n * a reference to the node's own: the node redraws into its canvas on any later resize, which would\n * otherwise mutate the cached frame under every other node reading it.\n *\n * A no-op without a DOM, without a 2D context, or when one frame alone would blow the pixel budget\n * (never evict the whole cache to seat a single giant ambient emitter).\n */\nexport function storeStaticParticleFrame(\n key: string,\n source: HTMLCanvasElement,\n width: number,\n height: number,\n): void {\n if (typeof document === \"undefined\") return;\n if (width < 1 || height < 1) return;\n const pixels = width * height;\n if (pixels > STATIC_FRAME_CACHE_PIXEL_LIMIT) return;\n const canvas = document.createElement(\"canvas\");\n canvas.width = width;\n canvas.height = height;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n ctx.drawImage(source, 0, 0);\n const replaced = staticFrameCache.get(key);\n if (replaced) {\n cachedPixels -= replaced.width * replaced.height;\n // `Map.set` on an EXISTING key keeps its original insertion slot, so a re-store would leave the\n // freshest frame sitting at the LRU front — where the eviction loop below could drop it on the\n // very next store. Delete first so it is re-inserted as most-recently-used.\n staticFrameCache.delete(key);\n }\n staticFrameCache.set(key, canvas);\n cachedPixels += pixels;\n while (\n staticFrameCache.size > STATIC_FRAME_CACHE_LIMIT ||\n (cachedPixels > STATIC_FRAME_CACHE_PIXEL_LIMIT && staticFrameCache.size > 1)\n ) {\n const oldest = staticFrameCache.keys().next().value;\n if (oldest === undefined) break;\n const evicted = staticFrameCache.get(oldest);\n staticFrameCache.delete(oldest);\n if (evicted) cachedPixels -= evicted.width * evicted.height;\n }\n}\n\n/** TEST-ONLY: empty the cache so a test starts cold. */\nexport function __resetStaticParticleFrameCacheForTest(): void {\n staticFrameCache.clear();\n cachedPixels = 0;\n}\n\n/** TEST-ONLY: live entry count + total cached pixels, for the eviction tests. */\nexport function __staticParticleFrameCacheStatsForTest(): {\n entries: number;\n pixels: number;\n} {\n return { entries: staticFrameCache.size, pixels: cachedPixels };\n}\n","import {\n type ParticleInstancePackInput,\n packParticleInstances,\n} from \"@godot-scene-web/effects/particles\";\n// Live 2D particle runtime — the sibling of `../webgl/runtime.ts`. Walks the opted-in `[data-godot-particle-runtime]` nodes a\n// renderer mounted, builds a per-node overlay canvas in the self-layer, runs the\n// deterministic CPU simulation, and draws instanced quads via the SHARED WebGL2\n// context (blitting to each node's 2D canvas). A single loop steps + redraws all\n// systems and STOPS when every system is idle (so finished one-shot bursts cost ~0); under an FPS\n// cap it PARKS on a timer to the next cap boundary instead of arming a rAF per display frame (see\n// `../effects-loop-pacing`, and `effectsLoopPacing: \"raf\"` to restore the per-frame spin).\n// Returns a disposer; a no-op when WebGL2 is unavailable (the static `<span>` preview\n// then stays as the fallback).\n//\n// Sizing: a binding's canvas is placed and sized from its self-layer's content box, and it is that\n// MEASUREMENT, not the drawing, that dominated this runtime's main-thread cost — a `clientWidth`\n// right after the writes the same pass just made is a forced style+layout flush. Two options bound\n// it. `particleRectCache` caches each binding's box so only a CREATE reads (one contiguous batched\n// run per reconcile, never a per-binding read/write interleave), and `particleObserverSizing` removes\n// even that: a new binding's canvas stays OUT of the DOM until the shared ResizeObserver hands over\n// the first box it measured during the browser's own layout step. With both on (the default) the\n// create path forces no layout at all and `stats().boxReads` settles at 0. See `../types`.\n//\n// Frozen one-shots: static/frozen mode warms a system to a representative frame and parks it, which is right\n// for an emitter that runs forever and wrong for a BURST. A one-shot binding whose own active window\n// (`lifetime * (2 - explosiveness)`, the same law the sim ends a live burst by) has elapsed since this client\n// first saw it emitting stops being drawn — see `retireExpiredBurst`, kill switch\n// `staticParticleOneShotExpiry: false`.\n//\n// Occlusion: a binding under a `data-godot-effects-suspended` ancestor is SUSPENDED — the loop\n// skips its simulate + draw (state FROZEN, never reset) and it doesn't count as live, so a covered\n// subtree (a full-screen dialog over the scene) parks the loop instead of burning CPU on ambient\n// emitters nobody can see. See `../effects-suspend` for the full contract.\n//\n// Dormancy PARK: a suspended binding stops SIMULATING, but its `<canvas>` is an unconditionally\n// promoted compositor layer, so it kept costing a layer, a render surface and its GPU backing store\n// for as long as the node stayed mounted — measured as +340 net layers and a monotonic GPU-process\n// climb (148 → 276 MB) across a combat trace. So a suspended binding is also PARKED (see\n// `../shader-dormant`, whose contract this is the particle sibling of): its canvas is hidden, every\n// `sizeCanvas` it is owed is deferred to the wake, and a binding parked longer than\n// `DORMANT_DISPOSE_SECONDS` is disposed for real by ONE per-runtime sweep. Kill switch:\n// `particleDormant: false` (see `../types`).\n//\n// Image swap: a binding whose canvas has been observed to stand still is shown as an `<img>` of its\n// own frame instead (the canvas stays, hidden), which drops its compositor layer, its render\n// surface and its per-frame GPU fill. The MECHANISM is `../surface-image-swap` (generic, no\n// particles in it); the POLICY comes from the `staticParticleImages` option, which is OFF by\n// default and whose `true` means the QUIET-WINDOW gate — \"nothing has painted this canvas for a\n// while\" is the only evidence a system that is still SIMULATING can ever offer. Every path that\n// writes pixels into a binding's canvas reports a paint (`notePaint`), the CACHE-HIT BLIT included.\n// A paint that lands on the pristine path also carries the frame's KEY, which is what collapses N\n// twins onto one encode and lets a re-blit re-state the frame it is already showing instead of\n// thawing it — see `notePaint` for the whole contract and for the cases that must stay keyless.\n//\n// Freeze at mount: a binding the host names through `canFreezeSurface` may skip the simulation\n// entirely (`staticParticleFreezeAtMount`) — warmed once, drawn once, never stepped — and, when this\n// document has already encoded its frame, mount as an `<img>` with NO canvas context and NO backing\n// store at all. See `claimFrozenMount` for the zero-canvas path, `liveifyBinding` for how such a\n// surface takes its canvas back, and `donateStill` for how a departing binding banks the frame its\n// successors will claim.\n\nimport {\n createParticleState,\n InstanceBuffer,\n oneShotBurstSeconds,\n type ParticleSystemState,\n particlesAreLive,\n preprocessParticles,\n simulateParticles,\n staticOneShotExpired,\n warmStaticParticles,\n} from \"@godot-scene-web/effects/particles\";\nimport {\n reportUnsupportedRender,\n type UnsupportedRenderReporter,\n} from \"../diagnostics\";\nimport {\n createEffectsLoopPacer,\n type EffectsLoopPacing,\n} from \"../effects-loop-pacing\";\nimport { isEffectsSuspended } from \"../effects-suspend\";\nimport { ownSelfLayer } from \"../render-structure\";\nimport type { GodotHtmlRuntimeOptions } from \"../runtime-options\";\nimport { DORMANT_DISPOSE_SECONDS } from \"../shader-dormant\";\nimport {\n applySurfaceVisibility,\n claimStaticStill,\n createStaticImageSwapCounters,\n createStaticSurfaceSwapper,\n disposeStaticImage,\n hasStaticStill,\n liveStaticImageUrlCount,\n noteStaticFrame,\n noteStaticSurfaceWake,\n revertStaticImage,\n type StaticImageState,\n type StaticImageSwapCounters,\n type StaticSurfaceCapture,\n type StaticSurfaceOption,\n type StaticSurfacePolicy,\n type StaticSurfaceSwapper,\n staticStillPoolStats,\n} from \"../surface-image-swap\";\nimport type { GodotEffectRenderInfo } from \"../types\";\nimport {\n backingStoreSize,\n effectivePixelRatio,\n getShared,\n MAX_PINNED_BACKING_DIM,\n normalizeStaticPixelRatio,\n nowSeconds,\n onTextureLoaded,\n parseSurfacePixelRatio,\n performanceNow,\n SURFACE_PIXEL_RATIO_ATTR,\n} from \"../webgl/shared-gl\";\nimport {\n acquireWebgpuDevice,\n latchWebgpuFallbackReason,\n onWebgpuDeviceLost,\n peekWebgpuDevice,\n type WebgpuFallbackReason,\n type WebgpuShared,\n webgpuFallbackReason,\n} from \"../webgpu/device\";\nimport { canvasFromPremultipliedRgba } from \"../webgpu/still-capture\";\nimport {\n frameSize,\n type ParticleExtents,\n type ParticleLocalRect,\n parseLocalVisibleRect,\n particleCanvasExtents,\n symmetricCanvasExtents,\n visibleAllowance,\n} from \"./extents\";\nimport {\n createWebglParticleBackend,\n type ParticleDrawOptions,\n type ParticleRenderBackend,\n type ParticleSurface,\n type ParticleTextureHandle,\n} from \"./render-backend\";\nimport {\n createWebgpuParticleBackend,\n peekWebgpuParticleBackend,\n} from \"./render-webgpu\";\nimport { type ParticleSpecConfig, parseParticleSpecConfig } from \"./spec\";\nimport {\n getStaticParticleFrame,\n particleStaticFrameKey,\n particleStaticFrameKeyBase,\n storeStaticParticleFrame,\n} from \"./static-frame-cache\";\n\n/** The host attribute naming the part of a particle node's OWN local px space that can actually be\n * seen (`\"x,y,width,height\"`), so the canvas is grown to contain the spray's travel but no further\n * than the visible stage. Optional: absent ⇒ the margin is capped at `PAD_CAP` per side instead\n * (see `./extents`). Re-read on every reconcile for a KEPT binding, so a node that moves re-sizes\n * its canvas — it does NOT re-create the binding, which would restart the simulation. */\nconst VISIBLE_RECT_ATTR = \"data-godot-particle-visible-rect\";\n\n// The baked color-LUT texture for a system on the WebGL backend. It MOVED to `./render-backend`\n// (which is where the GL texture cache is consulted from now that texture resolution is a backend\n// question — the WebGPU peer bakes the same gradient under the same key), and is re-exported here\n// because that is where its consumers have always imported it from.\nexport { lutTextureFor } from \"./render-backend\";\n\n// How many BAKE DONORS one runtime may hold at once (see `donateStill`). Small on purpose: a donor\n// pins a canvas backing store and a GPU buffer for a readback nobody is waiting on, and the\n// population this serves collapses onto a handful of distinct keys — a fleet of 70 nodes measured at\n// 2 — so a deep queue would only be holding duplicates of frames already in flight.\nconst MAX_STILL_DONORS = 4;\n\n// The pure sizing law — how big the overlay canvas has to be, per side — lives in `./extents`.\n// Re-exported from here because that is where its consumers have always imported it from (as\n// `lutTextureFor` above is), and because the directional law that replaced the symmetric pad still\n// has to be able to compute the old number: it is the FLOOR the new one can never go below.\nexport {\n emissionExtentPad,\n emissionExtents,\n type ParticleExtents,\n type ParticleLocalRect,\n particleCanvasExtents,\n spriteExtentPad,\n travelExtents,\n visibleAllowance,\n} from \"./extents\";\n\ninterface ParticleBinding {\n /** The outer `[data-godot-particle-runtime]` node element (the reconcile key; carries the host's\n * node-level styles, e.g. an additive material's `mix-blend-mode` — see `parkBindingBlend`). */\n node: HTMLElement;\n selfLayer: HTMLElement;\n /** The overlay canvas element. Owned by the runtime (it sizes, places, mounts, hides and removes\n * it) and ALSO reachable through `surface` — the DOM half of it is renderer-agnostic, the\n * context on it is not. */\n canvas: HTMLCanvasElement;\n /** What the backend draws into (see `./render-backend`), or NULL while no backend has claimed the\n * canvas yet. A surface-less binding is skipped by the draw paths exactly like an unmounted one:\n * it has no context, so it can neither draw nor be frozen. */\n surface: ParticleSurface | null;\n /** ASYNC ENCODE SOURCE for the frozen-surface image swap, set ONLY on a binding whose canvas\n * cannot be read back (a WebGPU one — see `attachSurfaceSwap`). Produces a fresh 2D canvas of\n * this binding's current frame through the backend's capture hook; the swap module owns and\n * releases it. Absent on a 2D-backed binding, whose canvas the swap reads directly. See\n * `../surface-image-swap`'s `StaticImageSwapBinding.captureCanvas`. */\n captureCanvas?: () => Promise<StaticSurfaceCapture>;\n config: ParticleSpecConfig;\n state: ParticleSystemState;\n /** The sprite sheet, resolved by the BACKEND that will sample it (`resolveTextures`), or null —\n * for a system with no `textureUrl`, and for any binding whose backend has not been decided yet\n * (see `surface`). The runtime reads only the renderer-agnostic quartet off it: `width`/`height`\n * for the canvas pad, `loaded` for the frozen-frame cache gate, `listeners` for the redraw hook. */\n texture: ParticleTextureHandle | null;\n /** Baked `colorLut` ramp (shared + cached by spec key on the backend's cache), or null. */\n lut: ParticleTextureHandle | null;\n /** `maskUrl` coverage mask (shared image-texture cache), or null. A transparent 1x1 until it decodes. */\n mask: ParticleTextureHandle | null;\n buffer: InstanceBuffer;\n packing?: ReturnType<typeof bindingPackInput>;\n /** Static preview spans we hid; restored on dispose. */\n hiddenPreview: HTMLElement[];\n /** Whether `canvas` is IN THE DOM. False between `createBinding` and the binding's first\n * successful `sizeCanvas`, which is the moment it acquires a real box (see `mountBinding`).\n *\n * A canvas is mounted by its FIRST SIZING, never by its create, because an unsized canvas is a\n * 300x150 default box at the self-layer origin — a wrong picture, an unconditional compositor\n * layer and a raster, for a surface that cannot draw anything yet. Deferring the insert is what\n * lets `particleObserverSizing` take that first box from the shared ResizeObserver's initial\n * delivery instead of from a create-time `clientWidth` (see `readBoxInto`). The preview spans are\n * hidden at CREATE either way: hiding them is a pure write, and leaving them visible for the extra\n * frame would both flash a static preview under the arriving canvas and keep a parked (occluded)\n * binding's spans painting — the very cost the park exists to drop.\n *\n * A binding that never mounts never draws (the loop skips it) and never freezes (it can have no\n * paint), so nothing downstream has to special-case it. */\n mounted: boolean;\n textureDisposers: Array<() => void>;\n /** Per-side canvas margin (css px) so a system's sprites and its TRAVEL aren't clipped to the\n * (usually zero-size) box — see `./extents`. `left`/`top` are also the draw's origin offset\n * (`packBinding`), which is why the two are read individually all over this file rather than as\n * one symmetric `pad`. With `particleTravelExtents` off all four are the same number, and every\n * geometry this runtime computes is byte-identical to the pre-directional one. */\n pad: ParticleExtents;\n /** The part of this node's own local px space that can be SEEN, as the host last said\n * (`VISIBLE_RECT_ATTR`), or null when it said nothing. Caps the margin above, so a burst that\n * travels off-screen allocates only the on-screen part of its flight. */\n visibleRect: ParticleLocalRect | null;\n /** …and the raw attribute string it was parsed from, so a reconcile can tell \"unchanged\" from\n * \"moved\" with one string compare and no parse. Null while the option is off (nothing is read). */\n visibleRectAttr: string | null;\n /** Per-binding backing-density MULTIPLIER (see `SURFACE_PIXEL_RATIO_ATTR` in `../webgl/shared-gl`):\n * how much bigger this surface is on screen than its own CSS box, as the host states it. Folded\n * into the density term by `measureCanvasGeometry`, so it applies to the live ratio, the frozen\n * pin and a freeze-at-mount claim's frame key alike. Always a finite positive number — an absent\n * or malformed attribute resolves to exactly `1`, the un-stamped, byte-identical case. */\n pixelRatioScale: number;\n /** …and the raw attribute string, compared per reconcile the way `visibleRectAttr` is: one string\n * compare for a node that did not move, no parse. */\n pixelRatioAttr: string | null;\n /** Last-measured self-layer content-box size in CSS px, and whether it has been measured at all.\n * Seeded by the ONE create-time layout read (`reconcile`'s measure pass) and kept current by the\n * shared ResizeObserver's `contentRect`, so every later `syncCanvasSize` — a fleet re-size\n * (`setRenderScale`, a pin change, a frozen-mode flip) or a texture-load re-pad, none of which\n * move the element box — reuses it instead of forcing another clientWidth/clientHeight layout\n * flush. The shader runtime's `NodeBinding.boxW`/`boxH` (`../webgl/runtime`), with one\n * difference: \"have we measured?\" lives in its OWN flag rather than in `boxW > 0`, because a\n * particle self-layer legitimately measures 0 (a never-laid-out or hidden subtree) and reading\n * that as \"unmeasured\" would leave exactly those bindings re-reading forever. Written but never\n * consulted while `particleRectCache` is false. */\n boxW: number;\n boxH: number;\n boxMeasured: boolean;\n /** The backing-store ratio this binding's canvas was LAST sized at — the live\n * `devicePixelRatio × renderScale`, the frozen-mode pin (`staticParticlePixelRatio`), or, when the\n * pinned size hit `MAX_PINNED_BACKING_DIM`, the reduced ratio that was actually allocated. The\n * draw scales its geometry by exactly this, so sprites can never be sized for a canvas the\n * binding did not get. Written by `syncCanvasSize`. */\n drawRatio: number;\n /** The raw `data-godot-particle-specs` string — the reconcile key for change detection. */\n signature: string;\n /** The binding-lifetime-constant half of the static-frame cache key, PRECOMPUTED here instead of\n * re-derived on every static tick (the shader runtime's `paramsKey` memo, same trick). Every term\n * in it is fixed for as long as the binding exists: a change to the spec attribute — the only\n * input that can move — RE-CREATES the binding (see `reconcile`), so this is recomputed exactly\n * when the attribute string changes and never otherwise. See `./static-frame-cache`. */\n staticKeyBase: string;\n /** Static/frozen mode: whether this binding has already been warmed + drawn since the last freeze. A fresh\n * binding (or one re-created by a spec/epoch change) starts false, so the static loop re-warms + re-freezes it. */\n frozen: boolean;\n /** Whether this binding's simulation state is still the DETERMINISTIC function of its\n * (config, count) that `createBinding` left behind — i.e. the live loop has never stepped it. Only\n * a pristine binding may be keyed into the static-frame cache: `warmStaticParticles` is pure, but\n * it is pure OF THE STATE IT IS GIVEN, and a system frozen mid-flight (live runtime, then\n * `setStaticParticles(true)`) warms from whatever phase it was in — which two nodes sharing a spec\n * do NOT share. Cleared for good the first time `simulateParticles` steps it. */\n pristine: boolean;\n /** A static tick served this binding's frozen frame from the cache and therefore SKIPPED its own\n * `warmStaticParticles`. The skipped warm is owed: without it, leaving frozen mode would resume\n * the simulation from the un-warmed post-create state instead of the mid-flight state it would\n * have had, i.e. a visible pop. `setStaticParticles(false)` pays it back before resuming. */\n pendingWarm: boolean;\n /** Occlusion suspend (see `../effects-suspend`): the node sits under a\n * `data-godot-effects-suspended` ancestor → the loop skips simulate + draw for it and it does\n * NOT keep the loop alive. The sim state is FROZEN, not reset, so a resume continues where it\n * left off. Recomputed on every `reconcile()`, never polled per frame. */\n suspended: boolean;\n /** A repaint of this canvas is OWED, so its pixels are NOT the frame anything may freeze: the\n * backing store was re-allocated (which CLEARS it), the binding is suspended and its resume\n * redraw has not run, or it has never been drawn at all. Read only by the surface image swap —\n * which refuses to freeze a dirty surface and reverts a swapped one — and cleared by `notePaint`.\n * The shader runtime's `NodeBinding.dirty`, and the same way it expresses occlusion to the swap.\n * Inert while the swap is off (nothing else in this runtime reads it). */\n dirty: boolean;\n /** PARKED (the shader runtime's `data-godot-shader-dormant` state, see `../shader-dormant`):\n * `suspended` AND the park is enabled (`particleDormant`, the default). A parked binding keeps\n * its object identity and its simulation state, but its canvas is HIDDEN — which is what drops\n * the compositor layer, the render surface and the backing store an occluded canvas would\n * otherwise hold for as long as the node stays mounted — and every `sizeCanvas` it is owed is\n * deferred (`canvasSyncDeferred`). Recomputed on every `reconcile()`, from the same\n * attribute-only read that recomputes `suspended`; never polled per frame.\n *\n * DISPLAY OWNERSHIP (the delicate part). This runtime NEVER writes `canvas.style.display`\n * itself: the park is published by setting this flag and calling the swap module's\n * `applySurfaceVisibility`, which stays the single writer and composes the two states — parked\n * hides BOTH surfaces (canvas and any stand-in `<img>`), swapped-and-awake hides the canvas\n * only, and neither restores the `display` the HOST left rather than a blanket `\"\"`. That is\n * exactly how `../webgl/runtime`'s `syncDormant` arbitrates, and it is why a park can never\n * strand an `<img>` over a hidden canvas, nor leave a canvas hidden after the wake. The one\n * direct write is at CREATE (`createBinding`), before any swap state exists — the swapper's\n * `attach` adopts that hide, as it does for a shader binding born dormant. */\n dormant: boolean;\n /** A `sizeCanvas` that was skipped while parked; run ONCE on wake (`../webgl/runtime`'s\n * `canvasSyncDeferred`). What is deferred, with WS-1's box cache in place, is mostly the WRITE\n * side — four inline style writes plus, whenever the density really moved, a `canvas.width`\n * assignment that RE-ALLOCATES (and clears) the backing store of a canvas nobody can see, and\n * with the image swap on a revert of its stand-in. However many of those pile up while parked (a\n * fleet `setRenderScale`, a pin change, a frozen-mode flip, an observer delivery), the wake pays\n * for one. It defers a READ too in the two cases where the box is not cached: a binding BORN\n * parked (never measured — the shader runtime's original motivation) and `particleRectCache:\n * false`. */\n canvasSyncDeferred: boolean;\n /** Ordinal of the moment this binding parked, from the runtime's monotonic counter (0 while\n * awake). The park-expiry sweep compares ordinals rather than a wall clock, so it needs no\n * per-binding timer and no clock reading (`../webgl/runtime`'s `dormantSeq`). */\n dormantSeq: number;\n /** Frozen-surface image-swap state (see `../surface-image-swap`), or null when the runtime's\n * `staticParticleImages` option is off — the default — in which case every swap call site is a\n * no-op and the binding takes exactly the path it took before the swap existed. */\n staticImage: StaticImageState | null;\n /** FROZEN-MODE one-shot expiry (`options.staticParticleOneShotExpiry`, see `retireExpiredBurst`): the\n * clock reading at which THIS CLIENT first saw this binding as an emitting one-shot, or null when it is\n * not one (a continuous emitter, or a one-shot the host says is not emitting).\n *\n * Set at CREATE and never moved, which is exactly right: a binding is re-created whenever its spec\n * attribute changes, and a re-triggered burst IS a spec change (the host bumps an epoch — see\n * `createParticleRuntime`), so every burst gets its own clock. The client cannot know when the GAME\n * started the burst; one full active window from first sight is what the burst itself would do. */\n emitSeenAt: number | null;\n /** The expired-burst blank has been painted (see `retireExpiredBurst`), so the frozen loop leaves this\n * binding alone. Never unset: a burst that ended does not restart — a re-trigger is a new binding. */\n burstCleared: boolean;\n /** Parked-blend neutralization (`options.parkStaticParticleBlend`): the node's inline\n * `mix-blend-mode` saved when this binding was parked in static mode (restored verbatim on\n * unpark/dispose), or null while not neutralized. The FIRST-saved value wins across re-asserts,\n * so a host style writer re-imposing the blend mid-park is healed without forgetting the\n * restore value. */\n parkedBlend: string | null;\n /** FREEZE AT MOUNT (`staticParticleFreezeAtMount`): the host's `canFreezeSurface` predicate has\n * been asked about this binding. Asked ONCE, at its first sizing (`claimFrozenMount`), and never\n * again — the answer is a property of the node, and re-asking it per sizing would let a host\n * change a binding's kind underneath a swap that is already standing on it. Always false while\n * the option is off, in which case nothing below it can ever be true either. */\n freezeDecided: boolean;\n /** …and the answer: this binding is warmed once, drawn once and NEVER simulated. The live loop\n * skips it before it clears `pristine` and it does not keep the loop alive, so a fleet of them\n * costs nothing per frame in a runtime that is otherwise live. Permanent for the binding's life:\n * a re-triggered system arrives as a spec change, i.e. a new binding, which decides again. */\n freezeAtMount: boolean;\n /** …and the strong form: this binding's canvas is in the DOM carrying its CSS box and NOTHING\n * else — no context, no backing store, never painted — because the frame it would have drawn was\n * already encoded in this document and an `<img>` of it stands in (`claimStaticStill`). It owes a\n * warm (`pendingWarm`) and a draw, both paid by `liveifyBinding` the moment the swap can no\n * longer vouch for the stand-in. Implies `freezeAtMount` and `surface === null`. */\n stillMounted: boolean;\n}\n\n// --- parked-blend neutralization (see `parkStaticParticleBlend` in types.ts) --------------------\n//\n// WHY: every element with a non-normal `mix-blend-mode` is a STANDING compositor blend render\n// surface — one offscreen render pass per composited frame — even when nothing under it ever\n// changes. In `staticParticles` mode the canvas is drawn once and parked, yet an additive VFX\n// node's `plus-lighter` (stamped by `material.ts`, or by a host's own style pipeline) kept that\n// per-frame pass alive; on a GPU-bound phone the parked particle fleet owned MOST of the scene's\n// blend surfaces. The canvas doesn't need the node blend for correctness: additive systems resolve\n// to source-over-complete pixels inside the canvas (render-webgl.ts resolve pass). So while parked,\n// force the node's inline blend to `normal`; restore the saved value the moment the binding leaves\n// the parked world (live resume, dispose). Idempotent + re-assertable: a re-park after a host\n// rewrite overwrites back to `normal` but keeps the ORIGINAL saved value.\nfunction parkBindingBlend(binding: ParticleBinding): void {\n const style = binding.node.style;\n const current = style.mixBlendMode;\n if (binding.parkedBlend === null) {\n if (current === \"\" || current === \"normal\") return; // nothing to neutralize\n binding.parkedBlend = current;\n } else if (current === \"normal\") {\n return; // already parked and untouched\n }\n style.mixBlendMode = \"normal\";\n}\n\nfunction unparkBindingBlend(binding: ParticleBinding): void {\n if (binding.parkedBlend === null) return;\n binding.node.style.mixBlendMode = binding.parkedBlend;\n binding.parkedBlend = null;\n}\n\n/** OPT-IN per-frame cost attribution for ONE particle runtime (`effectsProfiling`, see `../types`),\n * read from `stats().profile`. NULL when the option is off — see `ParticleRuntimeStats.profile`.\n *\n * WHY IT EXISTS. A live tick's wall clock alone says nothing about what to DO: \"9 ms/frame\" is the\n * same number whether the CPU integrator is chewing through 2000 particles × 4 sub-steps, the\n * instance buffer is being rebuilt per frame, or the GL→2D blit is fill-bound at\n * devicePixelRatio 3. Those have opposite fixes (`particleFps`/`amount`/`staticParticles` vs\n * `renderScale`), so the tick is split into the four buckets below and each carries its own WORK\n * counter — a millisecond total is only interpretable next to the work that produced it.\n *\n * Counters are monotonic and never reset (the `ParticleRuntimeStats` contract): a benchmark\n * snapshots the object, runs its window, and diffs. Times are `performance.now()` deltas in ms,\n * summed — wall clock on the main thread, so an interrupted frame charges its interruption to\n * whichever bucket was open. */\nexport interface ParticleProfile {\n /** LIVE ticks that simulated + drew at least one binding. The frozen (`staticParticles`) path\n * books NONE: it draws once and parks the loop, so a frozen runtime reports `ticks: 0` and every\n * other field 0 — which is the right report for a mode whose whole point is that it has no\n * per-frame cost. A deferred tick (the FPS cap re-arming without work) books none either. */\n ticks: number;\n /** Bindings simulated + drawn, summed across those ticks. `bindings / ticks` is the live system\n * count the frame really paid for — suspended, parked and unmounted bindings are skipped by the\n * loop and never counted. */\n bindings: number;\n /** Fixed sub-steps `simulateParticles` executed. THE sim work unit, and deliberately not the tick\n * count: the sim runs at `fixed_fps` (30 by default) regardless of display rate, so a 60Hz device\n * runs ~0.5 steps per tick per binding and a stalled frame runs several. `simMs / simSteps` is\n * therefore the only stable cost-per-unit, and a `simSteps` far from `bindings` is the proof that\n * the sim rate is decoupled from the display rate. */\n simSteps: number;\n /** Instances pushed into the GL instance buffer ≈ live particles actually drawn (a dead or\n * fully-transparent particle is skipped by the build loop). The denominator for both `buildMs`\n * and `glMs`, and the number to compare against `particleMaxInstances`. */\n instances: number;\n /** CPU simulation: `simulateParticles` (integrate + emit + curve sampling). The bucket the\n * mid-range-phone suspicion points at. */\n simMs: number;\n /** Instance-buffer BUILD: the per-particle push loop in `drawBinding` that turns simulation state\n * into the interleaved float array. Separate from `simMs` because it is a different fix — it\n * scales with LIVE particles, not with sub-steps. */\n buildMs: number;\n /** GL SUBMIT: `drawParticles` — uniform writes, the buffer upload and the instanced draw call.\n * SUBMIT ONLY: the GPU executes asynchronously, so this is main-thread issue cost, never GPU\n * time. A GPU-bound frame shows up as back-pressure in `blitMs` (the readback-shaped\n * `drawImage`), not here. */\n glMs: number;\n /** GL→2D BLIT: the binding canvas's `clearRect` plus the `drawImage` that copies the shared GL\n * canvas onto it. Pure fill cost, so it scales with BACKING-STORE AREA (`renderScale`,\n * `devicePixelRatio`, the sprite `pad`) and not with particle count — which is exactly the\n * distinction a \"particles are slow\" report cannot make without this split. */\n blitMs: number;\n}\n\n/** A zeroed `ParticleProfile`. Allocated ONCE per runtime, at create, and only when\n * `effectsProfiling` is on; the live paths then mutate it in place, so profiling adds no per-frame\n * allocation to measure. */\nfunction createParticleProfile(): ParticleProfile {\n return {\n ticks: 0,\n bindings: 0,\n simSteps: 0,\n instances: 0,\n simMs: 0,\n buildMs: 0,\n glMs: 0,\n blitMs: 0,\n };\n}\n\n/** Live, monotonically-increasing counters for ONE particle runtime (see `ParticleRuntime.stats`).\n * Same contract as the shader runtime's `WebglShaderRuntimeStats`: plain `++` writes, never reset,\n * the SAME object returned on every `stats()` call (snapshot to diff). */\nexport interface ParticleRuntimeStats extends StaticImageSwapCounters {\n /** Actual instanced GL draws of a particle-system frame (`drawBinding` reaching `drawParticles`). */\n draws: number;\n /** Frozen-mode static-frame cache hits: the warm AND the instanced draw were both skipped and a\n * cached canvas blitted instead (see `./static-frame-cache`). The shader runtime's `cacheHits`\n * sibling. A fleet of N identical frozen systems should settle at 1 `draws` + (N-1) `cacheHits`. */\n cacheHits: number;\n /** `syncCanvasSize` calls that sized a binding at the PINNED static ratio\n * (`staticParticlePixelRatio`) instead of `devicePixelRatio × renderScale` — the shader runtime's\n * `pinnedCanvasSyncs` sibling, and the same purpose: a device probe reads it to confirm the pin\n * is really in force (0 = option unset, or frozen mode never entered). */\n pinnedCanvasSyncs: number;\n /** Self-layer box reads: `selfLayer.clientWidth`/`clientHeight`, i.e. the layout this runtime\n * forces. Booked by the create-time measure pass and by any `syncCanvasSize` that had neither a\n * ResizeObserver `contentRect` nor a cached box. With the cache on (`particleRectCache`, the\n * default) it settles at exactly one per binding CREATED and zero for everything after — fleet\n * re-sizes, texture-load re-pads and observer deliveries are all reflow-free — so a device probe\n * can confirm that live. It counts READS, not flushes: the create-time reads are batched into one\n * contiguous run, so N new bindings book N reads and cost ONE forced layout flush.\n *\n * With `particleObserverSizing` also on (the default) even that per-create read is gone and this\n * settles at **0**: a new binding's first box arrives from the shared ResizeObserver, off the main\n * path. Any read left standing is therefore a real signal — a wake whose observation never landed,\n * or the mount backstop firing on an engine that does not deliver 0x0 initial observations. */\n boxReads: number;\n /** COUNTER. Bindings PARKED (canvas hidden) because their subtree went suspended — a create that\n * was born parked included. Monotonic: a binding that parks, wakes and parks again counts twice.\n * 0 means the park never engaged (nothing suspended, or `particleDormant: false`). */\n dormantParks: number;\n /** COUNTER. Parked bindings woken again (the resume half, so a probe can tell \"parked and stayed\n * parked\" from \"flapped\"). */\n dormantWakes: number;\n /** COUNTER. Bindings the expiry sweep DISPOSED for real after ~`DORMANT_DISPOSE_SECONDS` parked\n * (canvas removed from the DOM, GL buffer released) rather than holding them forever. */\n dormantDisposes: number;\n /** GAUGE — bindings parked RIGHT NOW, sampled on each `stats()` read (the `staticImagesLive`\n * contract exactly, and re-derived from the binding set rather than trusted incrementally). This\n * is the \"is the park actually engaged?\" measurement a device probe reads — `28/33` — which the\n * monotonic counters above cannot answer. */\n dormantLive: number;\n /** Per-frame cost attribution (see `ParticleProfile`), or NULL when `effectsProfiling` is off —\n * which is the default, and the no-op handle always. NULL rather than a zeroed object ON PURPOSE:\n * a bench that read `simMs: 0` out of an un-instrumented runtime would report \"the simulation is\n * free\" when the truth is \"nobody measured\". The same object every `stats()` call, mutated in\n * place by the live tick. */\n profile: ParticleProfile | null;\n /** GAUGE — which renderer this runtime's bindings are drawing through RIGHT NOW, re-derived on\n * each `stats()` read (a runtime can change backend mid-life, in one direction: a WebGPU device\n * that is lost is rebuilt on WebGL).\n *\n * `\"pending\"` is a real state, not a transient to be waited out politely: with\n * `effectsRenderer: \"auto\"`/`\"webgpu\"` on a browser that HAS `navigator.gpu`, the device arrives\n * from a promise, and until it does the bindings exist, are sized and are mounted but have no\n * surface and draw nothing. `\"none\"` is the no-op handle (no WebGL2, or particles disabled). */\n renderer: \"pending\" | \"webgpu\" | \"webgl\" | \"none\";\n /** COUNTER. Times this runtime adopted WebGL after being asked for `\"auto\"`/`\"webgpu\"` — the\n * SYNCHRONOUS \"this browser has no navigator.gpu\" decline included, which is the common case and\n * the reason a plain WebGL page reports 1 here rather than 0. Stays 0 for `effectsRenderer:\n * \"webgl\"` (nothing was ever asked for) and for a runtime that adopted WebGPU and kept it. */\n webgpuFallbacks: number;\n /** The FIRST reason this runtime declined WebGPU (later ones cannot un-explain it), or null while\n * it never has. THE diagnostic for a silent fallback: `renderer: \"webgl\"` under\n * `effectsRenderer: \"webgpu\"` says something went wrong, and only this says what. */\n webgpuFallbackReason: WebgpuFallbackReason | null;\n /** COUNTER — `queue.submit` calls this runtime's WebGPU backend has made, sampled on read. ONE per\n * tick that drew anything, whatever the binding count: that batching is the measured win (S7), so\n * a probe that finds it climbing with N systems has found the win being given back. 0 on WebGL,\n * where the question is meaningless. */\n webgpuSubmits: number;\n /** GAUGE — `device.lost` resolutions seen by the page-wide device (see `../webgpu/device`). A lost\n * device stops producing frames, so a non-zero value here next to `renderer: \"webgl\"` is the\n * device-loss rebuild having happened. */\n webgpuDeviceLosses: number;\n /** GAUGE — `uncapturederror` events on the page-wide device. Non-zero means a frame was silently\n * WRONG: WebGPU reports most command-level mistakes this way and nothing else says so. */\n webgpuErrors: number;\n /** GAUGE — BAKE DONORS this runtime is holding right now (see `donateStill`): bindings whose node\n * is gone but whose surface is kept alive, out of the DOM, only long enough to encode the frame\n * their successors will claim. Sampled on each `stats()` read. A number pinned at the bound means\n * bakes are not draining — check `staticStillDonorBakes` against `staticStillDonorsDropped`. */\n staticStillDonors: number;\n /** COUNTER. Donor bakes that PUBLISHED, i.e. banked a frame nothing had encoded yet. Each one is a\n * key the next binding to reach it can claim for free. */\n staticStillDonorBakes: number;\n /** COUNTER. Donors released WITHOUT publishing — evicted by the donor bound, or dropped at runtime\n * teardown. A donor is speculative work by construction, so these are not failures; a run where\n * they dominate `staticStillDonorBakes` means the bound is too small for the scene's churn, or\n * that retention (`encode.stillCacheBytes`) is off and every bake is refused. */\n staticStillDonorsDropped: number;\n}\n\n/** A zeroed `ParticleRuntimeStats` (the swap counters included — see `../surface-image-swap`,\n * whose `staticImageSwaps`/`…Reverts`/`…Encodes` a device probe reads to confirm the mechanism,\n * `staticImagesLive` to see how much of the set is engaged, and `staticImageUrlsLive` to confirm\n * it does not leak). */\nfunction createParticleRuntimeStats(): ParticleRuntimeStats {\n return {\n draws: 0,\n cacheHits: 0,\n pinnedCanvasSyncs: 0,\n boxReads: 0,\n dormantParks: 0,\n dormantWakes: 0,\n dormantDisposes: 0,\n dormantLive: 0,\n // OFF unless `createEngine` swaps in a real profile: the never-measured state, and the only one\n // a no-op handle can ever report.\n profile: null,\n // \"none\" is the no-op handle's permanent answer; a real runtime overwrites this on its first\n // `stats()` read (and the gate has usually settled it before anyone can look).\n renderer: \"none\",\n webgpuFallbacks: 0,\n webgpuFallbackReason: null,\n webgpuSubmits: 0,\n webgpuDeviceLosses: 0,\n webgpuErrors: 0,\n staticStillDonors: 0,\n staticStillDonorBakes: 0,\n staticStillDonorsDropped: 0,\n ...createStaticImageSwapCounters(),\n };\n}\n\n// The per-runtime render plumbing (the renderer seam, one offscreen WebGL2 context for the texture\n// cache, the instance cap). Null when WebGL2/particles are unavailable.\ninterface ParticleEngine {\n /** WHERE the pixels come from (see `./render-backend`). Every draw, clear, surface create/dispose,\n * texture resolution and the sizing law's own ceiling go through it; nothing else in this runtime\n * knows the renderer.\n *\n * NULL means PENDING: the runtime asked for WebGPU and the device has not arrived yet (see the\n * async gate in `createParticleRuntime`). Bindings are still created, sized and mounted in that\n * state — they simply have no surface and no textures, and every draw path skips them exactly as\n * it skips an unmounted one. It is never null again once a backend has been adopted. */\n backend: ParticleRenderBackend | null;\n /** The WebGL backend, built at create and kept for the runtime's whole life whatever `backend`\n * currently is. It is the FALLBACK, and a fallback that had to be constructed at the moment it\n * was needed would be a second way to fail — on the device-loss path, which is already the worst\n * moment to discover that the shared GL context cannot be had either. */\n glBackend: ParticleRenderBackend;\n maxInstances: number;\n /** Backing-store pixel ratio for LIVE bindings (devicePixelRatio × clamped renderScale) — the\n * low-end resolution knob. */\n pixelRatio: number;\n /** OPT-IN pinned backing ratio for FROZEN bindings (see `staticParticlePixelRatio`), or undefined\n * = not pinned (every binding keeps sizing at `pixelRatio`, exactly as before the option existed). */\n staticPixelRatio: number | undefined;\n /** Whether the runtime is in frozen (`staticParticles`) mode right now — the OTHER half of the pin\n * condition, and the loop's `isStatic`. Lives on the engine because the sizing + draw paths are\n * free functions that already carry it. */\n staticMode: boolean;\n /** Whether a binding's measured box may be REUSED (`particleRectCache`, default true). False is\n * the kill switch: `syncCanvasSize` re-reads `clientWidth`/`clientHeight` on every call and\n * `reconcile` runs no measure pass, i.e. exactly the create-then-size interleave this runtime had\n * before the cache existed. Read once at create — a code-path selector, not a live knob. */\n rectCache: boolean;\n /** Whether a NEW binding takes its first box from the shared ResizeObserver's INITIAL delivery\n * instead of a create-time layout read (`particleObserverSizing`, default true). On: `reconcile`\n * measures nothing for a create, the canvas stays out of the DOM until that delivery lands, and\n * `boxReads` settles at 0 — the create path forces no layout at all. Off is the kill switch: the\n * measure pass reads every new binding exactly as it did before, i.e. WS-1's one-read-per-create\n * floor. Requires BOTH `rectCache` (the mechanism that lets a `contentRect` stand in for a read)\n * and a real `ResizeObserver`, so a jsdom/SSR environment always takes the read path. Read once at\n * create — a code-path selector, not a live knob. */\n observerSizing: boolean;\n /** Whether a SUSPENDED binding is also PARKED (`particleDormant`, default true): canvas hidden,\n * `sizeCanvas` deferred, disposed after `DORMANT_DISPOSE_SECONDS`. False is the kill switch —\n * `dormant` then stays false for every binding, nothing ever writes a canvas's `display`, and\n * suspension means exactly what it meant before the park existed. Read once at create — a\n * code-path selector, not a live knob. */\n parkDormant: boolean;\n /** Whether the canvas margin is sized DIRECTIONALLY from where the particles actually travel\n * (`particleTravelExtents`, default true — see `./extents`), capped by the host's per-node visible\n * rect. False is the kill switch: the margin is the symmetric `spriteExtentPad +\n * emissionExtentPad` this had before, `VISIBLE_RECT_ATTR` is never read, and every canvas geometry\n * is byte-identical to the pre-directional one. Read once at create — a code-path selector, not a\n * live knob. */\n travelExtents: boolean;\n /** Whether a FROZEN one-shot stops being drawn once its own burst window has elapsed\n * (`staticParticleOneShotExpiry`, default true — see `retireExpiredBurst`). False is the kill switch:\n * `emitSeenAt` stays null for every binding, nothing is ever retired, and a frozen one-shot's warmed frame\n * is parked forever exactly as it was before this existed. Read once at create — a code-path selector, not a\n * live knob. */\n oneShotExpiry: boolean;\n /** Whether a binding the host names may be FROZEN AT MOUNT (`staticParticleFreezeAtMount`, default\n * false): warmed once, drawn once, never simulated — and, where its frame is already encoded,\n * mounted as an `<img>` over a canvas that never gets a context (see `claimFrozenMount`). False is\n * not a kill switch but the absence of the mechanism: `freezeDecided` stays false for every\n * binding, `createBinding` claims its surface exactly as it always did, and no code path below\n * this can be reached. Read once at create — a code-path selector, not a live knob. */\n freezeAtMount: boolean;\n /** The host's `canFreezeSurface` veto, lifted OFF the swap policy (`staticParticleImages`) so the\n * freeze-at-mount decision consults the same predicate the swap's own gate does. ONE predicate,\n * two mechanisms: a host that named which of its surfaces may be frozen must not have to name\n * them twice, and a binding that mounted frozen must not then be refused a stand-in by the gate.\n * Null = no predicate, which the swap reads as \"no veto\" and so does this — with\n * `freezeAtMount` on and no predicate, EVERY binding is frozen at mount. */\n canFreezeSurface:\n | ((node: HTMLElement, canvas: HTMLCanvasElement) => boolean)\n | null;\n /** Instrumentation counters (purely observational — no render decision reads them). */\n stats: ParticleRuntimeStats;\n /** Per-frame cost attribution (see `ParticleProfile`), or NULL when `effectsProfiling` is off.\n * Held HERE, next to the hot paths that write it, so a bracket costs one field read and one null\n * check — the whole reason an off runtime can carry the instrumentation for free. The same object\n * `stats.profile` exposes; it is mutated in place and never replaced. Read once at create — a\n * code-path selector, not a live knob. */\n profile: ParticleProfile | null;\n /** Optional per-binding render notification (see `GodotHtmlRuntimeOptions.onBindingRendered`):\n * fired from `notePaint`, i.e. from every path that WRITES this binding's canvas — the instanced\n * draw, the frozen cache-hit blit, and the clears that end a burst — and never from the paths that\n * write nothing (no surface, zero-size, a parked or claimed binding). Absent ⇒ byte-identical\n * behavior. */\n onBindingRendered?: (\n node: HTMLElement,\n canvas: HTMLCanvasElement,\n info: GodotEffectRenderInfo,\n ) => void;\n}\n\n/**\n * What a particle binding reports as `GodotEffectRenderInfo`. Three of the four\n * fields are CONSTANT for every particle system there is; only `staticKey`\n * varies, so this builds one small object per paint around those constants.\n *\n * `blend: \"mix\"` is a statement about the CANVAS, not about the system. A Godot\n * additive particle material really is additive, and the renderer resolves that\n * INSIDE this binding's own canvas (see `./render-webgl`'s accumulator pass); what\n * comes out is a finished premultiplied image that composites over whatever is\n * behind it source-over, exactly like a mix-mode one. A consumer that read the\n * material's mode off the spec and composited the canvas additively would apply\n * the mode twice.\n *\n * The screen flags are false because a particle system has no fragment stage of\n * its own to read SCREEN_TEXTURE/SCREEN_UV with.\n *\n * A FRESH OBJECT PER FIRING, not one shared mutable record: a consumer is entitled\n * to KEEP the info it was handed (couch-coop's fx registry stores it on the\n * surface), and a shared one would silently re-point every stored reference at the\n * last binding to paint. It is a four-field literal behind a callback that only\n * fires when a canvas was actually written, which is the same trade the shader\n * runtime's `renderInfoOf` already makes.\n */\nfunction particleRenderInfo(staticKey: string | null): GodotEffectRenderInfo {\n return {\n usesScreenTexture: false,\n usesScreenUv: false,\n blend: \"mix\",\n staticKey,\n };\n}\n\nfunction createEngine(options: GodotHtmlRuntimeOptions): ParticleEngine | null {\n const sharedGl = getShared();\n if (!sharedGl || !options.enableParticles) return null;\n // No renderable backend (the instanced program did not compile/link) ⇒ no runtime at all: the\n // caller returns the no-op handle and every opted-in node stays on its static preview.\n //\n // A WORKING WEBGL BACKEND IS REQUIRED EVEN FOR A WEBGPU RUNTIME. It is what every failure path\n // adopts — no adapter, a rejected pipeline, a device lost mid-run — so a runtime that could not\n // build one has no fallback to fall back TO, and \"WebGPU or nothing\" is not a trade this package\n // makes. The cost is one shared GL context + one program compile on a page that may never use\n // them; the alternative is discovering at device-loss time that there is nowhere to go.\n const glBackend = createWebglParticleBackend(sharedGl);\n if (!glBackend) return null;\n // ONE profile object for the runtime's whole life, or null forever (see `ParticleProfile`). The\n // stats object publishes the SAME reference, so `stats().profile` needs no per-read plumbing and a\n // bench may hold onto it across ticks.\n const profile =\n options.effectsProfiling === true ? createParticleProfile() : null;\n const stats = createParticleRuntimeStats();\n stats.profile = profile;\n return {\n // Left PENDING here on purpose: which backend this runtime adopts is the async gate's decision\n // (see `createParticleRuntime`), and for the common case — no `navigator.gpu` — it is made\n // synchronously, before anything can observe the null.\n backend: null,\n glBackend,\n maxInstances: Math.max(1, options.particleMaxInstances ?? 2048),\n pixelRatio: effectivePixelRatio(options.renderScale),\n staticPixelRatio: normalizeStaticPixelRatio(\n options.staticParticlePixelRatio,\n ),\n staticMode: options.staticParticles ?? false,\n rectCache: options.particleRectCache !== false,\n // Both halves are prerequisites, not preferences: without the box cache there is nowhere for a\n // delivered `contentRect` to live, and without a ResizeObserver no first box ever arrives.\n observerSizing:\n options.particleObserverSizing !== false &&\n options.particleRectCache !== false &&\n typeof ResizeObserver !== \"undefined\",\n parkDormant: options.particleDormant !== false,\n travelExtents: options.particleTravelExtents !== false,\n oneShotExpiry: options.staticParticleOneShotExpiry !== false,\n freezeAtMount: options.staticParticleFreezeAtMount === true,\n canFreezeSurface: hostFreezeVeto(options.staticParticleImages),\n stats,\n profile,\n onBindingRendered: options.onBindingRendered,\n };\n}\n\n// The host's `canFreezeSurface` predicate, read straight off the swap policy object (see\n// `ParticleEngine.canFreezeSurface`). `true`/`false`/absent carry no predicate — the swap reads that\n// as \"no veto\", and so does the freeze-at-mount decision.\nfunction hostFreezeVeto(\n option: StaticSurfaceOption | undefined,\n): ((node: HTMLElement, canvas: HTMLCanvasElement) => boolean) | null {\n if (typeof option !== \"object\" || option === null) return null;\n return typeof option.canFreezeSurface === \"function\"\n ? option.canFreezeSurface\n : null;\n}\n\n// Is there a WebGPU API on this page AT ALL? The gate's synchronous short-circuit (see `openGate`):\n// no `navigator.gpu` means no promise is created, no microtask is scheduled and no binding is ever\n// surface-less — which is what keeps jsdom and every non-WebGPU browser on the byte-identical path\n// under the default `effectsRenderer: \"auto\"`. Deliberately NOT `acquireWebgpuDevice`, which would\n// answer the same question one turn of the event loop later.\nfunction hasWebgpuApi(): boolean {\n return Boolean(\n (globalThis.navigator as (Navigator & { gpu?: unknown }) | undefined)?.gpu,\n );\n}\n\n// The pinned backing ratio in force right now, or undefined when the canvases follow the live\n// `devicePixelRatio × renderScale`. Pinned means BOTH: the consumer set `staticParticlePixelRatio`\n// AND the runtime is in frozen mode — a live binding is simulating against the current fit and must\n// keep tracking it.\nfunction pinnedRatio(engine: ParticleEngine): number | undefined {\n return engine.staticMode ? engine.staticPixelRatio : undefined;\n}\n\n// The longest-edge ceiling in force for one sizing: the PINNED static clamp\n// (`MAX_PINNED_BACKING_DIM`, pinned path only), whatever the backend can back a canvas with\n// (`maxBackingDim`), or the SMALLER of the two when both apply. Undefined = unbounded, which is what\n// the live path and the WebGL backend both are.\nfunction backingDimLimit(\n pinLimit: number | undefined,\n backendLimit: number | undefined,\n): number | undefined {\n if (pinLimit === undefined) return backendLimit;\n if (backendLimit === undefined) return pinLimit;\n return Math.min(pinLimit, backendLimit);\n}\n\n// THE layout read: measure one binding's self-layer content box into its cache, and book it.\n// Every `clientWidth`/`clientHeight` this runtime performs goes through here, so `boxReads` is the\n// whole truth about the forced layouts it causes.\n//\n// The read itself is cheap; what costs is the STYLE+LAYOUT FLUSH the browser must run first,\n// because this runtime always reads right after writing (it inserted a canvas, hid the preview\n// spans, set the canvas box). So the callers' job is not to avoid reading — each binding has its own\n// self-layer, so N bindings genuinely need N reads — but to keep the reads CONTIGUOUS (one flush for\n// the run; see `reconcile`'s measure pass) and to not read again once the box is known.\nfunction readBoxInto(\n binding: ParticleBinding,\n stats: ParticleRuntimeStats | undefined,\n): void {\n if (stats) stats.boxReads++;\n binding.boxW = binding.selfLayer.clientWidth;\n binding.boxH = binding.selfLayer.clientHeight;\n binding.boxMeasured = true;\n}\n\n// Put a binding's canvas in the DOM, once, at its first sizing (see `ParticleBinding.mounted`).\n// Returns TRUE only for the insert that really happened, so the sizer can report \"this surface has\n// never been painted\" to whoever must kick the loop.\n//\n// PURE WRITE. The canvas is `position: absolute` in an `overflow: visible` self-layer whose own\n// width/height are explicit inline px (see `../model`), so inserting it cannot change the box the\n// ResizeObserver is watching — which is what makes it safe to do from inside an observer callback\n// without provoking a second delivery (or a \"loop completed with undelivered notifications\").\nfunction mountBinding(binding: ParticleBinding): boolean {\n if (binding.mounted) return false;\n binding.mounted = true;\n binding.selfLayer.insertBefore(binding.canvas, binding.selfLayer.firstChild);\n return true;\n}\n\n// Give a binding the rendering context it draws through, if it does not have one yet. Returns\n// whether it now has a surface.\n//\n// The ONE place a surface is acquired after `createBinding`, because under\n// `staticParticleFreezeAtMount` a binding's surface is DEFERRED — a claimed still never needs one at\n// all (see `claimFrozenMount`), so asking the backend for one at create would allocate a context, a\n// backing store and a GPU buffer per node for the exact population this option exists to make free.\n//\n// A REFUSAL (`createSurface` returning null — on WebGL, `getContext(\"2d\")` failing) leaves the\n// binding surface-less rather than deleting it, which is a real narrowing of the create-time\n// behaviour and is confined to the opt-in path: a surface-less binding is skipped by every draw path\n// exactly like a still-pending one, so it renders nothing and costs nothing, but its static preview\n// spans stay hidden until the node goes away. The create-time refusal still deletes, because there\n// the runtime has the binding map in hand and can rebuild the node.\nfunction ensureSurface(\n engine: ParticleEngine,\n binding: ParticleBinding,\n): boolean {\n if (binding.surface) return true;\n const backend = engine.backend;\n // PENDING (the WebGPU device has not arrived): `adoptBackend` hands surfaces out.\n if (!backend) return false;\n binding.surface = backend.createSurface(binding.canvas, binding.config);\n return binding.surface !== null;\n}\n\n// Does this binding deliberately own NO surface right now? Two states, both freeze-at-mount's:\n// undecided (its first sizing has not run, so whether it needs one is still an open question) and\n// standing on a claimed still (it will never draw unless it is asked to go live). `adoptBackend`\n// consults this so an arriving backend does not hand out the surfaces this option exists to skip.\nfunction surfaceDeferred(\n engine: ParticleEngine,\n binding: ParticleBinding,\n): boolean {\n return (\n engine.freezeAtMount && (!binding.freezeDecided || binding.stillMounted)\n );\n}\n\n// FREEZE AT MOUNT, decided ONCE at a binding's first sizing (`staticParticleFreezeAtMount`).\n// Returns TRUE only when this binding is now standing on a claimed still — mounted as an `<img>`\n// over a canvas with no context and no backing store — in which case the caller must not size it.\n//\n// THE POINT OF THE WHOLE ROUND. For a fleet whose Nth copy renders a frame this document has\n// already encoded, everything the ordinary path does — allocate a context, allocate a backing store,\n// warm a simulation, draw it, wait out a quiet window, read the canvas back, encode it — is redundant\n// work for pixels that are already in hand. So: resolve the geometry (no writes), name the frame\n// with it, write the canvas's CSS BOX ONLY, mount, and ask the swap module for that key.\n// HIT ⇒ nothing else happens for this node, ever, unless something asks it to go live. The canvas\n// is contextless and blank, which is why `dirty` is cleared (an owed repaint is what the swap's\n// watchdog reverts on) and why `pendingWarm` is set: the warm this binding did not run is OWED,\n// and `liveifyBinding` pays it before it draws so a live-ify cannot pop.\n// MISS ⇒ today's path verbatim from here: acquire the surface, and the caller sizes, warms and\n// draws as it always did. The frame it produces is what the swap then encodes under this same\n// key, which is what makes the NEXT copy a hit.\n// A miss is the honest report even for the first-ever appearance of a key (nothing was there to\n// claim), so `staticStillCacheMisses` counts encodes-still-needed rather than only failures.\nfunction claimFrozenMount(\n engine: ParticleEngine,\n binding: ParticleBinding,\n contentRect?: { width: number; height: number },\n): boolean {\n binding.freezeDecided = true;\n // The SWAP's own veto, asked here (see `ParticleEngine.canFreezeSurface`). A refused binding is an\n // ordinary live one from here on and never asks again.\n if (\n engine.canFreezeSurface &&\n !engine.canFreezeSurface(binding.node, binding.canvas)\n ) {\n ensureSurface(engine, binding);\n return false;\n }\n binding.freezeAtMount = true;\n const pin = pinnedRatio(engine);\n const geom = measureCanvasGeometry(\n binding,\n pin ?? engine.pixelRatio,\n contentRect,\n backingDimLimit(\n pin === undefined ? undefined : MAX_PINNED_BACKING_DIM,\n engine.backend?.maxBackingDim(),\n ),\n engine.stats,\n engine.rectCache,\n engine.travelExtents,\n );\n binding.pad = geom.pad;\n binding.drawRatio = geom.ratio;\n // `staticFrameKeyFor` reads `pad`/`drawRatio` off the binding, so both are published above. It\n // refuses a state the live loop has stepped and an undecoded texture — neither is reachable at a\n // first sizing, but the key is the promise the `<img>` rests on and this is not the place to\n // assume that.\n const key =\n geom.w >= 1 && geom.h >= 1\n ? staticFrameKeyFor(binding, geom.w, geom.h)\n : null;\n if (key !== null) {\n // `claimStaticStill`'s preconditions, in its order: the canvas must be in the document (the\n // stand-in is inserted immediately before it) and must already carry the box the stand-in copies\n // verbatim. The BOX only — writing `width`/`height` here would allocate the backing store this\n // whole path exists not to allocate.\n writeCanvasBox(binding, geom);\n mountBinding(binding);\n binding.canvasSyncDeferred = false;\n if (claimStaticStill(binding, key, engine.stats)) {\n binding.stillMounted = true;\n binding.pendingWarm = true;\n binding.frozen = false;\n // The `<img>` IS this surface's frame, and the canvas under it owes nothing: it is not going\n // to be painted at all. Left dirty, the watchdog would read an owed repaint as evidence the\n // stand-in is stale and revert it on its first sweep.\n binding.dirty = false;\n return true;\n }\n // Refused after all (the key is in flight, or failed): the canvas is mounted and boxed, and the\n // caller's sizing pass takes it from here exactly as for a miss.\n }\n ensureSurface(engine, binding);\n return false;\n}\n\n// Size ONE binding's canvas through the one rule (live vs pinned) and book the stat. Every in-runtime\n// `syncCanvasSize` goes through here so the two paths can't drift. Returns `syncCanvasSize`'s\n// \"backing store was reallocated (and therefore CLEARED)\" flag.\n//\n// It is also the one place a re-size meets the image swap. A realloc CLEARS the canvas (so it no\n// longer holds the frame anything froze) and a placement change moves the box the stand-in `<img>`\n// copied at freeze time, so either one reverts the swap — WITHOUT blocking, because a re-size is\n// the runtime's own deliberate change and says nothing about whether this surface's content churns.\n// The `cssText` compare is paid ONLY by a binding that actually has swap state, so an off runtime\n// (the default) reads nothing extra.\nfunction sizeCanvas(\n engine: ParticleEngine,\n binding: ParticleBinding,\n contentRect?: { width: number; height: number },\n): boolean {\n // FIRST SIZING under `staticParticleFreezeAtMount`: this is where a binding finds out what kind it\n // is, and where a claimable frame short-circuits the rest of this function entirely. A claim\n // reports FALSE — nothing was allocated, nothing was cleared and nothing needs a redraw.\n if (engine.freezeAtMount && !binding.freezeDecided) {\n if (claimFrozenMount(engine, binding, contentRect)) return false;\n }\n const pin = pinnedRatio(engine);\n if (pin !== undefined) engine.stats.pinnedCanvasSyncs++;\n const swapped = binding.staticImage !== null;\n const boxBefore = swapped ? binding.canvas.style.cssText : \"\";\n const cleared = syncCanvasSize(\n binding,\n pin ?? engine.pixelRatio,\n contentRect,\n backingDimLimit(\n pin === undefined ? undefined : MAX_PINNED_BACKING_DIM,\n // PENDING (no backend yet): no ceiling of its own to fold in. A WebGPU adoption re-sizes every\n // binding it takes over, so a canvas sized past that device's limit here is corrected there.\n engine.backend?.maxBackingDim(),\n ),\n engine.stats,\n engine.rectCache,\n engine.travelExtents,\n );\n // The canvas is blank until something redraws it: not a surface to freeze, and not one to keep\n // frozen either.\n if (cleared) binding.dirty = true;\n if (swapped && (cleared || binding.canvas.style.cssText !== boxBefore)) {\n revertStaticImage(binding, engine.stats);\n }\n // Whatever sync was owed is paid: the park's backlog, or the wait for the observer's first box.\n // (Never reached while `dormant` — `sizeCanvasOrDefer` is the only way in for a parked binding.)\n binding.canvasSyncDeferred = false;\n // FIRST SIZING mounts (see `mountBinding`). Reported as \"cleared\" whatever `syncCanvasSize` said,\n // because a just-inserted canvas has never been painted — and a create whose backing store happens\n // to land on the 300x150 default would otherwise report no change and never be drawn.\n if (mountBinding(binding)) {\n binding.dirty = true;\n return true;\n }\n return cleared;\n}\n\n// `sizeCanvas`, DEFERRED while the binding is parked (see `ParticleBinding.canvasSyncDeferred`) —\n// the shader runtime's `syncCanvasSizeOrDefer`, same contract. EVERY size that is not the wake's own\n// goes through here, so a parked canvas cannot be re-allocated behind the park. Returns\n// `sizeCanvas`'s \"backing store was reallocated\" flag, and FALSE when the work was deferred: nothing\n// was cleared, so no caller needs to redraw anything.\n//\n// TWO reasons to defer, and they compose: the binding is PARKED (its canvas is hidden, so re-sizing\n// it is pure cost), or it has NO BOX YET and `particleObserverSizing` is on — in which case sizing it\n// here would mean reading `clientWidth`, which is exactly the forced layout the observer path exists\n// to avoid. The waiting binding is sized by the shared ResizeObserver's first delivery (or, if that\n// never comes, by `reconcile`'s mount backstop).\nfunction sizeCanvasOrDefer(\n engine: ParticleEngine,\n binding: ParticleBinding,\n): boolean {\n if (binding.dormant || (engine.observerSizing && !binding.boxMeasured)) {\n binding.canvasSyncDeferred = true;\n return false;\n }\n return sizeCanvas(engine, binding);\n}\n\n// EVERY path that writes pixels into a binding's canvas ends here — the live loop's `drawBinding`\n// (its clear-only frame included: a clear is a write), the frozen path's warm+draw, and the frozen\n// path's CACHE-HIT BLIT. That last one is the load-bearing case: it repaints from a cached frame\n// while counting as a `cacheHits`, not a `draws`, so a frozen fleet that keeps re-blitting would\n// otherwise read as QUIET and earn a swap whose `<img>` then sits over pixels that are still moving.\n//\n// Being that one convergence point is also why the EXTERNAL notification\n// (`GodotHtmlRuntimeOptions.onBindingRendered`) fires from here rather than from the draw: the two\n// audiences ask the same question — \"did this canvas just change?\" — and answering it in one place is\n// what stops a future paint path from telling the swap and forgetting the consumer.\n//\n// THE KEY. `staticFrameKeyFor` names the frame this canvas now holds — the same name the\n// static-frame cache stores that bitmap under — and it is reported, because on the pristine path it\n// is exactly the evidence the swap module's KEYED-OR-QUIET contract asks for: the frame is a PURE\n// FUNCTION of that key (spec + count + seed + textures + geometry), so two paints under one key are\n// the same pixels. Three consequences, and all three are the point:\n// - N twins share ONE encode and ONE object URL instead of N private synthetic ones;\n// - the CACHE-HIT BLIT re-states the frame its `<img>` is already showing, so the swap tolerates it\n// without reverting — which is what stops a re-blitting frozen fleet (one binding mounts, the\n// reconcile kicks the parked loop, every twin re-blits) from thawing its whole set every time;\n// - a host may pin `keyedQuietMs: 0` and freeze such a surface the instant it paints, since there\n// is nothing left to wait for.\n//\n// THE NULL CASES STAY KEYLESS, and they are not conservatism — a key here is a PROMISE, and the swap\n// module has no way to catch a broken one (its proxies all read \"explained\"). `staticFrameKeyFor`\n// refuses on its own two: a state the live loop has stepped (its phase depends on WHEN, not on the\n// spec) and an undecoded texture/mask (the frame is a placeholder paint). To those this adds the\n// caller's: a zero-sized backing store, and the EXPIRED-BURST BLANK (`keyedFrame: false`), whose\n// canvas is deliberately NOT the frame its key names — reporting it would hand a blank surface the\n// warmed burst every twin on that key is showing.\n//\n// A keyless surface still gets a private synthetic key inside the swap module, so nothing is shared\n// for it and the plain quiet window is all it ever earns — the live-mode steady state, unchanged.\nfunction notePaint(\n engine: ParticleEngine,\n binding: ParticleBinding,\n // Does the canvas now hold the frame `staticFrameKeyFor` names? False for the one paint that\n // deliberately does not: `retireExpiredBurst`'s blank.\n keyedFrame = true,\n): void {\n binding.dirty = false;\n const onBindingRendered = engine.onBindingRendered;\n // Nothing to compute a key FOR: no external consumer and no swap. The common configuration, and it\n // stays exactly as cheap as it was.\n if (!onBindingRendered && !binding.staticImage) return;\n const w = binding.canvas.width;\n const h = binding.canvas.height;\n // ONE key for both audiences. It is cheap on the path where it would be paid per frame: a live\n // (stepped) binding is not `pristine`, and `staticFrameKeyFor` answers null on that check alone.\n const key =\n keyedFrame && w >= 1 && h >= 1 ? staticFrameKeyFor(binding, w, h) : null;\n // THE EXTERNAL PAINT NOTIFICATION, from the one function every write already converges on (see the\n // header) — so a consumer that composites this canvas ITSELF hears about the cache-hit blit and the\n // burst-ending clear, not just the instanced draw. Both write pixels; a consumer that only heard\n // about draws would show one system out of a fleet of identical frozen twins, and would keep\n // painting a finished burst forever.\n //\n // BEFORE the `staticImage` gate below, deliberately: that gate is the image swap's, and the swap is\n // OFF in exactly the configuration this callback exists for (a host compositing the surface itself\n // has no compositor layer to trade away, so it disables the swap). Firing under it would be firing\n // never.\n onBindingRendered?.(binding.node, binding.canvas, particleRenderInfo(key));\n if (!binding.staticImage) return;\n noteStaticFrame(binding, key, engine.stats);\n}\n\n// Build one particle binding from an outer `[data-godot-particle-runtime]` node: parse its spec,\n// mount the overlay canvas in the self-layer and hide the static preview. Returns null when the node\n// has no self-layer or no parseable spec.\n//\n// DOM WRITES ONLY — it deliberately neither measures nor sizes. Sizing needs the self-layer box, and\n// reading that here (right after the canvas insert + preview hide this function just did) is a\n// forced layout flush PER new binding; `reconcile` instead measures every new binding in one\n// contiguous pass and sizes them afterwards. Texture-load hooks are armed there too, for the same\n// reason (`armBindingTextures`).\nfunction createBinding(\n node: HTMLElement,\n engine: ParticleEngine,\n onUnsupported?: UnsupportedRenderReporter,\n): ParticleBinding | null {\n // The runtime marker is mirrored onto BOTH the outer node and its self-layer;\n // process only the outer node (the self-layer carries `data-godot-self-layer`).\n if (node.hasAttribute(\"data-godot-self-layer\")) return null;\n const selfLayer = ownSelfLayer(node);\n if (!selfLayer) return null;\n const signature = node.getAttribute(\"data-godot-particle-specs\") ?? \"\";\n const config = parseParticleSpecConfig(signature);\n if (!config) {\n // Fail loud (deduped): a particle node was opted in but its serialized spec is missing/malformed, so it\n // stays on the static preview. Surface which node instead of dropping it silently.\n reportUnsupportedRender(\n {\n kind: \"particle\",\n id:\n node.getAttribute(\"data-godot-path\") ??\n (signature.slice(0, 80) || \"particle\"),\n reason: \"malformed particle spec\",\n },\n onUnsupported,\n );\n return null;\n }\n\n const canvas = document.createElement(\"canvas\");\n canvas.setAttribute(\"data-godot-particle-canvas\", \"true\");\n // Position + size are driven by `syncCanvasSize` (it grows the canvas beyond the node box\n // by `pad` so large sprites aren't clipped). The self-layer is `overflow: visible`.\n Object.assign(canvas.style, {\n position: \"absolute\",\n pointerEvents: \"none\",\n });\n // The renderer claims the canvas (a canvas can hold only ONE context type, ever — see\n // `./render-backend`). Refused ⇒ no binding, and the node keeps its static preview.\n //\n // PENDING (no backend yet — the WebGPU device has not arrived): the binding is built WITHOUT a\n // surface and without textures, and gets both when the gate adopts a backend (`adoptBackend`).\n // Everything else about it — the canvas element, its box, its mount, its simulation — is\n // renderer-agnostic and happens now, so an adoption is a surface hand-over and not a re-create.\n //\n // DEFERRED under `staticParticleFreezeAtMount`: which surfaces are needed at all is a question the\n // first sizing answers (`claimFrozenMount`), and a claimed one never gets a context — so asking\n // for one here would allocate exactly what the option exists to skip. See `ensureSurface`, which\n // is where every deferred acquisition (and every refusal) then happens.\n const surface = engine.freezeAtMount\n ? null\n : (engine.backend?.createSurface(canvas, config) ?? null);\n if (!engine.freezeAtMount && engine.backend && !surface) return null;\n\n const state = createParticleState(config, engine.maxInstances);\n preprocessParticles(state);\n\n // Sprite, colour LUT and coverage mask, from the cache of whichever backend will sample them (see\n // `ParticleRenderBackend.resolveTextures`). Until an image decodes its entry is the 1x1\n // TRANSPARENT placeholder, whose red is 0 — so a masked system draws nothing rather than flashing\n // an unmasked square, which is what the game shows too.\n const textures = engine.backend?.resolveTextures(config) ?? null;\n\n // The canvas is NOT inserted here — `mountBinding` does that at the binding's first sizing, so a\n // canvas never sits in the DOM at its 300x150 default box (see `ParticleBinding.mounted`).\n // Hide the static preview spans so they don't double with the live canvas. They stay hidden\n // through a park too: the subtree is occluded, so swapping a live canvas for a CSS preview would\n // buy nothing and cost paint. Hidden HERE rather than at mount, for that same reason and because a\n // preview left up for the frame before the canvas arrives is a visible flash of differently-placed\n // dots. Both are out-of-flow writes, so neither costs layout.\n const hiddenPreview = Array.from(\n selfLayer.querySelectorAll<HTMLElement>(\"[data-godot-particle]\"),\n );\n for (const span of hiddenPreview) span.style.display = \"none\";\n\n // Born under a suspended ancestor ⇒ born PARKED: the canvas starts hidden (so it never gets a\n // compositor layer at all) and the create-time measure + `sizeCanvas` are deferred to the wake.\n // This is the ONE direct write this runtime makes to a canvas's `display`; it happens before any\n // swap state exists, and the swapper's `attach` adopts the hide (see `../surface-image-swap`).\n const suspended = isEffectsSuspended(node);\n const dormant = engine.parkDormant && suspended;\n if (dormant) {\n canvas.style.display = \"none\";\n engine.stats.dormantParks++;\n }\n\n // An attribute read, not a layout read — it costs nothing and it must happen before the first\n // sizing, which is the moment the canvas gets its box (and, under freeze-at-mount, its frame key).\n const rectAttr = engine.travelExtents\n ? node.getAttribute(VISIBLE_RECT_ATTR)\n : null;\n // Same terms, and it must land before the first sizing for a stronger reason than the rect does:\n // under `staticParticleFreezeAtMount` the first sizing is where this binding NAMES its frame, and\n // a name computed at the wrong density is a name no twin shares. Read off the SELF-LAYER (where\n // the shader runtime reads it, so a host stamps one element for both families), not off the outer\n // node the spec and the visible rect live on.\n const pixelRatioAttr = selfLayer.getAttribute(SURFACE_PIXEL_RATIO_ATTR);\n\n const binding: ParticleBinding = {\n node,\n selfLayer,\n canvas,\n surface,\n config,\n state,\n texture: textures?.texture ?? null,\n lut: textures?.lut ?? null,\n mask: textures?.mask ?? null,\n buffer: new InstanceBuffer(),\n hiddenPreview,\n // Inserted by the first `sizeCanvas` — this reconcile's write pass on the read path, the\n // observer's first delivery on the observer path.\n mounted: false,\n textureDisposers: [],\n pad: { left: 0, right: 0, top: 0, bottom: 0 },\n // The host's visible-rect budget, read ONCE here and re-read per reconcile from then on (see\n // `VISIBLE_RECT_ATTR`). Never read at all while `travelExtents` is off — the whole feature is\n // one boolean test for a runtime that has it switched off.\n visibleRectAttr: rectAttr,\n visibleRect: parseLocalVisibleRect(rectAttr),\n pixelRatioAttr,\n pixelRatioScale: parseSurfacePixelRatio(pixelRatioAttr),\n // Both overwritten by the sizing pass `reconcile` runs right after this create (and by every\n // later resize) — a binding is never drawn before it has been sized.\n drawRatio: engine.pixelRatio,\n boxW: 0,\n boxH: 0,\n boxMeasured: false,\n signature,\n // `state.count` (not `config.amount`): the effective count AFTER the engine's `maxInstances`\n // clamp, which is a per-runtime option the module-scoped cache would otherwise alias across.\n staticKeyBase: staticFrameKeyBase(signature, config, state.count),\n frozen: false,\n // Fresh out of `createParticleState` + `preprocessParticles` — both pure functions of\n // (config, count) — so this state is exactly reproducible from the key. The live loop clears it.\n pristine: true,\n pendingWarm: false,\n // A brand-new canvas has never been painted, so it owes a draw and must not be frozen before it\n // gets one (the first `notePaint` clears this).\n dirty: true,\n dormant,\n canvasSyncDeferred: dormant,\n dormantSeq: 0,\n staticImage: null,\n suspended,\n // FIRST SIGHT of this burst (see `ParticleBinding.emitSeenAt`). A create IS first sight: an unchanged spec\n // keeps its binding, and a re-triggered burst arrives as a changed spec, i.e. a new binding.\n emitSeenAt:\n engine.oneShotExpiry && config.oneShot && config.emitting\n ? nowSeconds()\n : null,\n burstCleared: false,\n parkedBlend: null,\n // Decided at the first sizing, and only while the option is on (see `claimFrozenMount`).\n freezeDecided: false,\n freezeAtMount: false,\n stillMounted: false,\n };\n return binding;\n}\n\n// Arm a new binding's texture-load hooks. `scheduleRender` wakes the owning loop once an async\n// texture lands; resize is watched through the runtime's ONE shared ResizeObserver (see\n// `observeBinding` in `createParticleRuntime`), not from here.\n//\n// Called by `reconcile` AFTER the sizing pass, never from `createBinding`, because\n// `onTextureLoaded` fires its listener SYNCHRONOUSLY when the texture is already decoded — the\n// common case, since every twin of a VFX family after the first hits the shared texture cache. From\n// inside the create that landed a second `sizeCanvas` per binding, in the middle of the mutate pass\n// and before anything had measured: a second forced layout, which a live trace put at 29% of this\n// runtime's box reads. Armed here, that synchronous re-size sizes from the box the measure pass just\n// cached and reads no layout at all.\nfunction armBindingTextures(\n binding: ParticleBinding,\n engine: ParticleEngine,\n scheduleRender: () => void,\n): void {\n if (binding.texture) {\n // Re-size once the real texture dimensions are known (the pad depends on them), then redraw.\n // Only the PAD moved — the element box is exactly where it was — so this reuses the cached box\n // (`syncCanvasSize` tier 2) instead of re-measuring the self-layer. Parked ⇒ deferred: the pad\n // is recomputed by the wake's one sync, from a texture that has only got MORE decoded since.\n binding.textureDisposers.push(\n onTextureLoaded(binding.texture, () => {\n sizeCanvasOrDefer(engine, binding);\n scheduleRender();\n }),\n );\n }\n if (binding.mask) {\n // The mask does NOT change the canvas size (it is sampled over the sprite quad), but the loop may be\n // PARKED when it lands (a finished one-shot, or frozen/static mode) — and until then the system drew\n // NOTHING at all. Kick a redraw so the shaped burst actually appears.\n binding.textureDisposers.push(\n onTextureLoaded(binding.mask, scheduleRender),\n );\n }\n}\n\n// CUT A BINDING OUT OF THE WORLD: its texture hooks, its swap registration and stand-in, its canvas\n// element, the preview spans it hid and the node blend it neutralized. Everything a teardown does\n// EXCEPT hand back the GPU resources, because a BAKE DONOR (see `donateStill`) stops exactly here —\n// it must be unreachable from the DOM, the observer and the swapper, while its surface and its\n// simulation state stay alive because they are what the pending encode reads.\n//\n// IDEMPOTENT, and it has to be: a donor is detached now and disposed later, and the dispose runs\n// this again. `hiddenPreview` is CLEARED rather than merely walked, so a second pass cannot un-hide\n// spans that the replacement binding for the same node has since hidden for itself.\nfunction detachBinding(binding: ParticleBinding): void {\n for (const dispose of binding.textureDisposers) dispose();\n binding.textureDisposers.length = 0;\n // Drop the stand-in `<img>`, release this binding's object-URL refcount and unregister it from\n // the swapper BEFORE the canvas goes: a leaked blob URL outlives the node, the runtime and the\n // scene, and `staticImageUrlsLive` is the probe that says so.\n disposeStaticImage(binding);\n // A no-op for a binding that never mounted (disposed before its first box arrived, e.g. a\n // one-frame VFX or a born-parked binding the expiry sweep took) — its canvas was never inserted.\n binding.canvas.remove();\n for (const span of binding.hiddenPreview) span.style.display = \"\";\n binding.hiddenPreview.length = 0;\n // The preview spans are back (they DO rely on the node blend for their additive look) and the\n // node may outlive this binding — hand its blend back exactly as found.\n unparkBindingBlend(binding);\n}\n\nfunction disposeBinding(\n binding: ParticleBinding,\n backend: ParticleRenderBackend | null,\n): void {\n // Every GPU resource this binding owned — its instance buffer, and whatever else the backend hung\n // off the surface — goes back here. A binding with no surface never acquired any (it was created\n // while the backend was still pending, it is standing on a claimed still, or its adoption is what\n // is being undone).\n if (binding.surface && backend) {\n backend.disposeSurface(binding.surface, binding.buffer);\n binding.surface = null;\n }\n detachBinding(binding);\n}\n\n// Draw ONE binding's current simulation state: build the instance buffer, then hand it to the\n// backend, which submits the draw and puts the pixels on the binding's canvas (see\n// `./render-backend`).\n//\n// `prof` is the LIVE tick's cost attribution or null (see `ParticleProfile`), and it is a PARAMETER\n// rather than a read of `engine.profile` because the frozen path calls this too: a frozen binding is\n// warmed and drawn ONCE before the loop parks, and charging that one-off to the per-frame buckets\n// would make a mode with no per-frame cost look like it had one. Every bracket below — and every\n// bracket inside the backend — is behind the one hoisted null check, so an unprofiled draw takes no\n// clock reading at all.\nfunction drawBinding(\n engine: ParticleEngine,\n binding: ParticleBinding,\n prof: ParticleProfile | null = null,\n): void {\n // No surface yet ⇒ nothing to draw into, and nothing worth packing for (see\n // `ParticleBinding.surface`). No backend implies no surface, but state the pair here so the\n // renderer calls below need no assertion.\n const surface = binding.surface;\n const backend = engine.backend;\n if (!surface || !backend) return;\n const w = binding.canvas.width;\n const h = binding.canvas.height;\n if (w < 1 || h < 1) return;\n\n packBinding(binding, prof);\n const buffer = binding.buffer;\n\n if (buffer.count === 0) {\n // Nothing alive to draw: BLANK the canvas (a finished burst wipes itself) and stop. The clear is\n // a write, so this canvas just changed — it is not standing still, and any stand-in over it must\n // come down.\n backend.clear(surface, w, h, prof);\n notePaint(engine, binding);\n return;\n }\n\n engine.stats.draws++;\n // Everything renderer-specific from here — the shared-canvas sizing or the swap-chain image, the\n // submit, the blit (where one exists), and their `glMs`/`blitMs` brackets — belongs to the backend.\n backend.draw(surface, buffer, drawOptionsFor(binding, w, h), prof);\n // …and the paint is reported from inside `notePaint`, which is where EVERY path that writes this\n // canvas already converges — including the two above that return before this line.\n notePaint(engine, binding);\n}\n\n// Pack one binding's live particles into its instance buffer — the BUILD half of a draw, split out\n// only so `captureNodePixels` can re-produce a frame through exactly this code rather than a second\n// copy of it that could drift.\n//\n// Bracketed by hoisted guards rather than by wrapping the loop in a closure: the loop must stay\n// exactly the code it was, and an off runtime must not even read the clock (`prof ? … : 0` compiles\n// to a predictable branch; `performanceNow` is never called).\nfunction bindingPackInput(binding: ParticleBinding) {\n return {\n state: binding.state,\n instances: binding.buffer,\n config: { hframes: 1, vframes: 1, flipbookCropOnly: true },\n textureWidth: 0,\n textureHeight: 0,\n origin: [0, 0] as [number, number],\n transform: { xx: 1, xy: 0, yx: 0, yy: 1, originX: 0, originY: 0, scale: 1 },\n } satisfies ParticleInstancePackInput;\n}\n\nfunction packBinding(\n binding: ParticleBinding,\n prof: ParticleProfile | null,\n): void {\n const cfg = binding.config;\n // The ratio this canvas was actually sized at — live, pinned, or clamped-pinned (see `drawRatio`).\n const dpr = binding.drawRatio;\n const { frameW, frameH } = frameSize(cfg, binding.texture);\n // The LEFT/TOP margins shift the particle-local origin into the grown canvas (the canvas's own\n // left/top edges sit at exactly `-padX`/`-padY` within the self-layer, see `measureCanvasGeometry`).\n // Two numbers rather than one because the margin is directional now — a burst that falls 500px and\n // rises 60 has its origin near the TOP of its canvas, not in the middle.\n const padX = binding.pad.left;\n const padY = binding.pad.top;\n const buffer = binding.buffer;\n const buildStart = prof ? performanceNow() : 0;\n binding.packing ??= bindingPackInput(binding);\n const packing = binding.packing;\n packing.state = binding.state;\n packing.textureWidth = frameW;\n packing.textureHeight = frameH;\n packing.origin[0] = cfg.originX + padX;\n packing.origin[1] = cfg.originY + padY;\n packing.transform.xx = dpr;\n packing.transform.yy = dpr;\n packing.transform.scale = dpr;\n packParticleInstances(packing);\n if (prof) {\n prof.buildMs += performanceNow() - buildStart;\n // The instances this buffer really carries — the denominator for `buildMs` AND `glMs`, and it\n // must be read here rather than after the draw, since the clear-only path returns below.\n prof.instances += buffer.count;\n }\n}\n\n// One binding's draw inputs at the current canvas size (see `ParticleDrawOptions`). Pure.\nfunction drawOptionsFor(\n binding: ParticleBinding,\n w: number,\n h: number,\n): ParticleDrawOptions {\n const cfg = binding.config;\n return {\n width: w,\n height: h,\n texture: binding.texture,\n textured: Boolean(binding.texture),\n lutTexture: binding.lut,\n maskTexture: binding.mask,\n hframes: cfg.hframes,\n vframes: cfg.vframes,\n blendMode: cfg.blendMode,\n alphaFromRed: cfg.alphaFromRed,\n erode: cfg.alphaErode,\n uvPolar: cfg.uvPolar,\n };\n}\n\n// The constant half of a binding's static-frame cache key. Computed ONCE in `createBinding`; every\n// term is fixed for the binding's whole life (a spec-attribute change re-creates the binding).\nfunction staticFrameKeyBase(\n signature: string,\n cfg: ParticleSpecConfig,\n count: number,\n): string {\n return particleStaticFrameKeyBase({\n specJson: signature,\n count,\n blendMode: cfg.blendMode,\n seed: cfg.seed,\n textureUrl: cfg.textureUrl,\n maskUrl: cfg.maskUrl ?? null,\n });\n}\n\n// This binding's frozen-frame cache key at the current canvas size, or NULL when the frame it is\n// about to produce is not a pure function of that key and therefore must not be shared. Two reasons\n// to refuse:\n//\n// - NOT PRISTINE: the simulation has been stepped by the live loop, so `warmStaticParticles` will\n// warm from a phase that depends on WHEN the freeze happened, not on the spec (see\n// `ParticleBinding.pristine`). This is the mid-session `setStaticParticles(true)` downgrade; a\n// runtime that is frozen from the start (the low tier a phone boots into) keeps every binding\n// pristine, including every burst re-created later by a spec/epoch change.\n// - TEXTURE NOT DECODED: the sprite/mask are 1x1 placeholders until their image lands, so the frame\n// would be a placeholder paint. Caching it would publish that placeholder to every twin, and the\n// texture-load listener only re-renders THIS binding. (The shader runtime's `texturesLoaded`\n// gate, same reasoning.)\nfunction staticFrameKeyFor(\n binding: ParticleBinding,\n w: number,\n h: number,\n): string | null {\n if (!binding.pristine) return null;\n if (binding.texture && !binding.texture.loaded) return null;\n if (binding.mask && !binding.mask.loaded) return null;\n const { frameW, frameH } = frameSize(binding.config, binding.texture);\n return particleStaticFrameKey(binding.staticKeyBase, {\n width: w,\n height: h,\n drawRatio: binding.drawRatio,\n padX: binding.pad.left,\n padY: binding.pad.top,\n frameW,\n frameH,\n textureWidth: binding.texture?.width ?? 0,\n textureHeight: binding.texture?.height ?? 0,\n });\n}\n\n// One binding's frozen-mode step: serve the cached frame if this exact frame has already been\n// rendered by ANY binding (this runtime's or an earlier one's — the cache is module-scoped and\n// survives dispose), otherwise warm + draw once and publish the result.\n//\n// A HIT skips BOTH halves of the cost: `warmStaticParticles` (a bounded but real fixed-step sim, up\n// to ~2 lifetimes) and the instanced GL draw. N identical systems collapse to 1 warm + 1 draw + N\n// blits — and each of those blits IS a paint, reported like one (`notePaint`), so a consumer\n// compositing these canvases elsewhere sees all N surfaces rather than the one that drew.\nfunction staticStepBinding(\n engine: ParticleEngine,\n binding: ParticleBinding,\n): void {\n const w = binding.canvas.width;\n const h = binding.canvas.height;\n // The cache trades a warm+draw for a BLIT of another binding's frame, so it can only play on a\n // surface with a 2D context to blit through (see `ParticleSurface.ctx2d`) — every WebGL binding.\n // No context, no key: neither served nor published, so a frame that cannot be re-blitted can also\n // never be published into the module-scoped cache.\n const ctx2d = binding.surface?.ctx2d ?? null;\n const key =\n ctx2d !== null && w >= 1 && h >= 1\n ? staticFrameKeyFor(binding, w, h)\n : null;\n if (key !== null && ctx2d !== null) {\n const hit = getStaticParticleFrame(key);\n if (hit) {\n engine.stats.cacheHits++;\n ctx2d.clearRect(0, 0, w, h);\n ctx2d.drawImage(hit, 0, 0);\n // A BLIT IS A PAINT (see `notePaint`): pixels were just written, so this surface is not\n // standing still — even though no `draws` was booked and no simulation ran.\n notePaint(engine, binding);\n // `frozen` stays false: the state was NEVER warmed, so a later MISS at another canvas size\n // must still warm before it draws. The owed warm is recorded for the unfreeze path.\n if (!binding.frozen) binding.pendingWarm = true;\n return;\n }\n }\n if (!binding.frozen) {\n warmStaticParticles(binding.state);\n binding.frozen = true;\n binding.pendingWarm = false;\n }\n drawBinding(engine, binding);\n // Publish even a clear-only (no live instances) frame: an empty canvas IS this key's frame, and\n // the twins that would each have re-derived it are exactly what this cache exists to collapse.\n if (key !== null) storeStaticParticleFrame(key, binding.canvas, w, h);\n}\n\n// FROZEN-MODE ONE-SHOT EXPIRY — stop drawing a burst that has outlived its own active window.\n//\n// WHY. Frozen mode warms each system to a representative mid-flight frame and parks it FOREVER. For an ambient\n// emitter that is the whole point (it really does emit forever); for a ONE-SHOT it is wrong in a way nothing\n// else in this runtime can correct. A one-shot is a burst — in ANIMATED mode this runtime already ends it by\n// itself (the sim clears `state.emitting` after one cycle, the last particle dies at\n// `lifetime * (2 - explosiveness)`, and the final `drawBinding` leaves the canvas BLANK), and this restores\n// exactly that endpoint for the frozen path. Without it the only input that could ever retire the frame is the\n// host's `emitting` flag, and a host can get that stuck: the live case was a game-side visual freeze that left\n// `Emitting` latched true on every energy-counter VFX, so the mirror painted a permanent \"energy ring\" over a\n// counter the game itself was showing bare.\n//\n// SCOPE. Only one-shots, only after a FULL active window measured from this client's own first sight of the\n// burst (`ParticleBinding.emitSeenAt`), so legitimate transients — hit sparks, card flourishes — still show for\n// their natural life. `staticOneShotExpired` is the pure decision and carries the reasoning; the law is shared\n// verbatim with the game-side mod so the two sides agree on when a burst is over.\n//\n// Retiring is a CLEAR, once: no simulate, no draw, no static-frame-cache read or publish (the cached frame is\n// keyed by spec+size, not by age — an expired binding must neither serve nor poison it). A parked/unmounted\n// binding is skipped and blanked on its wake instead; `burstCleared` keeps it to one paint.\nfunction retireExpiredBurst(\n engine: ParticleEngine,\n binding: ParticleBinding,\n): void {\n if (binding.burstCleared || binding.suspended || !binding.mounted) return;\n const w = binding.canvas.width;\n const h = binding.canvas.height;\n // Unbracketed (`prof` null): the retire is a one-off, not a per-frame cost, exactly like the\n // frozen path's own warm+draw.\n if (binding.surface && w >= 1 && h >= 1) {\n engine.backend?.clear(binding.surface, w, h, null);\n }\n binding.burstCleared = true;\n // A CLEAR IS A PAINT (see `notePaint`): this canvas just changed, so a stand-in `<img>` frozen over the burst\n // must come down rather than outlive the burst it copied. KEYLESS on purpose — the blank this just painted is\n // not the frame this binding's static key names (that key names the WARMED burst, which its twins may be\n // showing right now), and a key reported over pixels that do not match it is the one mistake the swap module\n // cannot catch.\n notePaint(engine, binding, false);\n}\n\n// Seconds this binding's burst has been on screen, or null when it can never expire (not an emitting one-shot,\n// or the expiry is switched off). The frozen loop needs it twice: for the retire decision, and to schedule its\n// OWN wake — a parked loop has no other reason to run again, so without that wake an ended burst would sit\n// there until some unrelated event (a resize, a reconcile) happened to kick the loop.\nfunction burstElapsedSeconds(\n binding: ParticleBinding,\n now: number,\n): number | null {\n return binding.emitSeenAt === null ? null : now - binding.emitSeenAt;\n}\n\n// A single shared loop that steps + draws every live binding and self-stops when all are\n// idle (finished one-shot bursts cost ~0). `getBindings` is read each tick so the binding set\n// can grow/shrink across reconciles without re-creating the loop. Wakeups go through the pacer:\n// under an FPS cap it PARKS on a timer to the next cap boundary instead of arming a rAF per\n// display frame (see ../effects-loop-pacing).\nfunction createLoop(\n engine: ParticleEngine,\n getBindings: () => Iterable<ParticleBinding>,\n initialMinFrameTime: number,\n isStatic: () => boolean,\n pacing: EffectsLoopPacing | undefined,\n parkBlend: boolean,\n): {\n scheduleRender: () => void;\n setMinFrameTime: (v: number) => void;\n cancelPark: () => void;\n dispose: () => void;\n} {\n let disposed = false;\n let lastTime = nowSeconds();\n // Mutable so the runtime's setFps can retune the cap live (adaptive quality).\n let minFrameTime = initialMinFrameTime;\n const pacer = createEffectsLoopPacer(() => tick(), pacing);\n const tick = (): void => {\n if (disposed) return;\n // Frozen (static) mode: warm any not-yet-frozen binding to a representative mid-flight state, draw every\n // binding ONCE (a freshly resized canvas — e.g. after a live setRenderScale — was cleared, so redraw all),\n // then PARK the loop (return without rescheduling) → the frozen art stays on-screen at zero per-frame cost.\n if (isStatic()) {\n const staticNow = nowSeconds();\n // Seconds until the earliest burst end we still owe a wake to (see `burstElapsedSeconds`).\n let nextBurstEnd = Number.POSITIVE_INFINITY;\n // The frozen pass is a tick's worth of draws like any other (see `ParticleRenderBackend`) —\n // one that happens to be the LAST one before the loop parks.\n engine.backend?.beginFrame();\n for (const binding of getBindings()) {\n // Parked world: neutralize the node blend BEFORE the suspend skip, so a covered binding's\n // (already-painted, currently invisible) canvas stops costing a blend surface too.\n if (parkBlend) parkBindingBlend(binding);\n // A one-shot whose burst is over stops being drawn (see `retireExpiredBurst`). Evaluated BEFORE the\n // suspend skip so a burst that ends while its subtree is covered is already retired when it wakes —\n // the wake then blanks it instead of warming and drawing a burst the game finished long ago.\n const elapsed = burstElapsedSeconds(binding, staticNow);\n if (elapsed !== null) {\n if (staticOneShotExpired(binding.config, elapsed)) {\n retireExpiredBurst(engine, binding);\n continue;\n }\n const remaining = oneShotBurstSeconds(binding.config) - elapsed;\n if (remaining < nextBurstEnd) nextBurstEnd = remaining;\n }\n // Occluded (see ../effects-suspend): don't even pay the one-shot warm+draw; the resume\n // reconcile kicks the loop and `frozen` is still false, so it warms + draws then.\n if (binding.suspended) continue;\n // Not sized yet, so not in the DOM (see `ParticleBinding.mounted`). Skipping is not just an\n // optimization here: its canvas is still at the 300x150 default, and the frozen path would\n // PUBLISH that frame into the module-scoped static cache under a key derived from it.\n if (!binding.mounted) continue;\n // No surface yet (the WebGPU device has not arrived — see `ParticleEngine.backend`): skipped\n // exactly like an unmounted binding, and for a sharper reason than \"it cannot draw\" — the\n // frozen path PUBLISHES what it renders into the module-scoped static-frame cache, and a\n // binding with no surface would publish a blank canvas under a key every twin then serves.\n if (!binding.surface) continue;\n staticStepBinding(engine, binding);\n }\n engine.backend?.endFrame();\n // Frozen mode parks the loop, so a pending burst end is the ONE thing that still needs a wakeup. Arm the\n // nearest one (a no-op while a wakeup is already in flight — the pacer's one-at-a-time contract) and let\n // that tick re-derive the next. Each binding can only shorten this a bounded number of times: the window\n // strictly decreases and ends in a retire, so there is no self-sustaining wake.\n if (nextBurstEnd !== Number.POSITIVE_INFINITY) pacer.arm(nextBurstEnd);\n return;\n }\n const now = nowSeconds();\n // FPS cap: particles (esp. screen-filling background ambients) don't need 60fps; wait out the rest of the\n // capped frame interval (the sim still integrates the full dt, so motion stays time-correct).\n const remaining = minFrameTime > 0 ? minFrameTime - (now - lastTime) : 0;\n if (!pacer.isDue(remaining)) {\n pacer.arm(remaining);\n return;\n }\n // Clamp dt so a backgrounded tab (huge gap) doesn't explode the sim.\n const dt = Math.min(0.1, Math.max(0, now - lastTime));\n lastTime = now;\n let anyLive = false;\n // Cost attribution for THIS tick, or null (see `ParticleProfile`). Hoisted once per tick — every\n // bracket below is behind this one null check, so an unprofiled tick costs a compare per binding\n // and no clock reading whatever. Read AFTER the frozen branch and the cap deferral, both of which\n // do no per-frame work and therefore book nothing.\n const prof = engine.profile;\n // Bindings this tick really simulated + drew. Kept locally so the tick can decide whether it did\n // ANY work — `anyLive` cannot answer that (a tick whose last particles just died did a full\n // frame's work and still reports nothing alive).\n let profBindings = 0;\n // Seconds until the earliest FROZEN-AT-MOUNT burst end still owed a wake, exactly as the frozen\n // branch tracks it. Such a binding never simulates, so it can never end its own burst and can\n // never keep the loop alive to be asked again — without this the loop would park on the last\n // live system and a finished burst would sit there until something unrelated kicked it.\n let nextBurstEnd = Number.POSITIVE_INFINITY;\n // One frame's worth of draws (see `ParticleRenderBackend`). Opened AFTER the cap deferral above,\n // which does no work at all, so a deferred tick opens no frame either.\n engine.backend?.beginFrame();\n for (const binding of getBindings()) {\n // Occluded (see ../effects-suspend): FREEZE — no simulate, no draw, and it doesn't keep the\n // loop alive. The state is untouched, so a resume continues from exactly here (the wake\n // resets the loop clock, so there's no dt catch-up spike either).\n if (binding.suspended) continue;\n // Waiting for its first box (see `ParticleBinding.mounted`): no canvas in the DOM to draw\n // into. Skipped WITHOUT setting `anyLive` — the sizing itself kicks the loop, so a binding\n // that is only waiting cannot hold the loop open in the meantime.\n if (!binding.mounted) continue;\n // FROZEN AT MOUNT (`staticParticleFreezeAtMount`): warmed once, drawn once, never stepped.\n // Handled BEFORE `pristine` is cleared, which is the whole point — a state the loop has\n // stepped can never be named by the static frame key again, and the key is what makes this\n // binding's frame shareable, claimable and bakeable.\n //\n // The draw is owed on `dirty` rather than run every tick the way the FROZEN branch runs it.\n // That branch can be unconditional because it parks the loop immediately afterwards; this one\n // is inside a loop that keeps running for as long as any OTHER system is alive, so an\n // unconditional draw here would be a per-frame cost for a system that never changes. `dirty`\n // is precisely \"this canvas owes a repaint\" — set at create, and again by any re-size that\n // re-allocated (and therefore cleared) the backing store — and every path that sets it also\n // kicks the loop.\n //\n // NOT parked-blend-neutralized (`parkBindingBlend`), deliberately: that is the frozen MODE's\n // trade, made once for a whole runtime whose surfaces are all standing still. A live-mode\n // consumer's compositing must not change because one of its systems stopped moving.\n if (binding.freezeAtMount) {\n const elapsed = burstElapsedSeconds(binding, now);\n if (elapsed !== null) {\n if (staticOneShotExpired(binding.config, elapsed)) {\n retireExpiredBurst(engine, binding);\n continue;\n }\n const remaining = oneShotBurstSeconds(binding.config) - elapsed;\n if (remaining < nextBurstEnd) nextBurstEnd = remaining;\n }\n if (binding.dirty && binding.surface) {\n if (!binding.frozen) {\n warmStaticParticles(binding.state);\n binding.frozen = true;\n binding.pendingWarm = false;\n }\n drawBinding(engine, binding);\n }\n continue;\n }\n // No surface yet (the WebGPU device is still being acquired): skipped like an unmounted\n // binding, and BEFORE the simulation — so an adoption starts the spray from its deterministic\n // post-create state rather than from a phase nobody ever saw, and `pristine` survives to let\n // the frozen-frame cache key it.\n if (!binding.surface) continue;\n // The state stops being a pure function of (config, count) the instant a wall-clock `dt`\n // enters it — from here its phase depends on WHEN this ran, so it can never be shared\n // through the static-frame cache again (see `ParticleBinding.pristine`).\n binding.pristine = false;\n // CPU SIM bucket. The step COUNT comes back from the sim itself (it is what the sim actually\n // did — see `simulateParticles`), so `simMs / simSteps` is a cost per unit of work rather than\n // per display frame.\n if (prof) {\n profBindings++;\n const simStart = performanceNow();\n prof.simSteps += simulateParticles(binding.state, dt);\n prof.simMs += performanceNow() - simStart;\n } else {\n simulateParticles(binding.state, dt);\n }\n drawBinding(engine, binding, prof);\n if (particlesAreLive(binding.state)) anyLive = true;\n }\n engine.backend?.endFrame();\n // A tick that skipped every binding (all suspended/unmounted) did no frame work, so it books\n // none — `ticks` must stay the denominator of the buckets above, not a count of wakeups.\n if (prof && profBindings > 0) {\n prof.ticks++;\n prof.bindings += profBindings;\n }\n if (disposed) return;\n // `lastTime` is `now`, so the next boundary is one whole capped interval away.\n if (anyLive) {\n pacer.arm(minFrameTime);\n return;\n }\n // Nothing is alive, so this tick would be the last — except for a frozen-at-mount burst that\n // still owes a retire (see `nextBurstEnd`). Arm the nearest one and let that tick re-derive the\n // next; the window strictly decreases and ends in a retire, so this cannot self-sustain.\n if (nextBurstEnd !== Number.POSITIVE_INFINITY) pacer.arm(nextBurstEnd);\n };\n // Kick the loop from a wake path (a reconcile, a texture load, a live quality retune). A wakeup already\n // in flight is left alone — including a PARK, which fires within one cap interval, exactly the worst case\n // of the pre-pacing skip-and-re-arm (whose clock reset deferred a wake by the same interval).\n const scheduleRender = (): void => {\n if (disposed || pacer.isArmed()) return;\n // Reset the clock so a wake (e.g. texture load) doesn't inject a large dt.\n lastTime = nowSeconds();\n pacer.arm(isStatic() ? 0 : minFrameTime);\n };\n const setMinFrameTime = (v: number): void => {\n minFrameTime = v;\n // A pending park targets the OLD cap boundary — drop it so the new cap arms from here.\n pacer.cancelPark();\n };\n const dispose = (): void => {\n disposed = true;\n pacer.cancel();\n };\n return {\n scheduleRender,\n setMinFrameTime,\n cancelPark: pacer.cancelPark,\n dispose,\n };\n}\n\n/** A persistent particle runtime for a mounted scene root (see `createParticleRuntime`). */\nexport interface ParticleRuntime {\n /** Diff the current `[data-godot-particle-runtime]` nodes against the live bindings:\n * unchanged specs keep their running simulation, changed/new specs re-init that node only,\n * gone nodes are disposed. */\n reconcile(): void;\n /** Live retune the backing-store resolution (devicePixelRatio × clamped `scale`) without a\n * dispose+recreate — running simulations keep going, only the canvas density changes. While a pin\n * is in force (see `setStaticParticlePixelRatio`) AND the runtime is frozen this re-sizes nothing:\n * the frozen canvases are deliberately held still. The new scale applies to live bindings as soon\n * as frozen mode is left. */\n setRenderScale(scale: number): void;\n /** Live set/clear the pinned FROZEN backing-store ratio (see `staticParticlePixelRatio`). Pass\n * `undefined` (or a non-positive value) to un-pin. Only frozen bindings are affected. */\n setStaticParticlePixelRatio(ratio: number | undefined): void;\n /** Live retune the particle FPS cap (0 = uncapped). */\n setFps(fps: number): void;\n /** Live toggle frozen (single-shot) mode, mirroring `WebglShaderRuntime.setStaticShaders`: warm each system\n * to a representative mid-flight state, draw ONCE, then park the loop (true); or resume live simulation (false).\n * The low-cost fallback that shows a frozen spray of particles instead of a per-frame CPU sim + GL draw. */\n setStaticParticles(value: boolean): void;\n /** Live toggle of the frozen-surface image swap (see the `staticParticleImages` option and\n * `../surface-image-swap`), mirroring `WebglShaderRuntime.setStaticShaderImages`. Turning it OFF\n * reverts every live swap immediately and revokes its object URLs — the kill switch, safe to\n * throw at any time. Turning it ON arms the runtime's CONFIGURED policy (a boolean here never\n * replaces it) and every binding re-earns its window; a runtime left at the DEFAULT (`false`,\n * since this option is opt-in) has no configured policy to arm, so ON gives it the same\n * quiet-window policy `staticParticleImages: true` means — otherwise this switch could never\n * turn the mechanism on for the consumers it exists for. */\n setStaticParticleImages(value: boolean): void;\n /** HOST-DRIVEN revert of the surface image swap, WITHOUT blocking, mirroring the shader runtime's:\n * with no argument every swapped surface is handed back to its canvas; with a node list, only the\n * bindings at (or under) those elements. Each affected surface restarts its gate and re-earns the\n * swap on its own. This is how a host that knows something the runtime cannot see — it is about\n * to re-parent a subtree, it just re-themed, its own occlusion pass changed its mind —\n * un-shadows a stale stand-in immediately instead of waiting for a watchdog window. */\n invalidateStaticSurfaces(nodes?: Iterable<HTMLElement>): void;\n /** The runtime's live counters (see `ParticleRuntimeStats`). Returns the SAME live object every\n * call — read-only by convention; snapshot (spread) it to diff. All-zero on the no-op handle.\n * `.profile` carries the opt-in per-frame cost attribution (`effectsProfiling`) and is NULL\n * whenever it was not measured, the no-op handle included. */\n stats(): ParticleRuntimeStats;\n /**\n * TEST / DIAGNOSTIC HOOK: this node's binding re-rendered into an offscreen texture and read back\n * as tightly-packed RGBA (PREMULTIPLIED, top-down, at the canvas's BACKING-STORE size), or null\n * when there is nothing to read — no binding at (or under) `node`, no surface yet, a zero-sized\n * canvas, or a runtime rendering on WebGL.\n *\n * WEBGL RETURNS NULL ON PURPOSE, and it is not a gap: that canvas holds readable 2D pixels, so a\n * caller who wants them uses `getImageData` on it. This exists because a WebGPU canvas has no such\n * path — `drawImage`/`toDataURL` from one are blank under headless Chrome and pathological on\n * Android (docs/perf-harness.md S7) — so the frame has to be produced a SECOND time, into a\n * texture that `copyTextureToBuffer` can reach (`../webgpu/readback`). It renders the binding's\n * CURRENT state, which for a frozen/static binding is exactly the frame on screen.\n *\n * This is the WebGL↔WebGPU image-parity harness's capture path, and the seam the readback-based\n * surface image swap encodes from — `attachSurfaceSwap` gives a WebGPU binding a `captureCanvas`\n * hook built out of exactly this production (`../surface-image-swap`).\n */\n captureNodePixels?(node: HTMLElement): Promise<Uint8Array | null>;\n /** Tear down every binding and stop the loop. */\n dispose(): void;\n}\n\n/** The policy the convenience value `staticParticleImages: true` maps to: the QUIET-WINDOW gate\n * with the swap module's own defaults (window, watchdog, encode pacing, and `keyedQuietMs` inert at\n * `quietMs`). NOT the `content-key` gate that `staticShaderImages: true` maps to, even though this\n * runtime now names its frames (see `notePaint`): that gate swaps on N consecutive unchanged\n * observations, and its second clock is `noteStaticImageReconcile`, which this runtime never calls\n * — a frozen system paints ONCE and parks, so `stable` could never reach 2. The key is content\n * evidence WITHIN the quiet window (`keyedQuietMs`, key dedup, re-statement without a revert), not\n * a substitute for it. Documented where the option is declared (`../types`). */\nconst PARTICLE_QUIET_WINDOW_POLICY: StaticSurfacePolicy = {\n gate: { kind: \"quiet-window\" },\n};\n\n/** The option as the swap module wants it: absent/`false` ⇒ OFF (this option is opt-in, so an\n * absent value must resolve to `false` and NOT to the module's own default policy); `true` ⇒ the\n * quiet-window mapping above; an object is the host's policy, verbatim. */\nfunction resolveStaticParticleImages(\n option: StaticSurfaceOption | undefined,\n): StaticSurfaceOption {\n if (option === undefined || option === false) return false;\n return option === true ? PARTICLE_QUIET_WINDOW_POLICY : option;\n}\n\n/**\n * Create a persistent particle runtime over a mounted scene root. The caller keeps this handle\n * and calls `reconcile()` on each re-render. Bindings\n * are keyed by their outer node element; a binding whose `data-godot-particle-specs` is\n * unchanged keeps its running simulation (so ambient emitters and in-flight one-shot bursts are\n * NOT reset by an unrelated re-render), while a changed spec re-inits only that node — the seam\n * a host uses to re-trigger a one-shot burst (bump a value in the spec). A no-op handle when\n * WebGL2/particles are unavailable.\n */\nexport function createParticleRuntime(\n root: HTMLElement,\n options: GodotHtmlRuntimeOptions,\n): ParticleRuntime {\n const engine = createEngine(options);\n if (!engine) {\n // The no-op handle still carries (all-zero, never-incremented) stats so probes need no null case.\n const noopStats = createParticleRuntimeStats();\n return {\n reconcile() {},\n setRenderScale() {},\n setStaticParticlePixelRatio() {},\n setFps() {},\n setStaticParticles() {},\n setStaticParticleImages() {},\n invalidateStaticSurfaces() {},\n stats: () => noopStats,\n dispose() {},\n };\n }\n let disposed = false;\n // Frozen-surface image swap (see `../surface-image-swap`). The option carries the POLICY; the\n // runtime only decides when to consult it. Ships OFF (see `staticParticleImages`), and `null` IS\n // the off state — no binding is ever given swap state, so every swap call site is a no-op on a\n // stateless binding and the runtime takes exactly the path it took before this existed.\n const staticSurfacePolicy = resolveStaticParticleImages(\n options.staticParticleImages,\n );\n // The policy as the SWAPPER gets it. Under freeze-at-mount the runtime takes over\n // `StaticSurfacePolicy.onRevert` — the hook the swap module reserves for exactly this — because a\n // claimed surface is the one case where a revert uncovers nothing: no pixels, no backing store, no\n // context. The host's own handler (if it gave one) still runs, first and unconditionally; this\n // runtime's live-ify runs after it, in the same task, before anything composites. With the option\n // off the policy object is passed through untouched, so an existing consumer's swapper is built\n // from exactly what it configured.\n const swapPolicyFor = (option: StaticSurfaceOption): StaticSurfaceOption => {\n if (!engine.freezeAtMount || typeof option !== \"object\") return option;\n const hostOnRevert = option.onRevert;\n return {\n ...option,\n onRevert: (binding) => {\n hostOnRevert?.(binding);\n liveifyBinding(binding as ParticleBinding);\n },\n };\n };\n let surfaceSwapper: StaticSurfaceSwapper | null = createStaticSurfaceSwapper(\n swapPolicyFor(staticSurfacePolicy),\n engine.stats,\n );\n // THE way a binding enters the swap. Swap state exists only while the mechanism is on; NO swapper\n // IS the off state. `attach` also REGISTERS the binding, which is what lets the swapper's own\n // quiet-window/watchdog timers enumerate it without this runtime handing them anything. It also\n // ADOPTS the park's create-time hide, so the swap owns that `display` from here on (see\n // `../surface-image-swap`).\n //\n // TWO ENCODE SOURCES, and the choice is the BACKEND's, not the surface's. A 2D-backed (WebGL)\n // surface is read directly: the swap stands an `<img>` of the canvas's OWN pixels in for it, which\n // is the path that shipped first. A WebGPU surface cannot be read at all — `drawImage`/`toDataURL`/\n // `toBlob` go through presentation, blank headless and pathological on Android (S7) — so v1 never\n // registered one. v2 registers it with a CAPTURE HOOK: the backend re-renders the binding's current\n // frame into an offscreen texture and copies it back (`captureStillCanvas` below), and the swap\n // encodes that.\n //\n // Keying off `backend.captureSurface` rather than `surface.ctx2d` says the same thing about every\n // binding that has a surface (a WebGL one that could not give a 2D context was refused at create),\n // and it says it for a binding that has NO surface yet — which under freeze-at-mount is the normal\n // state of one about to claim a still, and a claim needs swap state to attach to.\n const attachSurfaceSwap = (binding: ParticleBinding): void => {\n const backend = engine.backend;\n if (!backend) {\n // PENDING (no backend). Normally `adoptBackend` attaches, since which encode source a binding\n // needs is a question only the backend answers. Freeze-at-mount cannot wait for it: the first\n // sizing may land before the device does, and a claim with no swap state is a silent miss.\n // Attach with the read-the-canvas default and let the adoption re-decide. Nothing can encode\n // in between — an unpainted binding is `dirty`, which the gate refuses, and a claim never\n // encodes at all.\n if (engine.freezeAtMount) surfaceSwapper?.attach(binding);\n return;\n }\n if (!backend.captureSurface) {\n // A re-attach must never leave a hook from a previous backend on a readable surface.\n binding.captureCanvas = undefined;\n surfaceSwapper?.attach(binding);\n return;\n }\n binding.captureCanvas = () => captureStillCanvas(binding);\n surfaceSwapper?.attach(binding);\n };\n\n // TAKE A CLAIMED SURFACE BACK. The swap can no longer vouch for the stand-in over this binding —\n // the watchdog saw its canvas re-allocated, the host called `invalidateStaticSurfaces`, a re-size\n // or a dormancy wake reverted it, its `<img>` would not decode — and under that stand-in is a\n // canvas with no context, no backing store and no pixels (see `claimFrozenMount`). So build the\n // surface, size it for real, pay the warm this binding never ran, and draw, all on the stack the\n // revert is already on: the revert un-hid the canvas a few lines ago and nothing composites until\n // this task ends, so the blank never reaches the screen.\n //\n // ORDER MATTERS, in the small: `stillMounted` is cleared FIRST because `sizeCanvas` below writes\n // `canvas.width`, which reverts the (already reverted) swap and re-enters here — the flag is what\n // makes that a no-op instead of a loop.\n const liveifyBinding = (binding: ParticleBinding): void => {\n if (!binding.stillMounted || disposed) return;\n binding.stillMounted = false;\n binding.dirty = true;\n // Refused (no backend yet, or no context to be had): the binding stays surface-less and is\n // skipped by every draw path, exactly like one whose device has not arrived. Its next sizing or\n // adoption tries again.\n if (!ensureSurface(engine, binding)) return;\n sizeCanvas(engine, binding);\n if (binding.burstCleared) {\n // An expired burst's frame is BLANK and a freshly allocated backing store already is, so there\n // is nothing to warm and nothing to draw — but the paint must still be reported, keyless (see\n // `retireExpiredBurst`), or the swap would go on believing this canvas holds the warmed burst.\n notePaint(engine, binding, false);\n return;\n }\n if (binding.pendingWarm) {\n warmStaticParticles(binding.state);\n binding.pendingWarm = false;\n binding.frozen = true;\n }\n drawBinding(engine, binding);\n loop.scheduleRender();\n };\n\n // One binding's current frame as a 2D canvas the encoder can read — the swap's WebGPU encode\n // source. The same production as the handle's `captureNodePixels` (the same pack loop, the same\n // draw options, so the capture is the frame on screen rather than a second interpretation of the\n // state), plus the premultiplied→straight conversion `putImageData` needs\n // (`../webgpu/still-capture`). Null whenever the pixels cannot be produced; the swap books that as\n // a capture failure and leaves the surface on its canvas.\n const captureStillCanvas = async (\n binding: ParticleBinding,\n ): Promise<StaticSurfaceCapture> => {\n const backend = engine.backend;\n const surface = binding.surface;\n if (!backend?.captureSurface || !surface) return null;\n const w = binding.canvas.width;\n const h = binding.canvas.height;\n if (w < 1 || h < 1) return null;\n packBinding(binding, null);\n // THE BLANK GUARD's assertion, read BEFORE the await like `w`/`h` and for the same reason: it\n // describes the instances THIS capture is about to encode, and a live tick landing mid-readback\n // must not re-interpret them. `packBinding` skips every particle that is inactive or at alpha 0,\n // so a non-zero count is exactly the condition under which `drawBinding` DRAWS rather than\n // CLEARS — i.e. a frame the runtime knows put pixels on the canvas. At count 0 the live path\n // blanks the canvas on purpose, so an invisible capture is the truth and is encoded as\n // one: a system that has emitted nothing, or one whose particles have all faded, still freezes.\n // See `../surface-image-swap`'s BLANK CAPTURES.\n const drewInstances = binding.buffer.count > 0;\n const pixels = await backend.captureSurface(\n surface,\n binding.buffer,\n drawOptionsFor(binding, w, h),\n );\n return pixels\n ? canvasFromPremultipliedRgba(pixels, w, h, drewInstances)\n : null;\n };\n const bindings = new Map<HTMLElement, ParticleBinding>();\n // ONE ResizeObserver for the whole runtime, dispatching by observed target, instead of one per\n // binding (each is its own registration + closure + callback slot; a burst of new emitters built\n // a storm of them). Per-binding semantics are preserved: a target's LAST entry in a delivery wins.\n const observedBindings = new Map<Element, ParticleBinding>();\n const sharedObserver =\n typeof ResizeObserver === \"undefined\"\n ? null\n : new ResizeObserver((entries) => {\n const latest = new Map<Element, ResizeObserverEntry>();\n for (const entry of entries) latest.set(entry.target, entry);\n let cleared = false;\n for (const [target, entry] of latest) {\n const binding = observedBindings.get(target);\n if (binding) {\n if (binding.dormant) {\n // PARKED: cache the delivered box — the observer has already paid for it off the\n // main path, and it will NOT re-fire after the wake (the box change has happened),\n // so a wake sized from the stale pre-park box would stay wrong — but do not touch\n // the canvas. Re-allocating the backing store of a hidden canvas is exactly the\n // work the park exists to skip; the wake's one deferred sync does it, from here.\n // A 0x0 delivery is cached like any other, exactly as the awake path caches it: a\n // particle self-layer legitimately measures 0, which is why WS-1 gave \"have we\n // measured?\" its own flag instead of the shader runtime's `boxW > 0` (whose parked\n // branch must therefore drop a 0 box).\n binding.boxW = entry.contentRect.width;\n binding.boxH = entry.contentRect.height;\n binding.boxMeasured = true;\n binding.canvasSyncDeferred = true;\n continue;\n }\n // The observer already measured this box off the main path, so passing its\n // `contentRect` both sizes the canvas AND refreshes the binding's cached box\n // (`syncCanvasSize`) for free — which is what makes every later re-size that is not a\n // real box change (a `setRenderScale` step, a pin change, a texture-load re-pad)\n // reflow-free.\n //\n // For an unmounted binding this delivery is its FIRST box (`particleObserverSizing`):\n // the same call sizes it and MOUNTS it (`sizeCanvas` → `mountBinding`), returning true\n // so the loop is kicked below. That is the whole optimization — a create then costs no\n // `clientWidth` at all, because the browser measured this box during its own layout\n // step and handed it over. Chrome delivers an initial observation for EVERY newly\n // observed target, a 0x0 one included (verified) — which matters because particle\n // self-layers are routinely 0x0 (a Node2D has no rect; the canvas is all `pad`).\n const resized = sizeCanvas(engine, binding, entry.contentRect);\n cleared = cleared || resized;\n }\n }\n // Re-assigning canvas.width/height REALLOCATES (so CLEARS) the 2D backing store. The\n // loop may be PARKED — always in frozen/static mode, and in live mode whenever nothing\n // is alive — and a parked loop never redraws, so the frozen spray simply VANISHED on any\n // resize (a rotate, a letterbox/viewport change, a panel opening). Kick the loop: its\n // static branch redraws every binding once from the (untouched) frozen state and parks\n // again. Only on a REAL resize, so a no-op observation still costs nothing.\n if (cleared) loop.scheduleRender();\n });\n const observeBinding = (binding: ParticleBinding): void => {\n if (!sharedObserver) return;\n observedBindings.set(binding.selfLayer, binding);\n sharedObserver.observe(binding.selfLayer);\n };\n const unobserveBinding = (binding: ParticleBinding): void => {\n if (!sharedObserver) return;\n observedBindings.delete(binding.selfLayer);\n sharedObserver.unobserve(binding.selfLayer);\n };\n\n // ---- bake donors (see `donateStill`) --------------------------------------------------------\n //\n // Insertion order is age order (a `Set` iterates that way and a donor is added exactly once), so\n // the bound's eviction is a plain oldest-first walk.\n const donors = new Set<ParticleBinding>();\n\n /** Let a donor go: its surface and instance buffer back to the backend, everything else already\n * gone (`detachBinding` ran at donation). `published` splits the two counters. */\n const releaseDonor = (binding: ParticleBinding, published: boolean): void => {\n if (!donors.delete(binding)) return;\n if (published) engine.stats.staticStillDonorBakes++;\n else engine.stats.staticStillDonorsDropped++;\n disposeBinding(binding, engine.backend);\n };\n\n // BANK THE PIXELS OF A DEPARTING BINDING. Returns whether this binding is now a donor and must NOT\n // be disposed by the caller.\n //\n // The other half of `claimFrozenMount`. A claim can only ever hit a key this document has already\n // encoded, and the ordinary way a key gets encoded is that some surface earned a swap for it —\n // which for a scene that mounts a fleet, holds it briefly and drops it may simply never happen.\n // A binding on its way out is holding the only cheap copy of its frame that will ever exist, so\n // it is kept alive JUST long enough to encode it: its canvas leaves the DOM (so it costs no\n // compositor layer and no paint), its surface and simulation state stay, and the encode goes\n // through the swapper's ordinary pacing (`bakeStill`).\n //\n // WHAT IT REFUSES, and why each one:\n // - a key that is already known in any state — the pixels exist, are coming, or have been proven\n // unobtainable, so a bake would be a duplicate readback (`bakeStill` refuses it too; asking\n // first is how a donor avoids being retained for a job that settles false on the same stack);\n // - a binding with no key, no surface, no mount or a zero-sized canvas — nothing to read;\n // - a DIRTY one: a repaint is owed, so those pixels are not the frame the key names, and a key\n // reported over pixels that do not match it is the one mistake the swap cannot catch;\n // - a NON-EMPTY encode queue. A bake is speculative work for a surface nobody is waiting on,\n // and the queue is ordered smallest-first rather than by who asked — so a bake enqueued\n // mid-burst competes for the slice budget with live candidates that are still costing a\n // compositor layer each. `queueLength() === 0` is the swapper's own \"the fleet has drained\".\n const donateStill = (binding: ParticleBinding): boolean => {\n const swapper = surfaceSwapper;\n if (disposed || !engine.freezeAtMount || !swapper) return false;\n if (!binding.surface || !binding.mounted || binding.dirty) return false;\n const w = binding.canvas.width;\n const h = binding.canvas.height;\n if (w < 1 || h < 1) return false;\n const key = staticFrameKeyFor(binding, w, h);\n if (key === null || hasStaticStill(key)) return false;\n if (swapper.queueLength() !== 0) return false;\n // From here the binding belongs to the bake: out of the DOM, out of the swapper, out of the\n // observer (the caller unobserved it) and out of `bindings` (the caller deletes it). The loop\n // and `reconcile` walk `bindings`, so neither can reach it again.\n detachBinding(binding);\n donors.add(binding);\n // BOUNDED BY COUNT, not by bytes: what a donor pins is a canvas backing store and a GPU buffer,\n // which the pool's byte budget does not see. Oldest first, and never the one just admitted —\n // evicting that would be a readback nobody ever asked for followed by an immediate teardown.\n for (const oldest of donors) {\n if (donors.size <= MAX_STILL_DONORS) break;\n if (oldest !== binding) releaseDonor(oldest, false);\n }\n // `onSettled` fires exactly once wherever the job ends — a synchronous refusal included, which\n // is why the donor is registered above first.\n swapper.bakeStill(\n { canvas: binding.canvas, captureCanvas: binding.captureCanvas },\n key,\n engine.stats,\n (published) => releaseDonor(binding, published),\n );\n return true;\n };\n\n // Take a binding out of the runtime, banking its frame first where that is worth doing. THE one\n // exit for a binding whose NODE went away or whose spec changed; a device loss and a refused\n // adoption dispose directly, because there the surface is exactly what is broken.\n const retireBinding = (binding: ParticleBinding): void => {\n unobserveBinding(binding);\n if (donateStill(binding)) return;\n disposeBinding(binding, engine.backend);\n };\n\n // ---- the dormancy park (see the module doc + `../shader-dormant`) ---------------------------\n //\n // Expiry is ONE per-runtime timer, armed only while at least one binding is parked, and it\n // compares monotonic ORDINALS rather than a clock: the sweep disposes every binding that was\n // ALREADY parked when the timer was armed (so it has been parked for ≥ one full interval), then\n // re-arms if any remain. No per-binding timer, no clock reading, no drift. Copied from\n // `../webgl/runtime` deliberately — one park, one expiry rule.\n //\n // A node whose subtree stays suspended is RE-CREATED by the next reconcile after its sweep, born\n // parked — no measure, no size, no draw, hidden canvas — and expires again one interval later.\n // That is the same steady state the shader runtime accepts, and it is the point: the binding's\n // canvas, backing store and GL instance buffer are handed back in between. The cost is that its\n // frozen simulation state does NOT survive the sweep (a burst re-created after expiry replays\n // from its start), which is why the window is 30s and not 3.\n let dormantSeq = 0;\n let dormantSweepArmedAt = 0;\n let dormantSweepTimer: ReturnType<typeof setTimeout> | null = null;\n const sweepDormant = (): void => {\n dormantSweepTimer = null;\n if (disposed) return;\n let remaining = false;\n const expired: HTMLElement[] = [];\n for (const [node, binding] of bindings) {\n if (!binding.dormant) continue;\n if (binding.dormantSeq <= dormantSweepArmedAt) expired.push(node);\n else remaining = true;\n }\n for (const node of expired) {\n const binding = bindings.get(node);\n if (!binding) continue;\n retireBinding(binding);\n bindings.delete(node);\n engine.stats.dormantDisposes++;\n }\n if (remaining) armDormantSweep();\n };\n const armDormantSweep = (): void => {\n if (disposed || dormantSweepTimer !== null) return;\n dormantSweepArmedAt = dormantSeq;\n dormantSweepTimer = setTimeout(\n sweepDormant,\n DORMANT_DISPOSE_SECONDS * 1000,\n );\n };\n\n // Publish a binding's park state, following its (just-updated) `suspended`. THE arbitration point\n // with the frozen-surface image swap: this runtime sets the flag and hands `display` to\n // `applySurfaceVisibility`, which is the swap module's single writer of both surfaces and composes\n // the two states (parked hides the canvas AND any stand-in `<img>`; awake restores what the HOST\n // left on the canvas, never a blanket `\"\"`). Exactly `../webgl/runtime`'s `syncDormant`, including\n // the order: visibility first, then — on the wake — `noteStaticSurfaceWake`, which reverts a\n // stand-in the swap cannot vouch for (always, under the keyless quiet-window gate this runtime\n // uses, and whenever a deferred re-size is about to clear the canvas under it).\n //\n // ALL WRITES, NO READS: the one owed `sizeCanvas` is handed to `owed` instead of run here, so it\n // lands in the reconcile's WRITE pass, after the measure pass has batched whatever box read it\n // needs (a binding born parked has never been measured at all). Running it inline would put a\n // forced layout back in the middle of the mutate pass — the exact interleave WS-1 removed.\n const syncDormant = (\n binding: ParticleBinding,\n owed: ParticleBinding[],\n ): void => {\n const dormant = engine.parkDormant && binding.suspended;\n if (dormant === binding.dormant) return;\n binding.dormant = dormant;\n if (dormant) {\n applySurfaceVisibility(binding);\n binding.dormantSeq = ++dormantSeq;\n engine.stats.dormantParks++;\n armDormantSweep();\n return;\n }\n binding.dormantSeq = 0;\n engine.stats.dormantWakes++;\n applySurfaceVisibility(binding);\n noteStaticSurfaceWake(binding, engine.stats, binding.canvasSyncDeferred);\n if (binding.canvasSyncDeferred) {\n binding.canvasSyncDeferred = false;\n owed.push(binding);\n }\n };\n // Optional FPS cap (options.particleFps). 0/undefined → uncapped (every rAF), preserving prior behaviour.\n const fps = options.particleFps ?? 0;\n const minFrameTime = fps > 0 ? 1 / fps : 0;\n // Frozen (single-shot) mode — warm+draw once per binding, then park the loop; MUTABLE via\n // setStaticParticles so an adaptive/settings consumer can drop into it as a downgrade rung (mirrors the\n // shader runtime's staticShaders) — lives on the ENGINE as `staticMode`, seeded from\n // `options.staticParticles`. The sizing + draw paths are free functions that already carry the engine, and\n // a second copy here would be a second source of truth for the pin condition.\n // Parked-blend neutralization (see `parkBindingBlend` + types.ts). Read once — a code-path\n // selector, not a live quality knob.\n const parkBlend = options.parkStaticParticleBlend === true;\n const loop = createLoop(\n engine,\n () => bindings.values(),\n minFrameTime,\n () => engine.staticMode,\n options.effectsLoopPacing,\n parkBlend,\n );\n\n // PORTABILITY BACKSTOP for `particleObserverSizing`. Chrome delivers an initial observation for\n // every newly observed target — a 0x0 one included, which is the case that matters here — but the\n // ResizeObserver spec only guarantees a delivery when the observed size DIFFERS from the\n // last-reported one, whose initial value is 0x0. A strict engine may therefore never report a\n // particle self-layer at all (they are routinely 0x0), and a binding that is never delivered is\n // never sized, never mounted and never drawn: the spray would silently vanish.\n //\n // So: two frames after a reconcile deferred any create, sweep whatever is STILL waiting and size it\n // the old way. Timing is what makes this free on Chrome — observer callbacks run after the layout\n // step of a frame, so a double `requestAnimationFrame` is guaranteed to be after the delivery for\n // any observation registered before it, and the sweep finds an empty set (one map walk, no layout).\n // Where it does fire it costs exactly what the read path costs: ONE contiguous run of reads, then\n // the writes — never the per-binding read/write interleave. `boxReads` is the tell: still 0 means\n // the observer really did all the sizing.\n let mountBackstopArmed = false;\n const runMountBackstop = (): void => {\n mountBackstopArmed = false;\n if (disposed) return;\n const waiting: ParticleBinding[] = [];\n for (const binding of bindings.values()) {\n if (!binding.mounted && !binding.dormant) waiting.push(binding);\n }\n if (waiting.length === 0) return;\n for (const binding of waiting) {\n if (!binding.boxMeasured) readBoxInto(binding, engine.stats);\n }\n for (const binding of waiting) sizeCanvas(engine, binding);\n loop.scheduleRender();\n };\n const armMountBackstop = (): void => {\n if (mountBackstopArmed || disposed) return;\n mountBackstopArmed = true;\n if (typeof requestAnimationFrame !== \"function\") {\n setTimeout(runMountBackstop, 0);\n return;\n }\n requestAnimationFrame(() => requestAnimationFrame(runMountBackstop));\n };\n\n const reconcile = (): void => {\n if (disposed) return;\n const nodes = Array.from(\n root.querySelectorAll<HTMLElement>(\"[data-godot-particle-runtime]\"),\n ).filter((node) => !node.hasAttribute(\"data-godot-self-layer\"));\n const seen = new Set<HTMLElement>(nodes);\n\n // Dispose bindings whose node vanished from the DOM (banking the frame first where that pays —\n // see `retireBinding`).\n for (const [node, binding] of bindings) {\n if (!seen.has(node)) {\n retireBinding(binding);\n bindings.delete(node);\n }\n }\n\n // Creating N new systems in one reconcile is MUTATE → MEASURE → WRITE, in three passes, because\n // doing all three per binding interleaves DOM writes with layout reads and costs one forced\n // layout flush per new system (a card shuffle / an enemy turn mounts many at once — measured at\n // up to 19 reflows in a single 75 ms task). Phasing does not remove reads: each binding has its\n // OWN self-layer, so N new bindings still take N `clientWidth` reads. What it buys is that those\n // reads sit in one contiguous run AFTER every write, so they cost ONE flush instead of N — the\n // flush is the cost, not the read.\n let changed = false;\n const created: ParticleBinding[] = [];\n // Bindings woken from the park that owe a `sizeCanvas` — measured in pass 2 if they have never\n // been measured, sized in pass 3, so a wake burst costs one flush like a create burst.\n const woken: ParticleBinding[] = [];\n // KEPT bindings that owe a RE-SIZE — because their visible-rect budget moved\n // (`VISIBLE_RECT_ATTR`), or because the host restated how magnified they are\n // (`SURFACE_PIXEL_RATIO_ATTR`). Paid in pass 3 with everything else. Deliberately NOT a\n // re-create in either case: both are properties of where the node currently is on screen, and a\n // node that drifts across the stage — or that a container grows — must not have its running\n // simulation restarted every time it does.\n const rebudgeted: ParticleBinding[] = [];\n // PASS 1 — MUTATE. Build + insert every new canvas and hide every preview; nothing measures.\n for (const node of nodes) {\n const existing = bindings.get(node);\n const signature = node.getAttribute(\"data-godot-particle-specs\") ?? \"\";\n // Unchanged spec → this binding is KEPT (its running simulation is untouched). A changed one\n // is disposed + re-created below, which is also why it is not worth parking or waking.\n const kept = existing !== undefined && existing.signature === signature;\n if (existing) {\n // Re-evaluate occlusion (attribute-only, no layout). A RESUME must re-kick the loop — it\n // may have parked while every binding was suspended.\n const suspended = isEffectsSuspended(node);\n if (suspended !== existing.suspended) {\n existing.suspended = suspended;\n // Either direction owes this canvas a redraw: going under, its next paint is deferred to\n // the resume; coming back, that paint has not happened yet. Marking it dirty is how the\n // shader runtime tells the image swap the same thing — a suspended surface must not be\n // frozen mid-suspension, because the freeze would have to survive a resume it cannot see.\n existing.dirty = true;\n // …and the park follows the same signal: hide the canvas going under (dropping its\n // compositor layer and its backing store), un-hide + pay the one owed `sizeCanvas`\n // coming back. Evaluated here, in PASS 1, because a wake's deferred sync is a WRITE and\n // this pass is the write phase; a park does no layout in either direction. Only for a\n // KEPT binding: one whose spec ALSO changed in this reconcile is about to be disposed, and\n // queueing its owed sync would leave pass 3 sizing an orphan canvas (its replacement is\n // created in the right state anyway).\n if (kept) syncDormant(existing, woken);\n if (!suspended) changed = true;\n }\n // …and re-evaluate the visible-rect budget, on the same terms: one attribute read, no\n // layout, KEPT bindings only (a re-created one reads it fresh in `createBinding`). Compared\n // as the RAW STRING so an unchanged node costs a compare and not a parse.\n let owesResize = false;\n if (kept && engine.travelExtents) {\n const rectAttr = node.getAttribute(VISIBLE_RECT_ATTR);\n if (rectAttr !== existing.visibleRectAttr) {\n existing.visibleRectAttr = rectAttr;\n existing.visibleRect = parseLocalVisibleRect(rectAttr);\n owesResize = true;\n }\n }\n // …and re-evaluate the host's magnification for this surface, on exactly those terms: one\n // SELF-LAYER attribute read, no layout, KEPT bindings only, compared as the RAW STRING so an\n // unchanged node costs a compare and not a parse. Ungated — unlike the visible rect there is\n // no option to switch this off, because \"absent\" already IS off (it parses to 1, and a\n // binding that has never been stamped never enters this branch at all).\n //\n // The re-size goes to pass 3 like the rebudgeted ones, and it is `sizeCanvas` — not the\n // watchdog — that retires any stand-in `<img>` over it: a re-allocation there is a change\n // this runtime MADE, so it reverts the swap deliberately without disqualifying the surface\n // from freezing again.\n if (kept) {\n const ratioAttr = existing.selfLayer.getAttribute(\n SURFACE_PIXEL_RATIO_ATTR,\n );\n if (ratioAttr !== existing.pixelRatioAttr) {\n existing.pixelRatioAttr = ratioAttr;\n existing.pixelRatioScale = parseSurfacePixelRatio(ratioAttr);\n owesResize = true;\n }\n }\n // ONE entry however many of the two moved: pass 3's `sizeCanvasOrDefer` resolves the whole\n // geometry from current state, so a second visit would re-measure and re-decide identically.\n if (owesResize) rebudgeted.push(existing);\n }\n if (kept) continue;\n if (existing) {\n retireBinding(existing);\n bindings.delete(node);\n }\n const binding = createBinding(node, engine, options.onUnsupported);\n if (binding) {\n bindings.set(node, binding);\n created.push(binding);\n changed = true;\n }\n }\n\n // PASS 2 — MEASURE. One contiguous run of self-layer box reads (see `readBoxInto`), no writes\n // between them. Skipped entirely when the box cache is off, in which case pass 3 reads per\n // binding exactly the way this runtime always did. A binding BORN PARKED is left out on purpose\n // — its canvas is hidden and its sizing deferred, so measuring it now would be the forced layout\n // the park exists to skip; the wake measures it, here, with whatever else that reconcile mounts.\n //\n // With `particleObserverSizing` on (the default) NEW bindings are left out too, and this pass\n // reads nothing at all in the common case: their first box comes from the shared ResizeObserver's\n // initial delivery, off the main path. That leaves this run as a pure BACKSTOP — the wake of a\n // binding born parked whose observation never landed, which is the only way `boxMeasured` can\n // still be false by now.\n if (engine.rectCache) {\n if (!engine.observerSizing) {\n for (const binding of created) {\n if (!binding.dormant) readBoxInto(binding, engine.stats);\n }\n }\n for (const binding of woken) {\n if (!binding.boxMeasured) readBoxInto(binding, engine.stats);\n }\n }\n\n // PASS 3 — WRITE. Size each new canvas from the box just measured (no read), then observe it and\n // arm its texture hooks. Sizing goes through the engine (not a captured ratio) so a create under\n // a live `setRenderScale` — or under a frozen-mode pin — uses the ratio in force at that moment.\n //\n // On the observer path nothing has been measured, so `sizeCanvasOrDefer` DEFERS every create and\n // the sizing+mount happens in the observer callback instead. `observeBinding` is therefore the\n // step that starts a new binding's life, not just the step that keeps it current.\n let awaitingFirstBox = false;\n for (const binding of created) {\n // BEFORE the sizing, which is where a freeze-at-mount binding claims its still and a claim\n // needs swap state to attach to. Harmless for every other binding: `attach` writes no\n // geometry, and the quiet window it starts here instead of three statements later is a\n // fraction of a millisecond of a window measured in seconds.\n attachSurfaceSwap(binding);\n sizeCanvasOrDefer(engine, binding);\n observeBinding(binding);\n if (!binding.mounted && !binding.dormant) awaitingFirstBox = true;\n // Born parked: arm the expiry sweep, so a node that is mounted already-occluded and never\n // uncovered still hands its canvas back (`sweepDormant`) instead of being kept forever.\n if (binding.dormant) {\n binding.dormantSeq = ++dormantSeq;\n armDormantSweep();\n }\n // Last: an already-decoded texture fires this listener SYNCHRONOUSLY, and it must land on a\n // binding that is already sized and cached (see `armBindingTextures`).\n armBindingTextures(binding, engine, loop.scheduleRender);\n }\n // The wakes' owed syncs, in the same write phase (see `syncDormant`): however many re-sizes,\n // renderScale steps and observer deliveries piled up while parked, each binding pays ONE.\n for (const binding of woken) sizeCanvas(engine, binding);\n // …and the moved budgets. `sizeCanvasOrDefer` because a PARKED binding must not be re-sized\n // behind its park (the wake pays it); a re-allocation clears the canvas, so kick the loop.\n for (const binding of rebudgeted) {\n if (sizeCanvasOrDefer(engine, binding)) changed = true;\n }\n // Anything left waiting on the observer gets a deadline (see `armMountBackstop`).\n if (awaitingFirstBox) armMountBackstop();\n\n // Re-assert the parked-blend neutralization on KEPT bindings. The loop's static branch parks\n // on every wake, but a host style pass can re-impose a node blend WITHOUT touching any effect\n // marker (so nothing kicks the loop); the host's own reconcile call is the heal point. Cheap:\n // one inline-style string compare per binding, no layout.\n if (parkBlend && engine.staticMode) {\n for (const binding of bindings.values()) parkBindingBlend(binding);\n }\n\n if (changed) loop.scheduleRender();\n };\n\n // ---- THE RENDERER GATE (see `effectsRenderer` in ../types) ----------------------------------\n //\n // Factories are SYNCHRONOUS and a `GPUDevice` is not, so the gate's whole job is to make the\n // asynchronous case rare and the synchronous case exact:\n //\n // \"webgl\" → adopt WebGL here, having probed nothing.\n // no `navigator.gpu` → adopt WebGL here too. This is the branch that keeps every jsdom\n // test and every non-WebGPU browser on the byte-identical path\n // they were on before this existed, even though the DEFAULT is\n // \"auto\" — no promise, no microtask, no surface-less window.\n // device already settled → adopt (or decline) here, synchronously. Page-wide memos make\n // this the answer for every runtime after the first.\n // otherwise → PENDING: bindings are created, sized and mounted with no\n // surface, drawing nothing, until the device resolves and\n // `adoptBackend` hands each one a surface + its textures.\n //\n // Every failure is SILENT and lands in the stats: `webgpuFallbacks`, `webgpuFallbackReason`.\n let gpuShared: WebgpuShared | null = null;\n let unsubscribeDeviceLost: (() => void) | null = null;\n // THIS runtime's first fallback reason. The module-level latch in `../webgpu/device` is page-wide\n // (one device, one story), but a runtime pinned to `\"webgl\"` must not report a reason it never\n // hit, so the stat is sourced from here.\n let fallbackReason: WebgpuFallbackReason | null = null;\n\n // Hand every surface-less binding a surface from `backend` (and the textures that go with it).\n // This is the ONLY way a binding acquires one after its create, and it is a HAND-OVER, not a\n // re-create: the canvas element, its box, its mount, its simulation and its frozen state are all\n // renderer-agnostic and survive untouched.\n const adoptBackend = (backend: ParticleRenderBackend): void => {\n if (disposed || engine.backend === backend) return;\n engine.backend = backend;\n const refused: HTMLElement[] = [];\n for (const binding of bindings.values()) {\n if (binding.surface) continue;\n // TEXTURES FIRST, and for every binding — surface or not. A binding whose surface is deferred\n // still needs them: the sprite's decoded size is a term in its frame key, so a binding that\n // keyed itself without them would name a frame nobody else names.\n const textures = backend.resolveTextures(binding.config);\n binding.texture = textures.texture;\n binding.lut = textures.lut;\n binding.mask = textures.mask;\n if (surfaceDeferred(engine, binding)) {\n // FREEZE AT MOUNT: this binding owns no surface on purpose (see `surfaceDeferred`). What the\n // adoption owes it is its textures, above, and the capture-hook decision it could not make\n // while the backend was unknown; the surface itself belongs to `claimFrozenMount` (at its\n // first sizing) or to `liveifyBinding`. A binding already standing on a claimed still is not\n // even sized — its canvas is deliberately storeless, and `dirty` would put the watchdog on it.\n attachSurfaceSwap(binding);\n if (!binding.stillMounted) {\n binding.dirty = true;\n sizeCanvasOrDefer(engine, binding);\n }\n armBindingTextures(binding, engine, loop.scheduleRender);\n continue;\n }\n if (!ensureSurface(engine, binding)) {\n refused.push(binding.node);\n continue;\n }\n // The canvas has never been painted, and the sprite's real dimensions (once they land) move\n // the pad — so re-size before the first draw rather than after it.\n binding.dirty = true;\n attachSurfaceSwap(binding);\n sizeCanvasOrDefer(engine, binding);\n // Last, and only now: an already-decoded texture fires this listener SYNCHRONOUSLY, and it\n // must land on a binding that is already sized (see `armBindingTextures`).\n armBindingTextures(binding, engine, loop.scheduleRender);\n }\n for (const node of refused) {\n const binding = bindings.get(node);\n if (!binding) continue;\n unobserveBinding(binding);\n disposeBinding(binding, backend);\n bindings.delete(node);\n }\n // A canvas that refuses a WebGPU context refuses it FOREVER (a canvas holds one context type for\n // its whole life), and one that refuses is evidence about the build, not about that element — so\n // take the whole runtime back to WebGL and let the reconcile below rebuild the refused nodes on\n // fresh canvases. On WebGL a refusal is the long-standing \"no 2D context\" case: no binding.\n if (refused.length > 0 && backend.kind === \"webgpu\") {\n fallbackToWebgl(\"context-refused\");\n reconcile();\n return;\n }\n // The loop may be parked (frozen mode always, live mode whenever nothing was alive), and these\n // bindings have never drawn.\n loop.scheduleRender();\n };\n\n // Adopt WebGL, silently, counting it. THE one place a fallback happens, so the counter and the\n // reason can never disagree.\n const fallbackToWebgl = (reason: WebgpuFallbackReason): void => {\n if (disposed || engine.backend?.kind === \"webgl\") return;\n engine.stats.webgpuFallbacks++;\n if (fallbackReason === null) fallbackReason = reason;\n latchWebgpuFallbackReason(reason);\n adoptBackend(engine.glBackend);\n };\n\n // A lost device takes every WebGPU surface with it — and the canvas ELEMENTS too, because a canvas\n // that has held a webgpu context can never yield a 2d one, so the WebGL rebuild cannot reuse them.\n // Disposing each binding removes its canvas and restores its static preview; `reconcile()` then\n // builds every node again from scratch, on WebGL, with NEW canvas elements.\n //\n // The simulations restart (a burst replays, an ambient emitter pops once). That is the accepted\n // trade for a rare event: preserving them would mean re-homing state onto surfaces that no longer\n // exist, and a device loss has already dropped a frame or several.\n const handleDeviceLost = (): void => {\n if (disposed || engine.backend?.kind !== \"webgpu\") return;\n for (const binding of bindings.values()) {\n unobserveBinding(binding);\n disposeBinding(binding, engine.backend);\n }\n bindings.clear();\n // Bake donors go too, and BEFORE the fallback re-points `engine.backend`: their surfaces belong\n // to the device that was just lost, so their pending readbacks can only fail, and a release\n // after the fallback would hand a WebGPU surface to the WebGL backend to dispose.\n for (const binding of [...donors]) releaseDonor(binding, false);\n fallbackToWebgl(\"device-lost\");\n reconcile();\n };\n\n const adoptWebgpu = (\n shared: WebgpuShared,\n backend: ParticleRenderBackend,\n ): void => {\n if (disposed) return;\n gpuShared = shared;\n unsubscribeDeviceLost = onWebgpuDeviceLost(handleDeviceLost);\n adoptBackend(backend);\n };\n\n // The PENDING resolution: await the device, then its pipelines, then adopt — or fall back with\n // whatever `../webgpu/device` classified the failure as. A runtime disposed while this was in\n // flight does nothing at all (its bindings are gone; there is nothing to hand a surface to).\n const resolveWebgpu = async (): Promise<void> => {\n const shared = await acquireWebgpuDevice();\n if (disposed) return;\n if (!shared) {\n fallbackToWebgl(webgpuFallbackReason() ?? \"no-adapter\");\n return;\n }\n const backend = await createWebgpuParticleBackend(shared);\n if (disposed) return;\n if (!backend) {\n fallbackToWebgl(webgpuFallbackReason() ?? \"pipeline-error\");\n return;\n }\n adoptWebgpu(shared, backend);\n };\n\n const openGate = (): void => {\n const wanted = options.effectsRenderer ?? \"auto\";\n if (wanted === \"webgl\") {\n engine.backend = engine.glBackend;\n return;\n }\n if (!hasWebgpuApi()) {\n fallbackToWebgl(\"no-navigator-gpu\");\n return;\n }\n const device = peekWebgpuDevice();\n if (device === null) {\n // Already tried and unavailable (or poisoned by an earlier device loss) — no second probe.\n fallbackToWebgl(webgpuFallbackReason() ?? \"no-adapter\");\n return;\n }\n if (device === undefined) {\n void resolveWebgpu();\n return;\n }\n const backend = peekWebgpuParticleBackend(device);\n if (backend) {\n adoptWebgpu(device, backend);\n return;\n }\n if (backend === null) {\n fallbackToWebgl(webgpuFallbackReason() ?? \"pipeline-error\");\n return;\n }\n // Device in hand, pipelines still compiling (this runtime is the second one on the page, in the\n // same turn as the first): finish asynchronously.\n void resolveWebgpu();\n };\n openGate();\n\n // The binding at `node`, or the first one UNDER it — a host that owns a subtree should not have to\n // know which of its descendants gsw bound (the `invalidateStaticSurfaces` convention).\n const bindingAt = (node: HTMLElement): ParticleBinding | null => {\n const exact = bindings.get(node);\n if (exact) return exact;\n for (const binding of bindings.values()) {\n if (node.contains(binding.node)) return binding;\n }\n return null;\n };\n\n // Live resolution retune (adaptive quality): change the engine pixel ratio and re-size every binding's\n // canvas; the running simulations are untouched (only the canvas density changes). drawBinding reads\n // the ratio live, so the next frame draws at the new density.\n const setRenderScale = (scale: number): void => {\n if (disposed) return;\n const next = effectivePixelRatio(scale);\n if (next === engine.pixelRatio) return;\n engine.pixelRatio = next;\n // PINNED + frozen: the pin decides the backing store, so nothing re-sizes — re-sizing would clear\n // every parked canvas and force the whole frozen fleet to re-warm + re-draw, on a device that is\n // stepping quality DOWN. The new ratio is recorded above and applies to live bindings the moment\n // frozen mode is left (or the pin is cleared).\n if (pinnedRatio(engine) !== undefined) return;\n // Reflow-free: only the DENSITY moved, so every binding sizes from its cached box (see\n // `syncCanvasSize`) and this loop reads no layout at all. Before the cache it was one forced\n // layout PER BINDING, on the exact path an adaptive consumer takes when it is already struggling.\n // A PARKED binding sizes nothing: its canvas is hidden, so re-allocating its backing store now\n // would be pure cost, and the wake collapses every step it slept through into one.\n for (const binding of bindings.values()) sizeCanvasOrDefer(engine, binding);\n loop.scheduleRender();\n };\n\n // Live set/clear of the frozen-mode pin. Applied immediately when the runtime is ALREADY frozen\n // (every binding re-sizes — which clears its canvas — so the loop is kicked to redraw the parked\n // spray); otherwise just recorded for the next `setStaticParticles(true)`.\n const setStaticParticlePixelRatio = (ratio: number | undefined): void => {\n if (disposed) return;\n const next = normalizeStaticPixelRatio(ratio);\n if (next === engine.staticPixelRatio) return;\n engine.staticPixelRatio = next;\n if (!engine.staticMode) return;\n // Density-only, so reflow-free — the cached box carries every binding (see `setRenderScale`),\n // and a parked one defers to its wake.\n for (const binding of bindings.values()) sizeCanvasOrDefer(engine, binding);\n loop.scheduleRender();\n };\n\n // Live FPS-cap retune. 0/undefined → uncapped. Re-kicks the loop so a change takes effect immediately.\n const setFps = (fps: number): void => {\n if (disposed) return;\n loop.setMinFrameTime(fps > 0 ? 1 / fps : 0);\n loop.scheduleRender();\n };\n\n // Live frozen-mode toggle (mirrors the shader runtime's setStaticShaders). Entering static re-arms every binding\n // for a fresh warm+freeze (so a system already mid-flight is re-warmed to a representative state) then kicks the\n // loop, which warms+draws once and parks. Leaving it resumes live simulation from the current (frozen) state. A\n // no-op if already in the requested mode.\n const setStaticParticles = (value: boolean): void => {\n if (disposed || value === engine.staticMode) return;\n engine.staticMode = value;\n // With a pin configured, the mode flip IS a backing-store change (pinned ratio ⇄ live\n // devicePixelRatio × renderScale), so every canvas re-sizes here (the loop kick below redraws\n // them). With no pin the effective ratio is the same in both modes and no sizing pass runs — as\n // before. Density-only, so reflow-free — the cached box carries every binding (see\n // `setRenderScale`).\n if (engine.staticPixelRatio !== undefined) {\n for (const binding of bindings.values())\n sizeCanvasOrDefer(engine, binding);\n }\n // Every binding is about to repaint: leaving frozen mode the surfaces ANIMATE again, and\n // entering it they are re-warmed and re-drawn. Either way a stand-in must come down NOW rather\n // than at the next paint — which a SUSPENDED binding may not reach for a long time. A\n // runtime-wide mode flip is this runtime's own decision, so nothing is blocked: each re-earns.\n for (const binding of bindings.values()) {\n binding.dirty = true;\n revertStaticImage(binding, engine.stats);\n }\n if (value) {\n for (const binding of bindings.values()) {\n // FROZEN AT MOUNT bindings are exempt, and it is not an optimization. Such a binding is\n // ALREADY warmed to the representative frame and its state was never stepped in between, so\n // re-arming it would make the frozen path warm a warmed state — a phase no key names, while\n // `pristine` still says one does. There is nothing for a mode flip to do to a system that\n // behaves identically in both modes.\n if (binding.freezeAtMount) continue;\n binding.frozen = false;\n binding.pendingWarm = false;\n // …and an already-retired burst re-earns its blank: the live loop owned this canvas in between and may\n // have left a half-drawn burst on it (`retireExpiredBurst` is a once-per-binding paint, so without this\n // those pixels would be the frozen frame forever).\n binding.burstCleared = false;\n }\n } else {\n for (const binding of bindings.values()) {\n // Pay back a warm the static-frame cache skipped (see `ParticleBinding.pendingWarm`), so the\n // resumed simulation continues from EXACTLY the state it would have had if this binding had\n // rendered its own frozen frame. Without this, a cache hit would make the unfreeze restart\n // the spray from the un-warmed post-create state — a visible pop the cache must not cause.\n if (binding.pendingWarm) {\n warmStaticParticles(binding.state);\n binding.pendingWarm = false;\n binding.frozen = true;\n }\n // Leaving the parked world: live simulation must composite EXACTLY as before this option\n // existed (dynamic modes stay byte-identical) — restore every saved node blend now, not on\n // some later tick.\n if (parkBlend) unparkBindingBlend(binding);\n }\n }\n // The cap does not apply in frozen mode (and a resume wants the full rate back), so a park\n // armed under the previous mode is stale.\n loop.cancelPark();\n loop.scheduleRender();\n };\n\n // Live kill switch / arm for the frozen-surface image swap. OFF disposes the swapper: every live\n // swap reverts, every object URL is released and every timer is cancelled immediately (the state\n // objects are dropped, so every swap call site goes back to being a no-op). ON builds a fresh\n // swapper — under the runtime's CONFIGURED policy, or, for a runtime left at the opt-in default,\n // the same quiet-window policy `staticParticleImages: true` means (see the interface doc) — and\n // each binding must earn its swap again. No loop kick: the quiet-window gate is measured off this\n // runtime's own paints and `attach` starts each window from here, so nothing has to be redrawn\n // for the gate to work (and redrawing a parked frozen fleet would be pure cost).\n const setStaticParticleImages = (value: boolean): void => {\n if (disposed || value === (surfaceSwapper !== null)) return;\n if (!value) {\n // Revert BEFORE the swapper goes: a revert un-hides the canvas and books the counter, while\n // the disposal that follows only drops state (a torn-down surface is gone, not handed back).\n for (const binding of bindings.values()) {\n revertStaticImage(binding, engine.stats);\n }\n // A bake donor exists only to feed this swapper, and the disposal below drops its job with the\n // queue — so nothing would ever settle it and its surface would be held until teardown.\n for (const binding of [...donors]) releaseDonor(binding, false);\n surfaceSwapper?.dispose();\n surfaceSwapper = null;\n return;\n }\n surfaceSwapper = createStaticSurfaceSwapper(\n swapPolicyFor(\n staticSurfacePolicy === false\n ? PARTICLE_QUIET_WINDOW_POLICY\n : staticSurfacePolicy,\n ),\n engine.stats,\n );\n for (const binding of bindings.values()) attachSurfaceSwap(binding);\n };\n\n // Host-driven revert-without-block (see `ParticleRuntime.invalidateStaticSurfaces`). With no\n // argument the swapper hands its whole set back; with elements, every binding AT or UNDER one of\n // them — a host that owns a subtree should not have to know which of its descendants gsw bound.\n const invalidateStaticSurfaces = (nodes?: Iterable<HTMLElement>): void => {\n if (disposed || !surfaceSwapper) return;\n if (!nodes) {\n surfaceSwapper.invalidate();\n return;\n }\n const targets: ParticleBinding[] = [];\n for (const element of nodes) {\n const exact = bindings.get(element);\n if (exact) {\n targets.push(exact);\n continue;\n }\n for (const binding of bindings.values()) {\n if (element.contains(binding.node)) targets.push(binding);\n }\n }\n if (targets.length > 0) surfaceSwapper.invalidate(targets);\n };\n\n const dispose = (): void => {\n disposed = true;\n loop.dispose();\n // Stop listening for a device loss this runtime can no longer act on (the subscription is\n // page-wide and would otherwise outlive every binding it exists to rebuild).\n unsubscribeDeviceLost?.();\n unsubscribeDeviceLost = null;\n // The park's ONE timer (see `armDormantSweep`): a disposed runtime leaves nothing armed.\n if (dormantSweepTimer !== null) {\n clearTimeout(dormantSweepTimer);\n dormantSweepTimer = null;\n }\n for (const binding of bindings.values())\n disposeBinding(binding, engine.backend);\n // Bake donors go with the runtime, unpublished. Their jobs are about to be dropped with the\n // swapper's queue, so `onSettled` will never fire for them and nothing else would ever hand\n // their surfaces back — a donor is the one thing in here that outlives its own binding map.\n for (const binding of [...donors]) releaseDonor(binding, false);\n // After every binding has released its URL ref (`disposeBinding` → `disposeStaticImage`):\n // cancels the swapper's gate/watchdog/encode timers, so a disposed runtime leaves nothing armed\n // and `staticImageUrlsLive` comes back to 0.\n surfaceSwapper?.dispose();\n surfaceSwapper = null;\n bindings.clear();\n observedBindings.clear();\n sharedObserver?.disconnect();\n };\n\n return {\n reconcile,\n setRenderScale,\n setStaticParticlePixelRatio,\n setFps,\n setStaticParticles,\n setStaticParticleImages,\n invalidateStaticSurfaces,\n stats: () => {\n // The two GAUGES among the counters, sampled on read (the shader runtime's contract exactly):\n // object URLs alive MODULE-wide across every runtime in the document — the leak probe — and\n // surfaces swapped right now in THIS runtime, re-derived from the swapper rather than trusted\n // incrementally, so the \"is it engaged?\" measurement cannot drift if a revert path is missed.\n engine.stats.staticImageUrlsLive = liveStaticImageUrlCount();\n engine.stats.staticImagesLive = surfaceSwapper?.liveSwapCount() ?? 0;\n // The still pool's two gauges, on exactly the same contract — DOCUMENT-wide (the pool is\n // shared by every swapper in the page, like the key registry) and re-read here rather than\n // tracked, because nothing in this runtime is told when the pool evicts. Left unsampled they\n // would report 0 forever, which is worse than absent: 0 retained bytes is what a working\n // budget and a broken one both look like from a dashboard.\n const pool = staticStillPoolStats();\n engine.stats.staticStillRetainedEntries = pool.entries;\n engine.stats.staticStillRetainedBytes = pool.bytes;\n // …and this runtime's own donors, counted rather than trusted (see `donateStill`).\n engine.stats.staticStillDonors = donors.size;\n // …and the park's own gauge, on the same contract: counted from the binding set, so it can\n // never drift from what is really hidden right now.\n let parked = 0;\n for (const binding of bindings.values()) if (binding.dormant) parked++;\n engine.stats.dormantLive = parked;\n // The renderer gauge, re-derived rather than remembered: a runtime can change backend once\n // (WebGPU → WebGL, on a device loss), and \"pending\" is a state a reader must be able to see.\n engine.stats.renderer = engine.backend?.kind ?? \"pending\";\n engine.stats.webgpuFallbackReason = fallbackReason;\n // Counters that live on the BACKEND (submits) and on the page-wide DEVICE (losses, errors),\n // sampled here so a fallback leaves the last real reading standing instead of zeroing it.\n const submits = engine.backend?.submits?.();\n if (submits !== undefined) engine.stats.webgpuSubmits = submits;\n if (gpuShared) {\n engine.stats.webgpuDeviceLosses = gpuShared.counters.deviceLosses;\n engine.stats.webgpuErrors = gpuShared.counters.gpuErrors;\n }\n return engine.stats;\n },\n captureNodePixels: async (\n node: HTMLElement,\n ): Promise<Uint8Array | null> => {\n if (disposed) return null;\n const backend = engine.backend;\n // Absent on WebGL (see `ParticleRuntime.captureNodePixels`): its canvas is readable directly.\n if (!backend?.captureSurface) return null;\n const binding = bindingAt(node);\n if (!binding?.surface) return null;\n const w = binding.canvas.width;\n const h = binding.canvas.height;\n if (w < 1 || h < 1) return null;\n // The SAME pack loop and the SAME draw options the live path uses, so what is captured is the\n // frame the canvas is showing and not a second interpretation of the same state. Unprofiled:\n // a capture is a diagnostic, not a frame anyone is paying for.\n packBinding(binding, null);\n return backend.captureSurface(\n binding.surface,\n binding.buffer,\n drawOptionsFor(binding, w, h),\n );\n },\n dispose,\n };\n}\n\n/** THE SIZING LAW's output for one binding at one backing ratio — where the canvas goes (CSS px) and\n * how many device pixels back it. Split out from the writes so `claimFrozenMount` can have the\n * numbers, and therefore the FRAME KEY, without writing `canvas.width` or asking for a context: the\n * whole claim rests on the key it computes being the one the ordinary path would have produced, and\n * a second copy of this arithmetic is exactly how those two would drift. */\ninterface CanvasGeometry {\n pad: ParticleExtents;\n left: number;\n top: number;\n cssW: number;\n cssH: number;\n /** Backing-store size, and the ratio really granted (see `ParticleBinding.drawRatio`). */\n w: number;\n h: number;\n ratio: number;\n}\n\n// Resolve one binding's canvas geometry. READ-ONLY of the DOM apart from the one `clientWidth` tier\n// below (which books itself through `readBoxInto`); it writes nothing.\nfunction measureCanvasGeometry(\n binding: ParticleBinding,\n dpr: number,\n contentRect: { width: number; height: number } | undefined,\n // Longest-edge ceiling for the resulting backing store, passed ONLY on the pinned static path\n // (see `staticParticlePixelRatio`). Undefined ⇒ unbounded, which is what the live path always was.\n maxDim: number | undefined,\n stats: ParticleRuntimeStats | undefined,\n // `particleRectCache` (default on): may a previously measured box be REUSED? See `readBoxInto`.\n cacheBox: boolean,\n // `particleTravelExtents` (default on): size the margin from where the particles actually GO\n // (`./extents`), capped by the host's visible rect. False ⇒ the symmetric sprite+emission pad, i.e.\n // byte-identical geometry to before that law existed.\n travelExtents = true,\n): CanvasGeometry {\n // Box (content-box) size, in priority order that AVOIDS a forced reflow after creation — the\n // shader runtime's order (`../webgl/runtime` `syncCanvasSize`), which this runtime lacked:\n // 1. the ResizeObserver-provided contentRect (already measured off the main path — no reflow), else\n // 2. the box we last measured (a renderScale step, a pin change, a frozen-mode flip and a\n // texture-load re-pad all change the DENSITY or the pad, never the element box, so the cached\n // size is still valid — reusing it is what keeps those paths reflow-free), else\n // 3. a single clientWidth/clientHeight layout read — on creation, when nothing has measured yet.\n // Particle self-layers have no padding, so content-box width == clientWidth. Whatever was resolved\n // is cached, and the observer refreshes it on any real box change.\n let boxW: number;\n let boxH: number;\n if (contentRect) {\n boxW = contentRect.width;\n boxH = contentRect.height;\n binding.boxW = boxW;\n binding.boxH = boxH;\n binding.boxMeasured = true;\n } else if (cacheBox && binding.boxMeasured) {\n boxW = binding.boxW;\n boxH = binding.boxH;\n } else {\n readBoxInto(binding, stats);\n boxW = binding.boxW;\n boxH = binding.boxH;\n }\n // Grow the canvas beyond the node box so what the system draws isn't clipped to the (often tiny,\n // point-emitter) box: sprite size, emission spread and — this is the part the symmetric pad never\n // modelled — how far the particles TRAVEL. Four numbers, one per side, capped by the part of this\n // element's local space the host says can be seen. See `./extents` for the whole law; the canvas is\n // positioned at `-left`/`-top` within the `overflow: visible` self-layer and the draw offsets by\n // the same two (`packBinding`).\n //\n // The box is resolved FIRST because the visible-rect budget is measured from the box's edges.\n const cfg = binding.config;\n const pad = travelExtents\n ? particleCanvasExtents(\n cfg,\n binding.texture,\n binding.visibleRect\n ? visibleAllowance(binding.visibleRect, {\n width: boxW,\n height: boxH,\n offsetX: cfg.boxOffsetX,\n offsetY: cfg.boxOffsetY,\n })\n : null,\n )\n : symmetricCanvasExtents(cfg, binding.texture);\n const cssW = boxW + pad.left + pad.right;\n const cssH = boxH + pad.top + pad.bottom;\n // DENSITY = the runtime-wide `dpr` the caller resolved (live `devicePixelRatio × renderScale`, or\n // the frozen `staticParticlePixelRatio` pin) TIMES this ONE surface's magnification as the host\n // stated it (`SURFACE_PIXEL_RATIO_ATTR`) — the shader runtime's `syncCanvasSize`, same law, same\n // reasoning. An un-stamped binding carries exactly `1`, so its size is what it always was.\n //\n // Folded in HERE rather than at the call site so `claimFrozenMount` — which reaches\n // `measureCanvasGeometry` directly to name a frame without allocating one — cannot name a frame at\n // a density the ordinary path would not have used.\n const { w, h, ratio } = backingStoreSize(\n cssW,\n cssH,\n dpr * binding.pixelRatioScale,\n maxDim,\n );\n return {\n pad,\n // Anchor the canvas at `-pad.left`/`-pad.top` (so the draw origin sits at the node-box corner),\n // then slide the WHOLE canvas by the spec's `boxOffset` so its emission center lands where the\n // CSS-<span> fallback puts it (`self-layer + -rect`), instead of the corner (→ top-left).\n // The canvas moves with its particles, so nothing clips. See core's particles/config.ts.\n left: Math.round(-pad.left + cfg.boxOffsetX),\n top: Math.round(-pad.top + cfg.boxOffsetY),\n cssW,\n cssH,\n w,\n h,\n ratio,\n };\n}\n\n// The BOX half of a sizing: four inline style writes, no backing store. This is everything a frozen\n// stand-in needs from the canvas (it copies `cssText` verbatim), which is why it is separable at all.\nfunction writeCanvasBox(binding: ParticleBinding, geom: CanvasGeometry): void {\n const style = binding.canvas.style;\n style.left = `${geom.left}px`;\n style.top = `${geom.top}px`;\n style.width = `${geom.cssW}px`;\n style.height = `${geom.cssH}px`;\n}\n\n// Size + place a binding's overlay canvas. Returns TRUE when the backing store was reallocated —\n// which CLEARS the canvas, so the caller must make sure something redraws it (see the runtime's\n// shared ResizeObserver; `setRenderScale` and the texture-load hook already schedule a render).\nfunction syncCanvasSize(\n binding: ParticleBinding,\n dpr: number,\n contentRect?: { width: number; height: number },\n maxDim?: number,\n stats?: ParticleRuntimeStats,\n cacheBox = true,\n travelExtents = true,\n): boolean {\n const geom = measureCanvasGeometry(\n binding,\n dpr,\n contentRect,\n maxDim,\n stats,\n cacheBox,\n travelExtents,\n );\n binding.pad = geom.pad;\n // The ratio the canvas REALLY got (= `dpr` unless the pinned-path clamp bit). `drawBinding` scales\n // its particle geometry by this, so a clamped canvas draws a smaller spray that still fits instead\n // of one sized for the backing store it asked for.\n binding.drawRatio = geom.ratio;\n writeCanvasBox(binding, geom);\n let cleared = false;\n if (binding.canvas.width !== geom.w) {\n binding.canvas.width = geom.w;\n cleared = true;\n }\n if (binding.canvas.height !== geom.h) {\n binding.canvas.height = geom.h;\n cleared = true;\n }\n return cleared;\n}\n","import type { UnsupportedRenderReporter } from \"./diagnostics\";\nimport type { EffectsLoopPacing } from \"./effects-loop-pacing\";\nimport type { StaticSurfaceOption } from \"./surface-image-swap\";\nimport type { GodotEffectRenderInfo, GodotHtmlRenderOptions } from \"./types\";\n\n/** Browser effect hosting controls, separate from HTML model projection. */\nexport interface GodotHtmlRuntimeOptions\n extends Pick<\n GodotHtmlRenderOptions,\n \"enableWebglShaders\" | \"enableParticles\"\n > {\n // The HOST attaches (and reconciles) the live WebGL shader + particle runtimes\n // itself — e.g. a persistent `createWebglShaderRuntime` kept across re-renders.\n // The built-in per-render attach in `GodotSceneView`/`mountHtmlScene` then skips\n // its own runtimes (which would double-bind every node: two stacked canvases,\n // translucent shader output painted twice). Attribute stamping (`material.ts` /\n // `visual-2d.ts`) is unaffected — the enable flags above still control that.\n externalRuntimes?: boolean;\n // Fetch a `.gdshader` source by its resource path and/or uid (the runtime\n // transpiles + compiles it). Required for `enableWebglShaders` to do anything;\n // a node whose source is unavailable or unsupported stays on the CSS/SVG paint.\n resolveShaderSource?: (\n path?: string,\n uid?: string,\n ) => Promise<string | undefined> | string | undefined;\n // Per-system cap on simulated/drawn particles (safety valve against a pathological\n // `amount`). Defaults to 2048.\n particleMaxInstances?: number;\n // Optional FPS cap for the particle runtime's step+draw loop. Live retune:\n // `ParticleRuntime.setFps`. Background ambient emitters\n // (screen-filling, slow-moving) don't need 60fps; a cap (e.g. 30) roughly halves the per-frame\n // draw cost. 0/undefined → uncapped (every rAF).\n particleFps?: number;\n // Cache each particle binding's measured self-layer content box instead of re-reading\n // `clientWidth`/`clientHeight` every time its canvas is (re-)sized. ON by default.\n //\n // That read is a FORCED SYNCHRONOUS LAYOUT: it follows the canvas insert, the preview hide and the\n // canvas style writes the same pass just made, so the browser must flush style+layout before it can\n // answer. Measured on a live combat scene it was ~all of the document's `get clientWidth` self-time\n // (468 forced layouts / 332 ms over 9.6 s), because a reconcile that mounts N new systems paid one\n // flush per system and every fleet re-size loop paid one per binding. With the cache a binding reads\n // its box ONCE, at create, inside a batched measure pass; every later size — an adaptive\n // `renderScale` step, a frozen-mode pin change, a texture-load re-pad — reuses it, and the runtime's\n // shared ResizeObserver refreshes it for free from the `contentRect` it has already measured off the\n // main path. This is exactly how the sibling WebGL shader runtime has always sized its canvases.\n //\n // Set false as a kill switch: every sync re-reads the element, i.e. the read-every-time behaviour\n // from before the cache existed. `ParticleRuntime.stats().boxReads` counts the reads either way.\n // (Scope: this switches the CACHE only. A reconcile always builds every new canvas before it\n // measures anything — that phasing changes no value the runtime computes, only when the writes\n // happen, so it is not on a switch.)\n particleRectCache?: boolean;\n // Take a NEW particle binding's first self-layer box from the runtime's shared ResizeObserver's\n // INITIAL delivery instead of a create-time `clientWidth`/`clientHeight` read. ON by default;\n // requires `particleRectCache` (the box cache is where a delivered `contentRect` lands) and a real\n // `ResizeObserver`, so a jsdom/SSR environment always takes the read path.\n //\n // `particleRectCache` cut this runtime's forced layouts to one per binding CREATED — a floor, not a\n // zero, and on a live phone trace that floor was the single largest remaining forced-layout cost in\n // the client (191 ms of `get clientWidth`, essentially all of it in the create measure pass). The\n // floor exists only because the runtime asked the browser for the box on the main thread. The\n // observer has already measured it during the browser's OWN layout step, so a create can simply\n // wait for it: `reconcile` measures nothing, and `stats().boxReads` settles at 0.\n //\n // The cost is one frame. A new binding's canvas is not inserted until it has a real box (an unsized\n // canvas is a 300x150 default box, a wrong picture and an unconditional compositor layer), so its\n // first particle frame lands one browser frame later — the static CSS preview is already hidden by\n // then, so what shows for that frame is nothing, not a stale preview. Fine for the ambient emitters\n // and one-shot bursts this runtime draws; set false if a consumer needs the first frame to be\n // synchronous with the mount.\n //\n // PORTABILITY. Chrome delivers an initial observation for every newly observed target, 0x0\n // included — which is the load-bearing case, since a particle self-layer is routinely 0x0 (a Node2D\n // has no rect; the canvas is entirely sprite/emission `pad`). The spec only guarantees a delivery\n // when the size DIFFERS from the last-reported one, initially 0x0, so a strict engine might never\n // report one at all. A binding still unmounted two frames after its reconcile is therefore swept\n // and sized the old way; on Chrome that sweep finds nothing and reads no layout.\n particleObserverSizing?: boolean;\n // PARK a particle binding whose subtree the host has suspended (`data-godot-effects-suspended`)\n // instead of only freezing its simulation: hide its canvas, defer every `sizeCanvas` it is owed\n // to the wake, and dispose it outright after ~`DORMANT_DISPOSE_SECONDS` still parked. ON by\n // default — this is the particle sibling of the WebGL runtime's `data-godot-shader-dormant`\n // contract (`./shader-dormant`), which has no switch at all.\n //\n // WHY IT MATTERS. Suspending stopped the CPU cost but not the GPU one: every binding owns a\n // `<canvas>`, which is an UNCONDITIONALLY promoted compositor layer, so an occluded system kept\n // its layer, its render surface and its backing store for as long as the node stayed mounted. In\n // a live combat trace the discard→draw shuffle cost tracked what was standing on screen rather\n // than what changed (an 85-byte delta costing 95 ms), with `Commit` self-time up 4.8x, 401\n // layers created against 61 deleted, and GPU-process memory climbing monotonically 148 → 276 MB.\n // Hiding the canvas is what actually drops the layer and hands the memory back.\n //\n // WHY IT IS DEFAULT-ON. It needs no new signal and makes no guess of its own: the host has\n // already said this subtree is occluded/off-screen, and `./effects-suspend` is explicit that\n // nothing there is visible. (Contrast `staticParticleImages`, which is opt-in precisely because\n // a quiet WINDOW is a cadence only the host can know.) The cost of being wrong is bounded and\n // reversible — the wake un-hides and redraws.\n //\n // SET FALSE IF your suspend stamp is LOOSER than \"invisible\" (e.g. you suspend a subtree behind\n // a translucent overlay, or one that is merely idle): a parked system does not show its last\n // frame, it shows nothing. False restores the pre-park behaviour exactly — `dormant` stays false\n // for every binding, no canvas `display` is ever written, the expiry sweep never arms, and a\n // suspended canvas keeps its last drawn frame as before. Probe it with\n // `ParticleRuntime.stats()`: `dormantLive` (parked right now), `dormantParks`, `dormantWakes`,\n // `dormantDisposes`.\n particleDormant?: boolean;\n // Size each particle system's overlay canvas from where its particles actually TRAVEL — four\n // per-side margins instead of one symmetric pad — capped by the part of the node's own local space\n // the host says is visible. ON by default.\n //\n // WHY. A `GPUParticles2D` is a POINT: its node box is zero-size and the spray happens entirely\n // outside it, so the canvas has always been the box grown by a margin. That margin modelled how big\n // one sprite is and how far apart the particles are BORN — and nothing about where they GO. Every\n // system therefore got a SQUARE canvas centred on its node origin, and anything that travelled was\n // cropped at the square's edge, hard, with the backing store as the cut line (there is no CSS clip\n // to relax). Measured on a chest's gold-coin burst: a 710x710 canvas over particles that fly\n // ~1264px sideways and fall ~2500px — a rectangle visibly cut out of the screen. Velocity, spread,\n // gravity, acceleration, damping, orbit and lifetime were all on the wire and none of them were\n // read. `./particles/extents.ts` now reads them.\n //\n // THE VISIBLE RECT is how this stays affordable. Travel bounds alone are unbounded in principle, so\n // a host that knows which part of an element's local space can be seen stamps it as\n // `data-godot-particle-visible-rect=\"x,y,width,height\"` on the particle node, and the margin is\n // clamped to it (re-read per reconcile; a change RE-SIZES the canvas and never re-creates the\n // binding, so a moving emitter keeps its running simulation). Without the attribute the margin is\n // capped per side at the same 1024 the symmetric pad always was, so the worst-case canvas is\n // unchanged — but a far-travelling system on a host that stamps nothing will allocate more than it\n // used to. The floor is the symmetric pad itself: this law can only ever GROW a canvas, never\n // shrink one, whatever the travel math or the clamp say.\n //\n // SET FALSE as the kill switch: the margin is `spriteExtentPad + emissionExtentPad` on all four\n // sides again, the attribute is never read, and every canvas geometry (and every frozen-frame key)\n // is byte-identical to the pre-directional one.\n particleTravelExtents?: boolean;\n // Frozen (single-shot) mode for the particle runtime, mirroring `staticShaders`. Live toggle:\n // `ParticleRuntime.setStaticParticles`. Each system is\n // WARMED to a representative mid-flight state, drawn ONCE, then the loop self-parks (no per-frame\n // simulate/draw). The frozen spray of particles stays on-screen at ~zero ongoing cost. A re-triggered\n // system (spec/epoch change) or a newly mounted node is re-warmed and re-frozen. Default false (live sim).\n staticParticles?: boolean;\n // Stop drawing a FROZEN one-shot burst once its own active window has elapsed (`staticParticles` mode only).\n // ON by default.\n //\n // WHY. `staticParticles` warms each system to a representative mid-flight frame and parks it FOREVER. For an\n // ambient emitter that is exactly right. For a ONE-SHOT it is not: a one-shot is a BURST, and in animated\n // mode this runtime already ends it by itself — the sim clears `emitting` after one cycle, the last particle\n // dies at `lifetime * (2 - explosiveness)` (Godot's own `active_time`), and the canvas ends BLANK. This\n // restores that same endpoint for the frozen path, so the two modes agree on what a finished burst looks\n // like. Without it the only thing that can ever retire the frame is the host's `emitting` flag, and a host\n // can get it stuck: the case this was built for is a game-side visual freeze that left Godot's `Emitting`\n // latched true on a cluster of one-shot VFX, so the browser drew a permanent burst over UI the game itself\n // was showing bare.\n //\n // MEASURED FROM FIRST SIGHT. The client cannot know when the host started the burst — it sees only \"this\n // spec says one_shot + emitting\". One full active window from the moment this runtime first saw the binding\n // is precisely what the burst itself would do, so legitimate transients (hit sparks, card flourishes) still\n // show for their natural life; only a burst that outlives its own window is dropped. A re-triggered burst\n // arrives as a spec change, which re-creates the binding and therefore restarts the window.\n //\n // SET FALSE to restore the previous behaviour exactly: no binding is ever retired and a frozen one-shot's\n // warmed frame is parked for as long as its node is mounted. Worth doing if your host deliberately uses a\n // one-shot spec as a STILL (authoring a permanent decoration as an un-simulated burst), since this option\n // reads that as a burst that should have ended.\n staticParticleOneShotExpiry?: boolean;\n // OPT-IN pinned backing-store ratio for FROZEN particle bindings (`staticParticles` mode only) — the exact\n // sibling of `staticShaderPixelRatio`, same rationale and same clamp (`MAX_PINNED_BACKING_DIM`, longest\n // edge, aspect preserved). The particle draw scales its instance geometry by the SAME ratio the canvas was\n // sized at, so a pinned system draws its sprites at the pinned density, not the live one. Unset ⇒ frozen\n // bindings size exactly like live ones (today's behavior). Live retune:\n // `ParticleRuntime.setStaticParticlePixelRatio`.\n staticParticlePixelRatio?: number;\n // PARKED-canvas blend neutralization for `staticParticles` mode. A particle node commonly carries a\n // non-normal CSS `mix-blend-mode` (additive VFX → `plus-lighter`, via `material.ts` or a host's own\n // style pipeline), and each such element keeps a STANDING compositor blend render surface — an\n // offscreen render pass per composited frame — even while the parked canvas never changes (measured\n // dominant on a GPU-bound phone: parked particle fleets owned most of the scene's blend surfaces).\n // The live canvas does not need it for correctness: additive systems are resolved to source-over-\n // complete pixels in the canvas itself (core's `particles/render-webgl.ts` resolve pass — the accumulated\n // `light` premultiplied against peak-channel coverage, \"the closest source-over approximation of\n // Godot's pure `light + dst`\"). With this option, a binding PARKED in static mode forces the host node's inline\n // `mix-blend-mode` to `normal` and restores the prior value the moment it unparks (animated resume,\n // dispose, mode off). Blend-vs-normal differs only by the `dst * (1 - coverage)` term, so this is a\n // fidelity trade the resolve pass was designed for — verify visually on additive-heavy scenes.\n // Default false: byte-identical behavior.\n parkStaticParticleBlend?: boolean;\n // Show a FROZEN particle surface as an `<img>` of its own drawn frame instead of its `<canvas>` —\n // the particle sibling of `staticShaderImages`, running the same mechanism\n // (`./surface-image-swap`) for the same measured reason: a canvas that never changes still costs\n // a compositor layer, a blend render surface and per-frame GPU fill. Particles are usually the\n // LARGER half of that population downstream (33 of 45 effect canvases in one measured combat\n // scene), so a shader-only swap leaves most of the win on the table.\n //\n // DEFAULT FALSE — opt-in, unlike the shader option, and the mapping of `true` differs too:\n //\n // `true` ⇒ `{ gate: { kind: \"quiet-window\" } }` with the module defaults\n // (`DEFAULT_QUIET_WINDOW_MS`, and `DEFAULT_SURFACE_WATCHDOG_MS` — the quiet-window gate ships\n // with its watchdog). Deliberately NOT the `content-key` gate that `staticShaderImages: true`\n // means: that gate swaps on N consecutive unchanged observations and takes its second clock\n // from `noteStaticImageReconcile`, which the particle runtime never calls — a frozen system\n // paints ONCE and then its loop parks, so the count could never get there.\n //\n // FRAMES ARE NAMED. Within the quiet window, a paint whose frame IS a pure function of its\n // static-frame key (a pristine system whose textures have decoded) reports that key, so: N\n // identical systems share ONE encode and ONE object URL; a cache-hit re-blit re-states the frame\n // its `<img>` is already showing instead of thawing it; and a host may pin `gate.keyedQuietMs: 0`\n // to freeze such a surface the instant it paints. A live-simulating system, an undecoded texture\n // and an expired-burst blank all stay KEYLESS and earn nothing but the plain window. See\n // `notePaint` in `./particles/runtime` for the whole contract, and the swap module's KEYED-OR-QUIET\n // section for what a key a host cannot honour would cost.\n // `false` / unset ⇒ the mechanism is OFF: no swap state, no timers, no encodes, and every\n // binding takes exactly the path it took before the swap existed.\n // A `StaticSurfacePolicy` object supplies the policy in full: the quiet window length, the\n // watchdog cadence, encode pacing, a `canFreezeSurface` host veto, and injectable\n // clock/timer seams.\n //\n // WHY OPT-IN. The only gate a keyless surface can ever satisfy is `quiet-window`, and a quiet\n // window is a cadence only the HOST knows: how long its scene really stands still, and how much\n // stale-frame exposure it accepts (that gate trades away the content-key invariant and leans on\n // the watchdog's observable proxies). gsw will not guess that for every consumer, so it stays off\n // until asked. Live arm/kill switch: `ParticleRuntime.setStaticParticleImages`.\n //\n // ON A WEBGPU BINDING the swap runs through a capture READBACK rather than a canvas read (that\n // canvas cannot be read — see `effectsRenderer`); the frame is otherwise identical and the extra\n // cost is visible as `staticImageCaptureMs`.\n staticParticleImages?: StaticSurfaceOption;\n // FREEZE A NAMED PARTICLE BINDING AT MOUNT — warm it once, draw it once, never simulate it, and\n // where this document has ALREADY encoded that frame, mount it as an `<img>` with no canvas\n // context and no backing store at all. Default false: byte-identical behaviour.\n //\n // WHICH BINDINGS. Exactly the ones the swap policy's `canFreezeSurface(node, canvas)` accepts —\n // ONE predicate, two mechanisms, consulted ONCE per binding at its first sizing and never again.\n // A host that supplies no predicate is saying \"all of them\", which is what an absent veto already\n // means to the swap; a host that wants only some of its systems frozen must name them there.\n //\n // WHAT IT COSTS AND BUYS. A frozen-at-mount binding is skipped by the live loop entirely (no\n // simulate, no draw, and it does not hold the loop open), so a fleet of ambient emitters costs\n // nothing per frame even in a runtime that is otherwise LIVE — `staticParticles` without the\n // whole-runtime flip. What it gives up is motion: those systems show one representative frame\n // forever. The second appearance of a frame is where the real win is — with\n // `staticParticleImages` on AND `encode.stillCacheBytes` set (which is what keeps an encoded\n // frame alive after its last holder lets go), a binding whose key is already in hand mounts\n // straight to an `<img>`: nothing is simulated, drawn, allocated or encoded for it, and its\n // canvas never gets a context. Measured shape of the population this is for: two emitters per\n // card, 35 identical instances each, 2 distinct keys for 70 nodes.\n //\n // A frozen-at-mount surface takes its canvas back the moment the swap can no longer vouch for it\n // (the watchdog, a host `invalidateStaticSurfaces`, a re-size, a dormancy wake) or the runtime\n // leaves frozen mode — it builds the surface, pays the warm it skipped and draws, so nothing ever\n // uncovers a blank canvas. Probe it with `ParticleRuntime.stats()`: `staticStillCacheHits` /\n // `staticStillCacheMisses` (claims that found a still and claims that had to render),\n // `staticStillMounts` (claims that reached the screen) and `staticStillDonors` /\n // `staticStillDonorBakes` (departing bindings whose pixels were banked for their successors).\n staticParticleFreezeAtMount?: boolean;\n // Optional FPS cap for the WebGL shader runtime's rAF loop, mirroring `particleFps`. Live retune:\n // `WebglShaderRuntime.setFps`. TIME-driven\n // shaders re-render every animated frame; on a GPU-bound device a cap (e.g. 30) halves that draw\n // cost for an imperceptible change to slow pulses/scrolls. 0/undefined → uncapped (every rAF).\n shaderFps?: number;\n // How BOTH capped effect loops (shader + particle) arm their next tick. \"timer\" (default) PARKS\n // on a setTimeout until the cap boundary and re-enters through one rAF, so a capped loop costs\n // no wakeup on the display frames it would only skip; \"raf\" keeps a rAF armed every display\n // frame and skips in the tick (the pre-pacing behaviour, kept as the kill switch). Only meaningful\n // with `shaderFps`/`particleFps` — an uncapped loop arms rAF every frame either way. See\n // `./effects-loop-pacing`.\n effectsLoopPacing?: EffectsLoopPacing;\n // WHICH GPU API the live effect runtimes render with. Default `\"auto\"`.\n //\n // \"auto\" — use WebGPU where a real adapter exists, WebGL everywhere else. The choice is made\n // once per runtime and is SILENT: no throw, no console noise, no visual difference\n // beyond what the two rasterizers disagree about. Read it back from\n // `stats().renderer` (`pending` while the device is still being acquired, then\n // `webgpu`/`webgl`), with `webgpuFallbacks` / `webgpuFallbackReason` saying whether\n // and why WebGL was adopted instead.\n // \"webgl\" — today's path exactly, synchronously, with no WebGPU probe at all (so\n // `webgpuFallbacks` stays 0 — nothing was ever asked for). This is what a parity\n // reference or a benchmark's control arm pins.\n // \"webgpu\" — the SAME never-throw mechanics as \"auto\": a device that cannot be acquired still\n // falls back to WebGL rather than failing to render. It differs only in intent, which\n // is readable against the stats — a run that asked for \"webgpu\" and reports\n // `renderer: \"webgl\"` has a `webgpuFallbackReason` to explain itself, whereas \"auto\"\n // reporting the same thing is business as usual.\n //\n // WHY WEBGPU. Measured on a mid-range Android (docs/perf-harness.md S6/S7): the shipped WebGL\n // architecture renders every effect into one shared canvas and BLITS it onto each node's own 2D\n // canvas, which saturates Chrome's GPU process and halves whole-page update rates (89 → 47 Hz).\n // Rendering straight into each node's canvas — no shared canvas, no blit, one submit per frame —\n // restored 87 Hz. WebGL stays as the fallback and as the Godot-parity reference.\n //\n // WHAT DIFFERS ON WEBGPU BINDINGS. Both of the mechanisms below are 2D-canvas ones, and a WebGPU\n // canvas cannot be read back at all (`drawImage`/`toDataURL`/`toBlob` from one are blank headless\n // and pathological on Android), so neither can work the way it does on WebGL:\n // - `staticParticleImages` / `staticShaderImages` (the surface image swap) DOES run here, since\n // v2. It never reads the canvas: a frozen WebGPU binding is given a CAPTURE HOOK that\n // re-renders its current frame into an offscreen texture and copies the pixels back, and the\n // `<img>` is encoded from those. The swap's counters therefore move exactly as they do on\n // WebGL (`staticImageSwaps`, `staticImagesLive`, `staticImageEncodes`, …), plus two that only\n // this path books: `staticImageCaptures` and `staticImageCaptureMs`/`MaxMs`, which are kept\n // OUT of `staticImageEncodeMs` because a GPU readback does not park the main thread the way a\n // canvas `toBlob` does. A third, `staticImageBlankCaptures`, counts the readbacks REFUSED\n // because they came back entirely transparent for a frame the renderer knew it had drawn —\n // measured on real hardware under one launch mode, and a surface that hits it keeps its live\n // canvas rather than publishing a picture of nothing (see the swap module's BLANK CAPTURES).\n // - the frozen-frame cache that lets N identical frozen systems share one bitmap stays INERT\n // here: it trades a redraw for a 2D blit, and there is no 2D canvas to blit into. Each WebGPU\n // binding re-warms and re-draws its own frozen frame (cheap — the simulation is <3% of a core)\n // and `cacheHits` stays 0.\n // Neither changes what is on screen. A runtime that falls back to WebGL gets the cache back too.\n effectsRenderer?: \"auto\" | \"webgl\" | \"webgpu\";\n // OPT-IN per-frame cost attribution for BOTH live effect runtimes (particle + shader). A benchmark\n // that only sees \"the tick took 9 ms\" cannot tell a CPU-bound simulation from a fill-bound blit, and\n // those two have opposite fixes (lower `particleFps`/`amount` vs lower `renderScale`), so the\n // particle runtime brackets its tick into CPU SIM / instance-buffer BUILD / GL SUBMIT / GL→2D BLIT\n // and the shader runtime into GL SUBMIT / BLIT. Read them from `ParticleRuntime.stats().profile`\n // and `WebglShaderRuntime.stats().profile` (see `ParticleProfile` / `ShaderProfile`).\n //\n // OFF IS OFF, AND SAYS SO. Unset/false ⇒ `stats().profile` is `null` — not an object of zeros —\n // because \"not measured\" and \"measured, cost nothing\" are different facts and a benchmark that\n // confused them would report a fantasy. Off, the hot path takes no clock reading at all (the\n // brackets are behind one hoisted null check, so a production frame pays a predictable-branch\n // compare and nothing else); on, it pays a handful of `performance.now()` calls per binding per\n // frame, which is why this is a benchmark switch and not a default.\n //\n // GL TIMES ARE SUBMIT TIMES. The GPU runs asynchronously, so `glMs` measures how long the main\n // thread spent ISSUING the draw, never how long the GPU took to execute it. A GPU-bound frame shows\n // up as back-pressure elsewhere (typically the blit), not as a large `glMs`.\n effectsProfiling?: boolean;\n // Backing-store resolution multiplier for BOTH live runtimes' per-node canvases (shader + particle).\n // Live retune: `WebglShaderRuntime.setRenderScale` / `ParticleRuntime.setRenderScale`.\n // applied on top of `devicePixelRatio`. <1 renders the WebGL effects at a lower internal resolution\n // and lets the browser upscale the (CSS-sized) canvas — a near-linear GPU fill / blit saving for\n // GPU-bound (low-end) devices, at the cost of effect sharpness. Clamped to (0, 1]; default 1.\n renderScale?: number;\n // Frozen-TIME (single-shot) mode for the WebGL shader runtime. Live toggle:\n // `WebglShaderRuntime.setStaticShaders`. Render each shader ONCE at a pinned\n // representative TIME, then stop the loop, instead of re-rendering TIME-driven shaders every frame. The\n // still frame is correct for any shader (blend is a node CSS mix-blend-mode, applied regardless of frame\n // count) at ~zero ongoing GPU cost — the low-end fallback below an animated tier. Default false (animated).\n staticShaders?: boolean;\n // The representative TIME (seconds) the frozen frame renders at, when `staticShaders` is set. Tune it so\n // looping shaders (glows, ripples) land on a visible phase rather than a trough. Default 1.\n staticShaderTime?: number;\n // OPT-IN pinned backing-store ratio for FROZEN shader bindings (`staticShaders` mode only). Normally a\n // node canvas is sized `contentBox × window × devicePixelRatio × renderScale`, so every change to the\n // host's fit scale (rotation, fullscreen entry, a widescreen-stretch toggle, an adaptive `setRenderScale`\n // step) re-sizes every canvas — which CLEARS it, re-renders it, and changes its static-frame cache key, so\n // the whole frozen set re-renders. When this is set, a frozen binding is sized at THIS ratio instead, so\n // its backing store (and its cached frames) stop moving with the fit scale; `setRenderScale` then re-sizes\n // only LIVE bindings. The consumer picks the value — typically \"what fullscreen would be on this device\",\n // so the frozen art is rendered once at the resolution it will eventually be shown at. The resulting size\n // is clamped to `MAX_PINNED_BACKING_DIM` on its longest edge (aspect preserved).\n //\n // The trade is deliberate and one-directional: a frozen surface may be shown at a size it was not rendered\n // at (browser-scaled, softer or sharper) in exchange for not re-rendering the static set on a device that\n // chose static mode for performance. Unset ⇒ frozen bindings size exactly like live ones, i.e. today's\n // behavior for every consumer that does not opt in. Live retune: `WebglShaderRuntime.setStaticShaderPixelRatio`.\n //\n // SCOPE: this pins the DENSITY, not the node's layout box. A host that scales its scene with a CSS transform\n // leaves the content box (and therefore the whole backing size) fixed; a host that RE-LAYOUTS on a fit change\n // moves the box, and a frozen canvas still follows it — it has to, or the shader would be sampling the wrong\n // geometry. What the pin removes is the density axis: `devicePixelRatio × renderScale`, i.e. every fit-derived\n // or adaptive-quality scale step.\n staticShaderPixelRatio?: number;\n // Show a frozen shader surface as an `<img>` of its own rendered frame instead of its `<canvas>`.\n // A canvas that never changes still costs a compositor layer, a blend render surface and\n // per-frame GPU fill; measured on a phone for 24 never-changing surfaces, the `<img>` form is 6\n // layers against 31, 0 render surfaces against 4, 29 ms of GPU clear/fill against 140, and a\n // worst activation gap of 21 ms against 93.\n //\n // A surface that CHANGES is worse as an image (every change is an encode plus a main-thread\n // decode: at a 2 s update cadence the same measurement inverts to an 18.0 ms frame-cost p95\n // against 4.9), so a surface is only swapped once a GATE says it is standing still.\n //\n // `true` (the default) / unset ⇒ the gate that shipped first: FROZEN mode only, the\n // static-frame key must be observed unchanged across several renders/reconciles, and any\n // change after a swap puts that binding back on its canvas permanently.\n // `false` (or `WebglShaderRuntime.setStaticShaderImages(false)`, live) is the kill switch and\n // restores the canvas-only path exactly.\n // A `StaticSurfacePolicy` object supplies the policy instead: which gate (including\n // `quiet-window`, for surfaces with no usable content key — a SCREEN_UV vignette, a CRT\n // overlay), whether an invalidation blocks or retries, encode pacing, a host veto over\n // individual surfaces, and injectable clock/timer seams. See `./surface-image-swap`.\n //\n // ON A WEBGPU BINDING the swap runs through a capture READBACK rather than a canvas read (that\n // canvas cannot be read — see `effectsRenderer`); the frame is otherwise identical and the extra\n // cost is visible as `staticImageCaptureMs`.\n staticShaderImages?: StaticSurfaceOption;\n // Cap (longest edge, source px) for GL texture UPLOADS in the shader runtime: an image larger than this is\n // downscaled before `texImage2D`, so a huge full-screen background doesn't cost a ~250ms main-thread upload\n // spike when it loads. Aspect preserved (uvFit unchanged), shader samples at a lower internal resolution.\n // Unset/0 ⇒ upload at native size (back-compat default).\n maxTextureDimension?: number;\n // Opt-in SCREEN_TEXTURE/SCREEN_PIXEL_SIZE support for the WebGL shader runtime. When set,\n // a shader reading them compiles and the runtime feeds it an APPROXIMATE screen capture:\n // the scene's self-layers drawn BEFORE the node (DOM order) that overlap its on-screen\n // rect, composited onto an offscreen canvas from their already-loaded image/canvas\n // sources (text and other non-drawable paints are skipped), refreshed throttled (~300ms)\n // and on resize — never per frame. Unset/false ⇒ such shaders keep today's behavior\n // (treated as unsupported → CSS/SVG fallback), so existing consumers are unaffected.\n enableScreenTextureCapture?: boolean;\n // Cap (longest edge, px) for the SCREEN_TEXTURE capture canvas: the viewport-sized\n // composite is downscaled to fit, bounding the per-refresh 2D composite + GL upload\n // cost. Only meaningful with `enableScreenTextureCapture`. Unset/0 ⇒ 1024.\n maxScreenCaptureDim?: number;\n // Fail-loud sink for shaders/particles the live runtimes CANNOT render (unsupported shader construct,\n // compile failure, unresolved source, malformed particle spec). The node still falls back to its CSS/SVG/\n // preview paint; this only reports the gap. Unset ⇒ a deduped `console.warn`. A consumer (e.g. a CI corpus\n // gate) can supply its own reporter to collect the failures instead. See `./diagnostics`.\n onUnsupported?: UnsupportedRenderReporter;\n // ADDITIVE per-binding render notification for BOTH live effect runtimes (shader + particle): fired\n // synchronously right after a runtime HANDLED/PRESENTED that binding's current frame. The rule is\n // presentation, not the GL draw or necessarily a pixel write. That distinction is load-bearing\n // for a consumer that composites the canvas ITSELF (uploads it as a texture, blits it into its\n // own stage) and tracks which binding currently presents each frame. So it fires on:\n // * a real draw — the shader runtime's `renderNode` reaching `gl.drawArrays` (loop tick or the\n // synchronous `renderBindingNow` anti-flicker path) and the particle runtime's `drawBinding`\n // reaching `drawParticles`;\n // * a STATIC-FRAME CACHE-HIT BLIT, in both runtimes. This is the case that must not be dropped: a\n // fleet of N identical frozen surfaces settles at ONE draw and N-1 blits, so a consumer that only\n // heard about draws would composite exactly one of them and silently lose the rest;\n // * a shader same-canvas static-frame cache hit. It intentionally skips clearRect/drawImage\n // because its named pixels are already presented, but remains a handled frame notification.\n // * the particle CLEARS that write a blank canvas — a burst with no live instances left, an expired\n // burst retired, a cleared burst re-surfaced — because a consumer holding the burst's last frame\n // would otherwise keep painting it forever.\n // It is NOT fired where no frame was handled: a dirty skip, a suspended/dormant park, a zero-sized or\n // context-less canvas, a binding standing on a claimed still. The draw-vs-blit split is still\n // available, in the stats (`draws` / `cacheHits` / shader `blitSkips`) — this callback answers\n // \"was this frame presented/handled\", which is a different question from \"did pixels change\" or\n // \"did the GPU work\". `node` is the consumer's effect node element (the reconcile key), `canvas`\n // the runtime-owned canvas for that frame, `info` what the runtime knows\n // about the frame (see `GodotEffectRenderInfo`). Absent ⇒ byte-identical behavior.\n onBindingRendered?: (\n node: HTMLElement,\n canvas: HTMLCanvasElement,\n info: GodotEffectRenderInfo,\n ) => void;\n}\n\n/** Options accepted by a host that both emits a model and mounts its effects. */\nexport type GodotHtmlMountOptions = GodotHtmlRenderOptions &\n GodotHtmlRuntimeOptions;\n\nexport const RUNTIME_OPTION_KEYS = [\n \"enableWebglShaders\",\n \"enableParticles\",\n \"externalRuntimes\",\n \"resolveShaderSource\",\n \"particleMaxInstances\",\n \"particleFps\",\n \"particleRectCache\",\n \"particleObserverSizing\",\n \"particleDormant\",\n \"particleTravelExtents\",\n \"staticParticles\",\n \"staticParticleOneShotExpiry\",\n \"staticParticlePixelRatio\",\n \"parkStaticParticleBlend\",\n \"staticParticleImages\",\n \"staticParticleFreezeAtMount\",\n \"shaderFps\",\n \"effectsLoopPacing\",\n \"effectsRenderer\",\n \"effectsProfiling\",\n \"renderScale\",\n \"staticShaders\",\n \"staticShaderTime\",\n \"staticShaderPixelRatio\",\n \"staticShaderImages\",\n \"maxTextureDimension\",\n \"enableScreenTextureCapture\",\n \"maxScreenCaptureDim\",\n \"onUnsupported\",\n \"onBindingRendered\",\n] as const satisfies readonly (keyof GodotHtmlRuntimeOptions)[];\n","// The RENDER-BACKEND seam of the shader runtime (`./runtime`), plus its WebGL implementation.\n//\n// WHY: the runtime is ~95% renderer-agnostic lifecycle machinery — reconcile, dormancy, occlusion,\n// sizing, the rect cache, the frozen-surface image swap, the loop and its pacing. The\n// renderer-specific surface is narrow and provable: give a node canvas a context, resolve the\n// textures the shader samples, draw one frame. That surface is `ShaderRenderBackend`; everything\n// below it is today's `renderNode` + the GL half of `createBinding`, MOVED here unchanged. A\n// second (WebGPU) implementation then lands as a peer instead of a fork of the runtime.\n//\n// PER-BINDING, not per-runtime: the backend hangs off the BINDING (`NodeBinding.backend`), because\n// a shader that cannot run on WebGPU (it samples SCREEN_TEXTURE, or its WGSL transpile failed) must\n// fall back to GL BY ITSELF rather than veto the other N canvases. The GL path is per-binding\n// self-contained, which is what makes that possible.\n//\n// WHAT STAYS IN THE RUNTIME: everything that is DOM work or module-scoped cache. Canvas creation,\n// styling, window placement, the self-layer insert, the node's mix-blend-mode, the loading-fallback\n// suppression, the program cache, the static-frame cache, and the SCREEN_TEXTURE composite (a walk\n// of the scene's self-layers with `drawImage` — DOM, not GL). The backend reaches the last three\n// through `WebglShaderBackendDeps` so they keep their single home.\n\nimport {\n clearWebglSurface,\n deleteWebglTexture,\n drawGodotWebglShaderFrame,\n} from \"@godot-scene-web/canvas-effects/webgl\";\nimport { noteStaticFrame } from \"../surface-image-swap\";\nimport { bakeTexture, type TextureBakeSpec } from \"./bake-texture\";\nimport type {\n CompiledProgram,\n NodeBinding,\n SamplerBinding,\n ScreenCaptureState,\n ShaderProfile,\n ViewportRect,\n WebglShaderRuntimeStats,\n} from \"./runtime\";\nimport {\n ensureSharedDrawSize,\n getBakedTexture,\n getImageTexture,\n getRegionTexture,\n getSolidTexture,\n getTexture,\n nodeTextureRepeats,\n performanceNow,\n type SharedGl,\n type TextureEntry,\n} from \"./shared-gl\";\n\n/** One binding's node surface: the `<canvas>` the runtime created, plus its 2D context — which\n * exists ONLY on the WebGL backend, whose shared offscreen output is blitted onto it. A WebGPU\n * backend renders straight into the node canvas (a canvas can hold exactly ONE context type for\n * its lifetime), so its `ctx2d` is null and every 2D-only feature keys off that. */\nexport interface ShaderSurface {\n canvas: HTMLCanvasElement;\n ctx2d: CanvasRenderingContext2D | null;\n}\n\n/** A sampler's visual source, independent of the backend-specific texture-cache handle. The\n * frozen-frame key records this alongside dimensions: two same-size URLs or bake specs are not the\n * same pixels, and repeat changes the shader's sampling outside [0,1]. */\nexport type StaticSamplerIdentity = readonly [\"url\" | \"bake\", string, boolean];\n\n/** The structural subset of a texture entry the RUNTIME carries around: enough to size the UV fit\n * and TEXTURE_PIXEL_SIZE (`width`/`height`), to know whether the real pixels have arrived\n * (`loaded` — the frozen-frame cache gate and the loading-fallback clear) and to be told when they\n * do (`listeners`, see `onTextureLoaded`). Deliberately NOT `TextureEntry`: the GPU resource itself\n * is the backend's business, and a WebGPU entry carries no `WebGLTexture`. The twin of\n * `ParticleTextureHandle` in `../particles/render-backend`, for the same reason. */\nexport interface ShaderTextureHandle {\n width: number;\n height: number;\n loaded: boolean;\n listeners: Set<() => void>;\n}\n\n/** What resolving a binding's backend-typed textures needs. The url is resolved by the runtime (a\n * self-layer attribute or the background paint) BEFORE the create suppresses that paint. */\nexport interface ShaderTextureSpec {\n node: HTMLElement;\n selfLayer: HTMLElement;\n /** Resolved texture url; null ⇒ the solid-white stand-in (the shader supplies its own colour). */\n textureUrl: string | null;\n}\n\n/** A create: the texture spec plus the already-created, already-placed node canvas. */\nexport interface ShaderSurfaceSpec extends ShaderTextureSpec {\n canvas: HTMLCanvasElement;\n program: CompiledProgram;\n /** `uid ?? path` — the shader's identity, the key both the GL program cache and the WebGPU\n * pipeline cache use. The GL backend has no use for it (it is handed the compiled program); a\n * backend whose compilation unit is the shader looks its pipeline up by it. */\n shaderKey: string;\n}\n\n/** A created surface + the backend-typed textures the binding samples: the node TEXTURE, and the\n * user sampler uniforms in whatever slots that backend numbers them with (`SamplerBinding.unit`). */\nexport interface CreatedShaderSurface extends ShaderSurface {\n texture: ShaderTextureHandle;\n samplers: SamplerBinding[];\n}\n\n/** The renderer-specific half of the shader runtime. One implementation per graphics API; the\n * binding decides which one it uses (see the per-binding note above). */\nexport interface ShaderRenderBackend {\n readonly kind: \"webgl\" | \"webgpu\";\n /** Give the node canvas its context and resolve the binding's textures. Null ⇒ no context (the\n * create is abandoned and the node keeps its CSS/SVG paint, exactly as before). */\n createSurface(spec: ShaderSurfaceSpec): CreatedShaderSurface | null;\n /** Re-resolve the node TEXTURE after a texture-url / atlas-region attribute change. */\n resolveNodeTexture(spec: ShaderTextureSpec): ShaderTextureHandle;\n /** Re-resolve the user sampler uniforms after a sampler attribute change. */\n resolveSamplers(\n node: HTMLElement,\n program: CompiledProgram,\n ): SamplerBinding[];\n /** Release the binding-OWNED backend state (never the shared, cache-owned programs/textures). */\n disposeSurface(binding: NodeBinding): void;\n /** Render one binding's frame. Returns whether this call HANDLED/PRESENTED the binding's current\n * frame — a real GL draw, a static-frame cache-hit blit, or an unchanged same-canvas cache hit\n * (see `renderNodeGl`); false only where no frame could be handled (the zero-size /\n * no-2D-context skips). It is NOT a draw counter — `stats.draws`, `stats.cacheHits`, and\n * `stats.blitSkips` distinguish the paths. `prof` is the LOOP's\n * opt-in cost attribution; the out-of-loop anti-flicker render passes null. */\n renderNode(\n binding: NodeBinding,\n time: number,\n staticMode: boolean,\n getRootRect: () => ViewportRect,\n stats?: WebglShaderRuntimeStats,\n prof?: ShaderProfile | null,\n ): boolean;\n /** Bracket the binding loop of ONE tick. No-ops on WebGL (each draw is issued as it comes);\n * a WebGPU backend opens/submits its single per-tick command encoder here. */\n beginFrame(): void;\n endFrame(): void;\n /** Longest edge this backend can give a node canvas, folded into the sizing law, or undefined for\n * \"the device decides\". ABSENT on WebGL, whose shared drawing buffer discovers its own ceiling at\n * draw time (`ensureSharedDrawSize`) and scales the blit to cover the node anyway; a WebGPU canvas\n * past `maxTextureDimension2D` simply cannot produce a texture, so it has to be capped up front. */\n maxBackingDim?(): number | undefined;\n /** `queue.submit` calls this backend has made. ABSENT on WebGL, where the question is meaningless\n * (every draw is its own submission). */\n submits?(): number;\n /** Re-render this binding's CURRENT frame into an offscreen texture and read it back as tightly\n * packed RGBA (premultiplied, top-down, at the canvas's backing-store size), or null when it\n * cannot be produced. ABSENT on WebGL — that canvas holds readable 2D pixels, so a caller uses\n * `getImageData` on it. See `WebglShaderRuntime.captureNodePixels`. */\n captureSurface?(\n binding: NodeBinding,\n time: number,\n staticMode: boolean,\n getRootRect: () => ViewportRect,\n ): Promise<Uint8Array | null>;\n}\n\n/** The runtime-owned services the GL backend calls into: the per-runtime option it needs, the\n * SCREEN_TEXTURE composite, and the module-scoped static-frame cache — all of which stay in\n * `./runtime` (they are DOM work or shared caches, not GL). */\nexport interface WebglShaderBackendDeps {\n /** Longest-edge cap for texture uploads (`maxTextureDimension`); undefined ⇒ native size. */\n maxTextureDim: number | undefined;\n /** Ensure + (throttled) refresh this binding's SCREEN_TEXTURE capture, or null when it can't be\n * built. Called at the one point in the draw where the texture units are still free. */\n captureScreenTexture(\n binding: NodeBinding,\n rootRect: ViewportRect,\n ): ScreenCaptureState | null;\n /** Frozen-mode static-frame cache (module-scoped in the runtime, shared across runtimes and\n * remounts): the key for this render, an LRU-bumping lookup, and the post-draw snapshot. */\n staticFrameKey(\n binding: NodeBinding,\n w: number,\n h: number,\n time: number,\n ): string;\n lookupStaticFrame(key: string): HTMLCanvasElement | undefined;\n storeStaticFrame(\n key: string,\n source: HTMLCanvasElement,\n w: number,\n h: number,\n ): void;\n}\n\n/** The WebGL backend: one shared WebGL2 context draws every node into a shared offscreen buffer,\n * and each frame is blitted onto the node's own 2D canvas. `shared` is the process-wide context\n * from `./shared-gl` — the backend owns no context of its own. */\nexport function createWebglShaderBackend(\n shared: SharedGl,\n deps: WebglShaderBackendDeps,\n): ShaderRenderBackend {\n const { gl } = shared;\n return {\n kind: \"webgl\",\n createSurface(spec) {\n const ctx2d = spec.canvas.getContext(\"2d\");\n if (!ctx2d) return null;\n return {\n canvas: spec.canvas,\n ctx2d,\n texture: resolveNodeTexture(gl, spec, deps.maxTextureDim),\n samplers: resolveSamplers(\n gl,\n spec.node,\n spec.program,\n deps.maxTextureDim,\n ),\n };\n },\n resolveNodeTexture: (spec) =>\n resolveNodeTexture(gl, spec, deps.maxTextureDim),\n resolveSamplers: (node, program) =>\n resolveSamplers(gl, node, program, deps.maxTextureDim),\n disposeSurface(binding) {\n // No gl.deleteTexture/deleteProgram for the sampled textures/program: those are shared\n // module-cache entries reused across nodes/remounts (textureCache/programCache). The\n // SCREEN_TEXTURE capture texture is the one binding-OWNED piece of GL state.\n if (binding.screenCapture) {\n deleteWebglTexture(gl, binding.screenCapture.texture);\n binding.screenCapture = null;\n }\n },\n renderNode: (binding, time, staticMode, getRootRect, stats, prof) =>\n renderNodeGl(\n shared,\n deps,\n binding,\n time,\n staticMode,\n getRootRect,\n stats,\n prof,\n ),\n // GL submits every draw as it is issued, so a tick needs no bracketing.\n beginFrame() {},\n endFrame() {},\n };\n}\n\n// Returns whether this call HANDLED/PRESENTED the binding's current frame: true for a real GL draw,\n// a static-frame cache-hit blit, and a same-canvas cache hit whose pixels already match. False only\n// where no frame could be handled at all — the zero-size and no-2D-context skips. The runtime's\n// optional `onBindingRendered` notification keys off this: a consumer that COMPOSITES the canvas\n// itself must hear about every handled binding, while `blitSkips` says no pixels changed. See\n// `GodotHtmlRuntimeOptions.onBindingRendered`.\nfunction renderNodeGl(\n sharedGl: SharedGl,\n deps: WebglShaderBackendDeps,\n binding: NodeBinding,\n time: number,\n staticMode: boolean,\n getRootRect: () => ViewportRect,\n stats?: WebglShaderRuntimeStats,\n // The LOOP's cost attribution (see `ShaderProfile`), or null/absent — the out-of-loop\n // `renderBindingNow` and every test caller pass nothing and take the un-timed path. Every bracket\n // below sits behind this one null check, so an unprofiled render reads no clock.\n prof: ShaderProfile | null = null,\n): boolean {\n const { gl } = sharedGl;\n const w = binding.canvas.width;\n const h = binding.canvas.height;\n if (w < 1 || h < 1) return false;\n // The blit target. Always present on a GL binding (the create fails without it); the null case is\n // the type talking about a WebGPU surface, which this backend never renders into.\n const ctx2d = binding.ctx2d;\n if (!ctx2d) return false;\n // Static-frame cache: in frozen mode, reuse an identical previously-rendered frame and skip the GL draw.\n // Excluded: SCREEN_UV shaders (output depends on on-screen position, not node-local inputs),\n // SCREEN_TEXTURE shaders (output depends on the content drawn behind the node) and not-yet-\n // loaded textures (a placeholder frame must not be cached — the texture-load listener re-renders when ready).\n const cacheable =\n staticMode &&\n !binding.program.usesScreenUv &&\n !binding.program.usesScreenTexture &&\n texturesLoaded(binding);\n let cacheKey: string | null = null;\n if (cacheable) {\n cacheKey = deps.staticFrameKey(binding, w, h, time);\n const hit = deps.lookupStaticFrame(cacheKey);\n if (hit) {\n if (stats) stats.cacheHits++;\n // This target already presents the exact named frame. The key includes backing dimensions and\n // every frozen input, while resize clears the canvas before changing dimensions, so clearing\n // and re-blitting would only spend main-thread fill work. It is still a handled frame: retain\n // the static-frame/swap bookkeeping and let the runtime notify its consumer below.\n if (binding.lastStaticKey === cacheKey) {\n if (stats) stats.blitSkips++;\n } else {\n const hitBlitStart = prof ? performanceNow() : 0;\n ctx2d.clearRect(0, 0, w, h);\n ctx2d.drawImage(hit, 0, 0);\n if (prof) prof.blitMs += performanceNow() - hitBlitStart;\n }\n // The canvas now holds exactly the frame `cacheKey` names — the invariant the image swap's\n // gate (and its revert) needs. See `../surface-image-swap`. The same fact, published to the\n // outside consumer through `GodotEffectRenderInfo.staticKey`: this canvas and the one that\n // really drew hold the SAME pixels, which is what lets a host composite them from one texture.\n binding.lastStaticKey = cacheKey;\n if (binding.staticImage && stats)\n noteStaticFrame(binding, cacheKey, stats);\n // A cache hit is presented/handled and reported as one. A cross-canvas hit wrote pixels; a\n // same-canvas hit confirms that this binding still presents the named frame. In both cases a\n // consumer tracking effect surfaces must hear about it, and `blitSkips` distinguishes the latter.\n return true;\n }\n }\n // GROW-ONLY, VERIFIED: the shared backbuffer only ever grows to the largest node seen (a per-node\n // per-frame realloc was the dominant profiling cost); each node renders into a bottom-left sub-rect\n // (GL origin is bottom-left) and blits just that region. The grow is verified against the ACTUAL\n // drawing buffer (`ensureSharedDrawSize`): the buffer can come back smaller than the attribute (GPU\n // max size, failed realloc keeping the old buffer), and drawing/blitting at the attribute size then\n // reads out-of-bounds → the black band on a node whose backing outgrew the buffer. `vw`×`vh` ≤ w×h\n // is what can really be drawn; the blit below scales it up to the node's full canvas.\n const { vw, vh, bufH } = ensureSharedDrawSize(sharedGl, w, h);\n\n // GL SUBMIT bucket, first segment: the viewport/clear/program preamble. It stops at the\n // SCREEN_TEXTURE capture below and resumes after it, because that capture is a 2D composite of\n // the DOM plus a texture upload on its OWN ~300 ms throttle — real cost, but not per-frame draw\n // submit, and charging it here would make `glMs` spike on the frames it happens to refresh.\n const glPreambleStart = prof ? performanceNow() : 0;\n clearWebglSurface(gl, vw, vh, true);\n const { program } = binding;\n if (prof) prof.glMs += performanceNow() - glPreambleStart;\n\n // Refresh the (throttled) SCREEN_TEXTURE capture BEFORE the draw's texture units are\n // bound — the upload binds TEXTURE_2D itself, so it must not clobber unit 0/1..N.\n const screenCapture = program.usesScreenTexture\n ? deps.captureScreenTexture(binding, getRootRect())\n : null;\n\n // …and the second segment: texture binds, uniform writes, the draw call.\n const glDrawStart = prof ? performanceNow() : 0;\n const fit = uvFit(binding, w, h);\n // The node-local sub-rect this canvas covers (default full); the shader remaps GODOT_UV through it.\n let screenOrigin: [number, number] | undefined;\n let screenSize: [number, number] | undefined;\n let screenPixelSize: [number, number] | undefined;\n if (program.usesScreenUv) {\n const [ox, oy, sx, sy] = screenRect(binding, getRootRect());\n screenOrigin = [ox, oy];\n screenSize = [sx, sy];\n }\n if (program.usesScreenPixelSize) {\n // 1/captureSize when a capture exists (matches the texel grid the shader samples);\n // else 1/viewport (CSS px) — the natural screen-pixel size without a capture.\n const rootRect = screenCapture ? null : getRootRect();\n const sw = screenCapture\n ? screenCapture.width\n : Math.max(1, rootRect?.width ?? 1);\n const sh = screenCapture\n ? screenCapture.height\n : Math.max(1, rootRect?.height ?? 1);\n screenPixelSize = [1 / sw, 1 / sh];\n }\n drawGodotWebglShaderFrame(gl, {\n program: program.program,\n locations: program.uniformLocations,\n uniforms: program.uniforms,\n quad: sharedGl.quad,\n width: vw,\n height: vh,\n texture: glTextureOf(binding.texture),\n time: program.usesTime ? time : undefined,\n texturePixelSize: program.usesTexturePixelSize\n ? [1 / binding.texture.width, 1 / binding.texture.height]\n : undefined,\n modulate: binding.modulate,\n uvFit: fit,\n uvWindow: binding.window,\n screenOrigin,\n screenSize,\n screenTexture: screenCapture?.texture,\n screenTextureUnit: SCREEN_TEXTURE_UNIT,\n screenPixelSize,\n samplers: binding.samplers.map((sampler) => ({\n name: sampler.name,\n unit: sampler.unit,\n texture: glTextureOf(sampler.entry),\n })),\n params: binding.params,\n paramKinds: binding.paramKinds,\n });\n if (prof) prof.glMs += performanceNow() - glDrawStart;\n if (stats) stats.draws++;\n\n // Blit the shared GL output onto this node's own 2D canvas. The node rendered into the BOTTOM-left\n // vw×vh of the (possibly larger) shared canvas; in the top-down 2D canvas that's the rows\n // [bufH - vh, …). The dest covers the node's FULL w×h backing: when the buffer capped the draw\n // below w×h, the frame scales up (full content at reduced resolution) instead of leaving the\n // out-of-window remainder transparent (the black band).\n const blitStart = prof ? performanceNow() : 0;\n ctx2d.clearRect(0, 0, w, h);\n ctx2d.drawImage(sharedGl.canvas, 0, bufH - vh, vw, vh, 0, 0, w, h);\n if (prof) prof.blitMs += performanceNow() - blitStart;\n\n // Cache this frozen frame so an identical node (or this node re-created) reuses it without a GL draw.\n if (cacheable && cacheKey)\n deps.storeStaticFrame(cacheKey, binding.canvas, w, h);\n // What this canvas now holds, for the consumer (see the cache-hit branch above). NULL on every\n // un-cacheable frame — live TIME, a screen-space shader, a texture still decoding — because those\n // frames are not a pure function of any key and must never be shared by one.\n binding.lastStaticKey = cacheKey;\n // Feed the image swap the key this canvas now holds — a null key (live mode, a screen-space\n // shader, textures still loading) retires any live swap, since the surface is not frozen output.\n if (binding.staticImage && stats) noteStaticFrame(binding, cacheKey, stats);\n return true;\n}\n\n// This backend only ever renders bindings IT created (the binding remembers the backend that made\n// its surface, and a backend swap re-creates the binding on a NEW canvas), so every handle it is\n// handed really is a GL `TextureEntry` — the same discipline as `glTextureOf` in\n// `../particles/render-backend`.\nfunction glTextureOf(handle: ShaderTextureHandle): WebGLTexture | null {\n return (handle as TextureEntry).texture;\n}\n\n// SCREEN_TEXTURE binds on a FIXED high unit, clear of unit 0 (the node TEXTURE) and\n// `resolveSamplers`' 1..N user-sampler range (real shaders carry a handful of samplers,\n// nowhere near 8).\nconst SCREEN_TEXTURE_UNIT = 8;\n\n/** Quantize a key number so near-identical params collapse to one cache entry (absorbs streamed\n * jitter). Exported because `./runtime` builds the PRECOMPUTED halves of the key below with it. */\nexport function quantizeForKey(value: number): number {\n return Math.round(value * 100) / 100;\n}\n\n/**\n * The CONTENT KEY of one frozen frame: everything a re-render of this binding at this size and this\n * pinned time would read. Two renders that agree on it produce the same pixels, which is what makes\n * it usable as both a frame-cache address and the image swap's stability evidence.\n *\n * RENDERER-AGNOSTIC, and here rather than in `./runtime` for exactly that reason: the WebGL backend\n * addresses its static-frame CACHE with it, the WebGPU backend (`../webgpu/render-shader`) has no\n * such cache but names its frozen frames with the same string so the swap's gate observes the same\n * identity on either renderer. Every term is a node-local input — which is why a SCREEN_UV or\n * SCREEN_TEXTURE shader is excluded by its caller instead of being keyed here.\n */\nexport function staticFrameKey(\n binding: NodeBinding,\n w: number,\n h: number,\n time: number,\n): string {\n // JSON is intentional framing, rather than the old delimiter-concatenation: shader URLs, names\n // and serialized bake specs can themselves contain every delimiter. Do not read the DOM here;\n // both renderers call this pure function and the binding stores its canonical resolved inputs.\n return JSON.stringify([\n \"gsw-static-frame/v2\",\n binding.shaderKey,\n [w, h],\n binding.fit,\n [\n binding.textureUrl,\n binding.textureRepeat,\n binding.textureRegion\n ? [\n binding.textureRegion.x,\n binding.textureRegion.y,\n binding.textureRegion.width,\n binding.textureRegion.height,\n ]\n : null,\n [binding.texture.width, binding.texture.height],\n ],\n binding.modulateKey,\n binding.samplers.map((sampler) => [\n sampler.name,\n sampler.frameIdentity,\n [sampler.entry.width, sampler.entry.height],\n ]),\n binding.windowKey,\n quantizeForKey(time),\n binding.paramsKey,\n binding.paramKindsKey,\n ]);\n}\n\n/** The hot-path wrapper around the pure staticFrameKey. Its epoch is advanced by the runtime at\n * every effective input writer; dimensions and time remain explicit memo coordinates because they\n * are render-call inputs rather than binding fields. */\nexport function memoStaticFrameKey(\n binding: NodeBinding,\n w: number,\n h: number,\n time: number,\n): string {\n // The memo coordinate must be the same representative TIME the key names. Static callers\n // normally pass one pinned value, but a direct/capture caller inside this 0.01 quantum must\n // reuse the same memo rather than doing a useless re-serialization.\n const keyTime = quantizeForKey(time);\n const memo = binding.staticKeyMemo;\n if (\n memo &&\n memo.epoch === binding.staticKeyEpoch &&\n memo.w === w &&\n memo.h === h &&\n memo.time === keyTime\n ) {\n return memo.key;\n }\n const key = staticFrameKey(binding, w, h, time);\n binding.staticKeyMemo = {\n epoch: binding.staticKeyEpoch,\n w,\n h,\n time: keyTime,\n key,\n };\n return key;\n}\n\n/** Have the node TEXTURE and every user sampler finished loading? (A placeholder frame must not be\n * cached, and the loading fallback must not be cleared, until they have.) */\nexport function texturesLoaded(binding: NodeBinding): boolean {\n return (\n binding.texture.loaded &&\n binding.samplers.every((sampler) => sampler.entry.loaded)\n );\n}\n\n// The binding's cached self-layer rect, measured on demand if the batched read hasn't covered it\n// yet (a fresh binding, or a render outside the loop). Refreshed — never read per frame — by the\n// runtime's `refreshRects`.\nexport function layerRect(binding: NodeBinding): ViewportRect {\n if (!binding.layerRect) {\n binding.layerRect = binding.selfLayer.getBoundingClientRect();\n }\n return binding.layerRect;\n}\n\n/** The node's normalized rect within the scene-root viewport — Godot's SCREEN_UV\n * domain (origin + the node-local UV scaled by the node's on-screen size). Tracks layout/scroll\n * via the (invalidated + TTL-refreshed) rect cache; scale/zoom cancels (node ÷ root).\n * Exported so a second backend feeds its shader the SAME numbers rather than a re-derivation. */\nexport function screenRect(\n binding: NodeBinding,\n root: ViewportRect,\n): [number, number, number, number] {\n const node = layerRect(binding);\n const rw = root.width || 1;\n const rh = root.height || 1;\n return [\n (node.left - root.left) / rw,\n (node.top - root.top) / rh,\n node.width / rw,\n node.height / rh,\n ];\n}\n\n/** The UV scale so the texture is contained/covered/filled in the node rect like\n * Godot's TextureRect stretch (mirrors the self-layer `background-size`). Returns\n * the fraction of the canvas the drawn texture occupies on each axis; the shader\n * maps `UV = (v_uv - 0.5) / fit + 0.5`. Exported for the same reason as `screenRect`:\n * two backends must not each own a copy of this arithmetic. */\nexport function uvFit(\n binding: NodeBinding,\n canvasW: number,\n canvasH: number,\n): [number, number] {\n if (binding.fit === \"fill\") return [1, 1];\n const tw = binding.texture.width;\n const th = binding.texture.height;\n // The texture is fit to the FULL node box, and the windowed GODOT_UV is in box-UV space, so contain/cover\n // must be computed against the box dimensions (canvas ÷ window), not the sub-rect canvas. Full window ⇒ box\n // == canvas (unchanged).\n const boxW = canvasW / binding.window[2];\n const boxH = canvasH / binding.window[3];\n if (tw <= 0 || th <= 0 || boxW <= 0 || boxH <= 0) return [1, 1];\n const scale =\n binding.fit === \"cover\"\n ? Math.max(boxW / tw, boxH / th)\n : Math.min(boxW / tw, boxH / th);\n return [(tw * scale) / boxW, (th * scale) / boxH];\n}\n\n// ---- texture resolution ----------------------------------------------------\n\n// The shader's input texture: the atlas SUB-RECT when the self-layer carries a region (map-node/relic icons),\n// else the whole image. Cropping is what stops an atlas-sprite recolor shader from sampling page padding →\n// white; with the sprite filling UV [0,1], `uvFit`/`TEXTURE_PIXEL_SIZE` also come out right unchanged. With no\n// url at all the shader samples a 1×1 solid white (it supplies its own colour).\nfunction resolveNodeTexture(\n gl: WebGL2RenderingContext,\n spec: ShaderTextureSpec,\n maxDim: number | undefined,\n): TextureEntry {\n const { node, selfLayer, textureUrl } = spec;\n if (!textureUrl) return getSolidTexture(gl, \"white\", [255, 255, 255, 255]);\n const repeat = nodeTextureRepeats(node);\n const region = parseAtlasRegion(\n selfLayer.getAttribute(\"data-godot-atlas-region\"),\n );\n return region\n ? getRegionTexture(gl, textureUrl, region, { repeat, maxDim })\n : getTexture(gl, textureUrl, repeat, maxDim);\n}\n\n// Parse `data-godot-atlas-region` (\"x,y,w,h\" in atlas page px) into a positive sub-rect, or null when absent\n// (a non-atlas texture) or malformed. It's stamped on every atlas TextureRect/Sprite2D self-layer for the CSS\n// crop, so it doubles as the signal to sample only the sprite's sub-rect of the atlas PAGE. Exported for tests\n// (the GL crop itself needs a real WebGL2 context — device-verified).\nexport function parseAtlasRegion(\n attr: string | null,\n): { x: number; y: number; width: number; height: number } | null {\n if (!attr) return null;\n const parts = attr.split(\",\").map((value) => Number(value.trim()));\n if (parts.length < 4 || parts.some((value) => !Number.isFinite(value))) {\n return null;\n }\n const [x, y, width, height] = parts;\n return width > 0 && height > 0 ? { x, y, width, height } : null;\n}\n\n// Bake or load the shader's user sampler uniforms from the node attributes emitted\n// by material.ts. Each gets its own texture unit (1..N; unit 0 is the node TEXTURE).\nfunction resolveSamplers(\n gl: WebGL2RenderingContext,\n node: HTMLElement,\n program: CompiledProgram,\n maxTextureDim?: number,\n): SamplerBinding[] {\n if (program.samplers.length === 0) return [];\n const specs = parseSamplerSpecs(\n node.getAttribute(\"data-godot-shader-samplers\"),\n );\n const urls = parseSamplerUrls(\n node.getAttribute(\"data-godot-shader-sampler-urls\"),\n );\n const out: SamplerBinding[] = [];\n let unit = 1;\n for (const sampler of program.samplers) {\n const url = urls[sampler.name];\n if (url) {\n out.push({\n name: sampler.name,\n entry: getImageTexture(gl, url, {\n repeat: sampler.repeat,\n maxDim: maxTextureDim,\n }),\n unit,\n frameIdentity: [\"url\", url, sampler.repeat],\n });\n unit += 1;\n continue;\n }\n const spec = specs[sampler.name];\n if (!spec) continue; // no resolvable source -> sampler stays unbound (reads 0)\n const specIdentity = JSON.stringify(spec) ?? \"undefined\";\n const key = `${sampler.name}:${specIdentity}`;\n const entry = getBakedTexture(gl, key, () => bakeTexture(spec), {\n repeat: sampler.repeat,\n });\n out.push({\n name: sampler.name,\n entry,\n unit,\n frameIdentity: [\"bake\", specIdentity, sampler.repeat],\n });\n unit += 1;\n }\n return out;\n}\n\n/** The `data-godot-shader-samplers` bake specs, by uniform name. Exported so a second backend reads\n * the SAME attributes with the same tolerance for garbage (a malformed value is \"no samplers\", never\n * a throw in a render path). */\nexport function parseSamplerSpecs(\n value: string | null,\n): Record<string, TextureBakeSpec> {\n if (!value) return {};\n try {\n const parsed = JSON.parse(value);\n return typeof parsed === \"object\" && parsed ? parsed : {};\n } catch {\n return {};\n }\n}\n\n/** The `data-godot-shader-sampler-urls` map, by uniform name. Exported alongside\n * `parseSamplerSpecs`, and for the same reason. */\nexport function parseSamplerUrls(value: string | null): Record<string, string> {\n if (!value) return {};\n try {\n const parsed = JSON.parse(value);\n if (!parsed || typeof parsed !== \"object\") return {};\n const out: Record<string, string> = {};\n for (const [key, raw] of Object.entries(parsed)) {\n if (typeof raw === \"string\" && raw !== \"\") out[key] = raw;\n }\n return out;\n } catch {\n return {};\n }\n}\n\n// ---- uniform setters -------------------------------------------------------\n","// The WebGPU implementation of `ShaderRenderBackend` — the peer of `../webgl/shader-backend`'s\n// WebGL one, rendering each Godot `ShaderMaterial` node STRAIGHT into its own canvas.\n//\n// WHY IT EXISTS (docs/perf-harness.md S6/S7, measured on a mid-range Android): the WebGL path draws\n// every node into one shared offscreen canvas and BLITS the result onto the node's 2D canvas. That\n// blit chain saturates Chrome's GPU process and halves whole-page update rates (89 → 47 Hz).\n// Presenting from a per-node WebGPU canvas — no shared canvas, no blit, ONE `queue.submit` per tick\n// — restored 87 Hz.\n//\n// PER-SHADER, NOT PER-RUNTIME. Each Godot shader gets its own WGSL module, bind-group layout and\n// pipeline, cached at DEVICE scope and keyed by the same shader id the GL program cache uses. A\n// shader this backend cannot express — it samples SCREEN_TEXTURE, its WGSL failed to compile, its\n// pipeline failed validation — is remembered as `\"webgl-only\"` and its BINDING renders on the WebGL\n// backend instead (the runtime counts `webgpuBindingFallbacks`). One screen-reading shader must not\n// veto the other N canvases, which is the whole reason the backend hangs off the binding.\n//\n// THE THREE PAIRINGS THAT ARE LOAD-BEARING AND SILENT WHEN BROKEN:\n// 1. `alphaMode: \"premultiplied\"` + a fragment returning `vec4f(rgb*a, a)` + `PREMULTIPLIED_BLEND`.\n// The transpiler emits the premultiplied return; this file supplies the other two. A straight\n// fragment under this blend halos; a premultiplied one under src-alpha double-multiplies.\n// Neither raises an error.\n// 2. The uniform struct is one opaque byte block. `./pack-uniforms` writes it at the offsets\n// `./transpile-wgsl` computed; nothing here re-derives an offset.\n// 3. A texture's `GPUTextureView` is REPLACED when its image decodes (`./textures`), so any bind\n// group built from the placeholder is stale from that moment. `ensureBindGroup` compares the\n// views it bound against the current ones — a check that cannot miss — and the `loaded`\n// listeners registered at create just save it a frame.\n//\n// WHAT THIS BACKEND DELIBERATELY DOES NOT DO: consult the frozen-frame CACHE. That cache trades a\n// redraw for a 2D-canvas blit and there is no 2D canvas here, so the runtime keeps it for its WebGL\n// bindings and never arms it on a WebGPU one (`cacheHits` stays 0 on this renderer, by design).\n//\n// WHAT IT DOES DO, since v2: feed the surface image SWAP. `renderNode` names each frozen frame with\n// the shared `staticFrameKey`, which is all the swap's gate needs; the pixels themselves come from\n// `captureSurface` below — an offscreen re-render plus `copyTextureToBuffer`, never a read of the\n// canvas, because `drawImage`/`toDataURL`/`toBlob` on a WebGPU canvas is blank headless and\n// pathological on Android (S7).\n\nimport {\n compileModule,\n createPipeline,\n createUniformStaging,\n createWebgpuShaderBindGroup,\n createWebgpuShaderBindGroupLayout,\n createWebgpuShaderUniformBuffer,\n packShaderUniforms,\n readTexturePixels,\n type ShaderUniformValues,\n type UniformStaging,\n WebgpuShaderExecutor,\n} from \"@godot-scene-web/canvas-effects/webgpu\";\nimport {\n type TranspiledWgslShader,\n transpileGodotShaderWgsl,\n} from \"@godot-scene-web/effects/shaders\";\nimport { noteStaticFrame } from \"../surface-image-swap\";\nimport { bakeTexture } from \"../webgl/bake-texture\";\nimport type {\n CompiledProgram,\n NodeBinding,\n SamplerBinding,\n ViewportRect,\n} from \"../webgl/runtime\";\nimport {\n memoStaticFrameKey,\n parseAtlasRegion,\n parseSamplerSpecs,\n parseSamplerUrls,\n type ShaderRenderBackend,\n type ShaderTextureHandle,\n type ShaderTextureSpec,\n screenRect,\n texturesLoaded,\n uvFit,\n} from \"../webgl/shader-backend\";\nimport {\n nodeTextureRepeats,\n onTextureLoaded,\n performanceNow,\n} from \"../webgl/shared-gl\";\nimport {\n configureCanvas,\n latchWebgpuFallbackReason,\n SHADER_STAGE,\n type WebgpuShared,\n} from \"./device\";\nimport {\n type GpuTextureEntry,\n getBakedTextureGpu,\n getImageTextureGpu,\n getRegionTextureGpu,\n} from \"./textures\";\n\n/**\n * `one / one-minus-src-alpha` on colour AND alpha: the composite for fragments that are already\n * premultiplied, which is the only kind a `alphaMode: \"premultiplied\"` canvas can present.\n *\n * DELIBERATELY THE ONLY BLEND STATE HERE. Godot's `render_mode blend_*` is NOT translated into a GPU\n * blend state on either backend: it is applied as a CSS `mix-blend-mode` on the NODE element\n * (`blendToMixBlendMode` in `../webgl/runtime`), because the thing an additive shader must add to is\n * the DOM painted behind the node, which no blend inside the node's own canvas can reach. Turning\n * `blend_add` into `ADDITIVE_BLEND` here would additionally blend the shader against the canvas's own\n * cleared transparent black — a no-op that then double-applies once CSS does the real compositing.\n * (The identical constant in `../particles/render-webgpu` is a local copy for the same reason: it is\n * one line of measured law, and an import across the particle/shader seam would be the only edge\n * between two otherwise independent renderers.)\n */\nconst PREMULTIPLIED_BLEND: GPUBlendState = {\n color: {\n srcFactor: \"one\",\n dstFactor: \"one-minus-src-alpha\",\n operation: \"add\",\n },\n alpha: {\n srcFactor: \"one\",\n dstFactor: \"one-minus-src-alpha\",\n operation: \"add\",\n },\n};\n\nconst _TRANSPARENT: GPUColor = { r: 0, g: 0, b: 0, a: 0 };\n\n/** Readback target format. A canvas is usually `bgra8unorm`, and a pipeline's fragment target format\n * must match its attachment — hence the twin pipeline in `captureSurface`. */\nconst CAPTURE_FORMAT: GPUTextureFormat = \"rgba8unorm\";\n\n/** The stand-in the node samples when it has no texture url at all: a 1×1 opaque WHITE, so the\n * implicit `COLOR = texture(TEXTURE, UV)` yields the shader's own colour unchanged. Same key string\n * as `getSolidTexture(gl, \"white\", …)` in `../webgl/shared-gl`. */\nconst SOLID_WHITE_KEY = \"solid:white\";\n\n/** One shader compiled for one device. */\ninterface GpuShaderProgram {\n transpiled: TranspiledWgslShader;\n module: GPUShaderModule;\n bindGroupLayout: GPUBindGroupLayout;\n pipelineLayout: GPUPipelineLayout;\n pipeline: GPURenderPipeline;\n /** The `rgba8unorm`-targeted twin, compiled on the first `captureSurface`. Null until then — a\n * runtime that never captures never pays for it. */\n capture: GPURenderPipeline | null;\n}\n\n/** Device-scope render state: every shader this page has compiled, plus the in-flight compiles so N\n * bindings of one shader created in one reconcile share a single transpile+compile. Rebuilt from\n * scratch when the device changes, exactly like the texture cache — pipelines belong to the device\n * that made them. */\ninterface ShaderProgramsGpu {\n device: GPUDevice;\n format: GPUTextureFormat;\n /** `\"webgl-only\"` is a CACHED DECISION, not a missing entry: a shader that cannot be expressed in\n * WGSL cannot become expressible later, and re-transpiling it per binding would pay for the same\n * answer N times. */\n shaders: Map<string, GpuShaderProgram | \"webgl-only\">;\n pending: Map<string, Promise<boolean>>;\n}\n\n// The device-scope memo, in the shape of `./device`'s and `../particles/render-webgpu`'s: `settled`\n// is the SYNC view the runtime's gate peeks at so a second runtime adopts without another\n// round-trip.\nlet programsMemo: Promise<ShaderProgramsGpu | null> | undefined;\nlet programsSettled: ShaderProgramsGpu | null | undefined;\nlet programsDevice: GPUDevice | null = null;\n\nfunction acquireShaderPrograms(\n shared: WebgpuShared,\n): Promise<ShaderProgramsGpu | null> {\n if (programsDevice !== shared.device) {\n programsMemo = undefined;\n programsSettled = undefined;\n programsDevice = shared.device;\n }\n if (programsMemo) return programsMemo;\n // ASYNC even though nothing shader-independent needs compiling: this backend's unit of compilation\n // is the SHADER, and shaders arrive one at a time as bindings are created (`prepareShader`). The\n // promise is the contract the runtime's gate already speaks — the same one the particle backend\n // uses to await its fixed WGSL — and it is where a future device-scope resource would land.\n programsMemo = Promise.resolve<ShaderProgramsGpu>({\n device: shared.device,\n format: shared.format,\n shaders: new Map(),\n pending: new Map(),\n }).then((programs) => {\n programsSettled = programs;\n return programs;\n });\n return programsMemo;\n}\n\n/** `undefined` = not built yet (the caller must await `createWebgpuShaderBackend`), otherwise the\n * device's program cache. */\nfunction peekShaderPrograms(\n shared: WebgpuShared,\n): ShaderProgramsGpu | null | undefined {\n if (programsDevice !== shared.device) return undefined;\n return programsSettled;\n}\n\n/** Per-binding GPU state, keyed by the node CANVAS rather than stored on the binding: a canvas holds\n * exactly ONE context for its lifetime, so it is the natural identity for the context and the\n * buffers hanging off it, and keeping it here leaves `NodeBinding` renderer-agnostic. */\ninterface WebgpuShaderSurface {\n context: GPUCanvasContext;\n program: GpuShaderProgram;\n staging: UniformStaging;\n uniformBuffer: GPUBuffer;\n bindGroup: GPUBindGroup | null;\n /** The views `bindGroup` was built from, in binding order — see the header's point 3. */\n boundViews: unknown[];\n bindGroupDirty: boolean;\n disposers: Array<() => void>;\n executor: WebgpuShaderExecutor;\n}\n\nconst surfaces = new WeakMap<HTMLCanvasElement, WebgpuShaderSurface>();\n\n/** The WebGPU backend's own surface over `ShaderRenderBackend`: the same interface every binding\n * drives, plus the two calls the runtime's gate needs to decide PER BINDING whether this backend can\n * render a given shader at all. */\nexport interface WebgpuShaderBackend extends ShaderRenderBackend {\n readonly kind: \"webgpu\";\n /**\n * Compile `shaderKey` for this device, resolving TRUE when a binding of it can render here and\n * FALSE when it is webgl-only (unsupported construct, failed WGSL compile, failed pipeline\n * validation). Never throws and never fails the backend — that asymmetry is the point.\n *\n * `source` may be undefined when the caller could not resolve it; the answer is then false and is\n * NOT cached, since a later create may well have the source.\n */\n prepareShader(\n shaderKey: string,\n source: string | undefined,\n ): Promise<boolean>;\n /** The SYNC view of `prepareShader`: `undefined` = not decided yet. */\n peekShader(shaderKey: string): boolean | undefined;\n submits(): number;\n}\n\nexport interface WebgpuShaderBackendDeps {\n /** Longest-edge cap for texture uploads, from `maxTextureDimension`. Accepted for signature parity\n * with the WebGL backend and DELIBERATELY UNUSED in v1: the GL cap exists to avoid a multi-hundred\n * -millisecond main-thread `texImage2D`, a cost a queue copy does not have, and honouring it would\n * change the uploaded size and therefore `TEXTURE_PIXEL_SIZE` — a parity difference for a\n * mitigation this path does not need. */\n maxTextureDim?: number | undefined;\n}\n\n/**\n * The WebGPU shader backend over an already-acquired device, or NULL when this device cannot host one.\n *\n * ASYNC, unlike `createWebglShaderBackend`: WebGPU offers no synchronous way to learn that a shader\n * compiled. The runtime is already inside an async gate when it calls this — the device itself came\n * from a promise.\n */\nexport async function createWebgpuShaderBackend(\n shared: WebgpuShared,\n deps: WebgpuShaderBackendDeps = {},\n): Promise<WebgpuShaderBackend | null> {\n const programs = await acquireShaderPrograms(shared);\n return programs ? backendOver(shared, programs, deps) : null;\n}\n\n/** The SYNC path for a runtime on a page where the device-scope state already exists: `undefined` =\n * not ready (await the factory), `null` = this device refused it, otherwise a fresh backend. Each\n * backend object owns its OWN frame state, so two runtimes ticking over one device cannot land in\n * each other's command encoder. */\nexport function peekWebgpuShaderBackend(\n shared: WebgpuShared,\n deps: WebgpuShaderBackendDeps = {},\n): WebgpuShaderBackend | null | undefined {\n const programs = peekShaderPrograms(shared);\n if (programs === undefined) return undefined;\n return programs === null ? null : backendOver(shared, programs, deps);\n}\n\nfunction backendOver(\n shared: WebgpuShared,\n programs: ShaderProgramsGpu,\n _deps: WebgpuShaderBackendDeps,\n): WebgpuShaderBackend {\n const { device } = shared;\n const executor = new WebgpuShaderExecutor(device);\n // ONE encoder per tick (see the module header). `recorded` keeps an empty tick from submitting an\n // empty command buffer; `implicit` covers a draw that arrives OUTSIDE a begin/endFrame bracket —\n // the runtime always brackets, but a frame that silently never reached the screen is not a failure\n // mode worth leaving open.\n let encoder: GPUCommandEncoder | null = null;\n let recorded = false;\n let implicit = false;\n let _submits = 0;\n\n const _ensureEncoder = (): GPUCommandEncoder => {\n if (!encoder) {\n encoder = device.createCommandEncoder({ label: \"gsw-shaders\" });\n implicit = true;\n }\n return encoder;\n };\n const flush = (): void => {\n if (encoder && recorded) {\n device.queue.submit([encoder.finish()]);\n _submits += 1;\n }\n encoder = null;\n recorded = false;\n implicit = false;\n };\n\n return {\n kind: \"webgpu\",\n\n async prepareShader(shaderKey, source) {\n const known = programs.shaders.get(shaderKey);\n if (known !== undefined) return known !== \"webgl-only\";\n const inFlight = programs.pending.get(shaderKey);\n if (inFlight) return inFlight;\n if (!source) {\n // No source and no cached decision: this binding takes WebGL, but the SHADER is not condemned\n // — the source may resolve on a later create.\n return false;\n }\n const compile = buildShaderProgram(\n shared,\n programs,\n shaderKey,\n source,\n ).finally(() => {\n programs.pending.delete(shaderKey);\n });\n programs.pending.set(shaderKey, compile);\n return compile;\n },\n\n peekShader(shaderKey) {\n const known = programs.shaders.get(shaderKey);\n return known === undefined ? undefined : known !== \"webgl-only\";\n },\n\n createSurface(spec) {\n const program = programs.shaders.get(spec.shaderKey);\n // Unreachable through the runtime (it only creates a WebGPU binding after `prepareShader`\n // answered true), and a null here is the same benign outcome as a refused context: no binding,\n // and the node keeps its CSS/SVG paint.\n if (!program || program === \"webgl-only\") return null;\n // A canvas holds ONE context type for its whole life, so this is where the choice is made — and\n // a refusal is permanent for this element, not a retryable error. `configureCanvas` has already\n // latched `context-refused`.\n const context = configureCanvas(spec.canvas, shared);\n if (!context) return null;\n // The flag `drawableSource` (the SCREEN_TEXTURE composite in `../webgl/runtime`) keys on to\n // never `drawImage` this canvas. Stamped by this backend ONLY.\n spec.canvas.setAttribute(\"data-godot-effects-backend\", \"webgpu\");\n\n const texture = resolveNodeTextureGpu(shared, spec);\n const samplers = resolveSamplersGpu(shared, spec.node, spec.program);\n const staging = createUniformStaging(\n program.transpiled.uniformStructSizeBytes,\n );\n const uniformBuffer = createWebgpuShaderUniformBuffer(\n device,\n `gsw-shader-uniforms:${spec.shaderKey}`,\n staging.bytes.byteLength,\n );\n const state: WebgpuShaderSurface = {\n context,\n program,\n staging,\n uniformBuffer,\n bindGroup: null,\n boundViews: [],\n bindGroupDirty: true,\n disposers: [],\n executor,\n };\n // THE VIEW-REPLACEMENT CONTRACT (`./textures`): a decode REPLACES the entry's texture AND view,\n // so a bind group built from the placeholder is stale from that moment. `ensureBindGroup`'s\n // view comparison catches it either way; this just means the rebuild lands on the same frame\n // the runtime's own listener re-renders.\n for (const entry of [texture, ...samplers.map((s) => s.entry)]) {\n state.disposers.push(\n onTextureLoaded(entry, () => {\n state.bindGroupDirty = true;\n }),\n );\n }\n surfaces.set(spec.canvas, state);\n // `ctx2d: null` is not an omission — it is the fact every 2D-only feature in the runtime keys\n // off (the frozen-frame cache, the surface image swap). See `ShaderSurface`.\n return { canvas: spec.canvas, ctx2d: null, texture, samplers };\n },\n\n resolveNodeTexture: (spec) => resolveNodeTextureGpu(shared, spec),\n resolveSamplers: (node, program) =>\n resolveSamplersGpu(shared, node, program),\n\n disposeSurface(binding) {\n const state = surfaces.get(binding.canvas);\n if (!state) return;\n for (const dispose of state.disposers) dispose();\n state.disposers.length = 0;\n state.uniformBuffer.destroy();\n state.bindGroup = null;\n // Hand the swap chain back. The canvas element itself belongs to the runtime, which removes it\n // — but an unconfigured context stops holding its backing images either way.\n try {\n state.context.unconfigure();\n } catch {\n // A context whose device is already lost refuses this on some implementations; there is\n // nothing left to release in that case anyway.\n }\n surfaces.delete(binding.canvas);\n },\n\n renderNode(binding, time, staticMode, getRootRect, stats, prof) {\n const w = binding.canvas.width;\n const h = binding.canvas.height;\n if (w < 1 || h < 1) return false;\n const state = surfaces.get(binding.canvas);\n if (!state) return false;\n // NO STATIC-FRAME CACHE HERE, on purpose — and that is still true. That cache trades a redraw\n // for a 2D blit onto the node canvas; on this backend the blit does not exist and there is no\n // readable 2D canvas to publish a frame INTO, so a frozen WebGPU binding renders its one frame\n // and parks, with the canvas holding its last PRESENTED image. What IS fed, since v2, is the\n // image SWAP: the frozen frame is named below with the same `staticFrameKey` the GL backend\n // uses, and the swap's capture hook re-renders those pixels offscreen rather than reading the\n // canvas (see `../surface-image-swap`'s CAPTURE-HOOK SOURCES). `staticMode` is the caller's\n // promise that `time` is the pinned one, which is what makes the key nameable at all.\n // GL SUBMIT bucket — ENCODE + UPLOAD issue cost only (the GPU runs async, and the submit itself\n // happens at `endFrame`). `blitMs` stays untouched: there is no blit on this path, and an\n // honest zero there IS the architecture.\n const start = prof ? performanceNow() : 0;\n writeUniforms(shared, state, binding, time, getRootRect, w, h);\n const bindGroup = ensureBindGroup(shared, state, binding);\n if (\n !executor.draw({\n context: state.context,\n pipeline: state.program.pipeline,\n bindGroup,\n uniformBuffer: state.uniformBuffer,\n uniformBytes: state.staging.bytes,\n width: w,\n height: h,\n })\n )\n return false;\n recorded = true;\n if (prof) prof.glMs += performanceNow() - start;\n if (stats) stats.draws++;\n // The frozen-frame signal, at the same point in the draw the GL backend emits it (and with the\n // same cacheable predicate): frozen mode, no screen-space input, every texture decoded. A null\n // key retires a live swap, because the surface is not producing frozen output at all.\n //\n // Computed whether or not a swap is live, since v3: the key is also what the CONSUMER-facing\n // `GodotEffectRenderInfo.staticKey` reports, and a host compositing these canvases wants to know\n // two of them hold the same frame even on a backend that never blits one into the other.\n const cacheable =\n staticMode &&\n !binding.program.usesScreenUv &&\n !binding.program.usesScreenTexture &&\n texturesLoaded(binding);\n binding.lastStaticKey = cacheable\n ? memoStaticFrameKey(binding, w, h, time)\n : null;\n if (binding.staticImage && stats) {\n noteStaticFrame(binding, binding.lastStaticKey, stats);\n }\n if (implicit) flush();\n return true;\n },\n\n beginFrame() {\n executor.beginFrame();\n },\n endFrame() {\n executor.endFrame();\n },\n\n // A WebGPU canvas larger than the device's limit simply cannot produce a texture — unlike the GL\n // path, whose shared drawing buffer discovers its ceiling at draw time and scales the blit up to\n // cover the node. So the ceiling is folded into the sizing law up front.\n maxBackingDim() {\n return shared.limits.maxTextureDimension2D;\n },\n\n submits() {\n return executor.submits();\n },\n\n captureSurface: (binding, time, staticMode, getRootRect) =>\n captureSurfacePixels(shared, binding, time, staticMode, getRootRect),\n };\n}\n\n// ---- shader compilation ----------------------------------------------------\n\nasync function buildShaderProgram(\n shared: WebgpuShared,\n programs: ShaderProgramsGpu,\n shaderKey: string,\n source: string,\n): Promise<boolean> {\n const { device } = shared;\n let transpiled: TranspiledWgslShader;\n try {\n transpiled = transpileGodotShaderWgsl(source);\n } catch {\n // EVERY throw is \"webgl-only\" here, `UnsupportedWgslShaderError` and the plain\n // `UnsupportedShaderError` alike. The distinction the two classes carry — \"WGSL can't\" vs \"no\n // backend can\" — is the CSS-fallback ladder's business, and by the time this runs the GL side has\n // already compiled this shader successfully, so a plain unsupported error can only mean the two\n // front-ends disagree. Either way this binding renders on WebGL, which is the answer that is\n // right in both readings. No `reportUnsupportedRender`: nothing failed to render.\n programs.shaders.set(shaderKey, \"webgl-only\");\n return false;\n }\n\n const label = `gsw-shader:${shaderKey}`;\n const module = await compileModule(device, transpiled.wgsl, label, () =>\n latchWebgpuFallbackReason(\"pipeline-error\"),\n );\n if (!module) {\n programs.shaders.set(shaderKey, \"webgl-only\");\n return false;\n }\n const bindGroupLayout = createWebgpuShaderBindGroupLayout(\n device,\n label,\n bindGroupLayoutEntries(transpiled),\n );\n const pipelineLayout = device.createPipelineLayout({\n label,\n bindGroupLayouts: [bindGroupLayout],\n });\n const pipeline = await createPipeline(\n device,\n pipelineDescriptor(\n transpiled,\n module,\n pipelineLayout,\n shared.format,\n label,\n ),\n label,\n () => latchWebgpuFallbackReason(\"pipeline-error\"),\n );\n if (!pipeline) {\n programs.shaders.set(shaderKey, \"webgl-only\");\n return false;\n }\n programs.shaders.set(shaderKey, {\n transpiled,\n module,\n bindGroupLayout,\n pipelineLayout,\n pipeline,\n capture: null,\n });\n return true;\n}\n\n/** `@group(0)`, exactly as `./transpile-wgsl` declares it: the uniform struct at 0, the node TEXTURE\n * and its sampler at 1/2, and each user sampler as a texture/sampler PAIR from 3. */\nfunction bindGroupLayoutEntries(\n transpiled: TranspiledWgslShader,\n): GPUBindGroupLayoutEntry[] {\n const { bindings } = transpiled;\n const entries: GPUBindGroupLayoutEntry[] = [\n {\n binding: bindings.uniform,\n visibility: SHADER_STAGE.VERTEX | SHADER_STAGE.FRAGMENT,\n buffer: {\n type: \"uniform\",\n minBindingSize: transpiled.uniformStructSizeBytes,\n },\n },\n {\n binding: bindings.texture,\n visibility: SHADER_STAGE.FRAGMENT,\n texture: {},\n },\n {\n binding: bindings.textureSampler,\n visibility: SHADER_STAGE.FRAGMENT,\n sampler: {},\n },\n ];\n for (let i = 0; i < transpiled.samplers.length; i++) {\n entries.push({\n binding: bindings.userSamplersBase + 2 * i,\n visibility: SHADER_STAGE.FRAGMENT,\n texture: {},\n });\n entries.push({\n binding: bindings.userSamplersBase + 2 * i + 1,\n visibility: SHADER_STAGE.FRAGMENT,\n sampler: {},\n });\n }\n return entries;\n}\n\nfunction pipelineDescriptor(\n transpiled: TranspiledWgslShader,\n module: GPUShaderModule,\n layout: GPUPipelineLayout,\n format: GPUTextureFormat,\n label: string,\n): GPURenderPipelineDescriptor {\n return {\n label,\n layout,\n // No vertex buffers: `vs_main` builds the full-screen strip from `@builtin(vertex_index)`.\n vertex: { module, entryPoint: transpiled.vertexEntry },\n fragment: {\n module,\n entryPoint: transpiled.fragmentEntry,\n // ALWAYS premultiplied — see `PREMULTIPLIED_BLEND` for why the Godot blend mode is not here.\n targets: [{ format, blend: PREMULTIPLIED_BLEND }],\n },\n primitive: { topology: \"triangle-strip\" },\n };\n}\n\n// ---- texture resolution ----------------------------------------------------\n\n/** The shader's input texture, resolved from the SAME attributes the WebGL backend reads: the atlas\n * SUB-RECT when the self-layer carries a region (cropping is what stops an atlas-sprite recolour\n * from sampling page padding), the whole image otherwise, and a 1×1 solid white with no url at all. */\nfunction resolveNodeTextureGpu(\n shared: WebgpuShared,\n spec: ShaderTextureSpec,\n): GpuTextureEntry {\n const { node, selfLayer, textureUrl } = spec;\n if (!textureUrl) {\n return getBakedTextureGpu(shared, SOLID_WHITE_KEY, () => ({\n width: 1,\n height: 1,\n data: new Uint8ClampedArray([255, 255, 255, 255]),\n }));\n }\n const repeat = nodeTextureRepeats(node);\n const region = parseAtlasRegion(\n selfLayer.getAttribute(\"data-godot-atlas-region\"),\n );\n return region\n ? getRegionTextureGpu(shared, textureUrl, region, { repeat })\n : getImageTextureGpu(shared, textureUrl, { repeat });\n}\n\n/** The user sampler uniforms, from the same `data-godot-shader-sampler*` attributes and under the\n * same bake keys as the WebGL path — so a page that falls back mid-run re-derives the same textures\n * rather than a second interpretation of the same specs. `unit` is the user-sampler INDEX here (see\n * `SamplerBinding.unit`); a sampler with no resolvable source is omitted, and `ensureBindGroup`\n * fills its slot with the node TEXTURE. */\nfunction resolveSamplersGpu(\n shared: WebgpuShared,\n node: HTMLElement,\n program: CompiledProgram,\n): SamplerBinding[] {\n if (program.samplers.length === 0) return [];\n const specs = parseSamplerSpecs(\n node.getAttribute(\"data-godot-shader-samplers\"),\n );\n const urls = parseSamplerUrls(\n node.getAttribute(\"data-godot-shader-sampler-urls\"),\n );\n const out: SamplerBinding[] = [];\n program.samplers.forEach((sampler, index) => {\n const url = urls[sampler.name];\n if (url) {\n out.push({\n name: sampler.name,\n entry: getImageTextureGpu(shared, url, { repeat: sampler.repeat }),\n unit: index,\n frameIdentity: [\"url\", url, sampler.repeat],\n });\n return;\n }\n const spec = specs[sampler.name];\n if (!spec) return;\n const specIdentity = JSON.stringify(spec) ?? \"undefined\";\n const key = `${sampler.name}:${specIdentity}`;\n out.push({\n name: sampler.name,\n entry: getBakedTextureGpu(shared, key, () => bakeTexture(spec), {\n repeat: sampler.repeat,\n }),\n unit: index,\n frameIdentity: [\"bake\", specIdentity, sampler.repeat],\n });\n });\n return out;\n}\n\n// This backend only ever renders bindings IT created (the binding remembers the backend that made its\n// surface, and a backend swap re-creates the binding on a NEW canvas), so every handle it is handed\n// really is a `GpuTextureEntry`.\nfunction gpuEntry(handle: ShaderTextureHandle): GpuTextureEntry {\n return handle as GpuTextureEntry;\n}\n\n// ---- the draw --------------------------------------------------------------\n\n/** Pack this binding's uniform struct and upload it. The VALUES are the same ones `renderNodeGl`\n * writes through `gl.uniform*` — TIME, TEXTURE_PIXEL_SIZE, MODULATE, the uv fit/window and the\n * SCREEN_UV rect — computed by the SAME functions (`uvFit`, `screenRect`), so the two renderers\n * cannot drift on arithmetic. Only the destination differs: named locations there, byte offsets\n * here (`./pack-uniforms`). */\nfunction writeUniforms(\n shared: WebgpuShared,\n state: WebgpuShaderSurface,\n binding: NodeBinding,\n time: number,\n getRootRect: () => ViewportRect,\n w: number,\n h: number,\n): void {\n const { transpiled } = state.program;\n const texture = binding.texture;\n const values: ShaderUniformValues = {\n uvFit: uvFit(binding, w, h),\n uvWindow: binding.window,\n modulate: binding.modulate,\n params: binding.params,\n paramKinds: binding.paramKinds,\n };\n if (transpiled.usesTime) values.time = time;\n if (transpiled.usesTexturePixelSize) {\n values.texturePixelSize = [1 / texture.width, 1 / texture.height];\n }\n if (transpiled.usesScreenUv) {\n const [ox, oy, sx, sy] = screenRect(binding, getRootRect());\n values.screenOrigin = [ox, oy];\n values.screenSize = [sx, sy];\n }\n packShaderUniforms(transpiled, values, state.staging);\n shared.device.queue.writeBuffer(\n state.uniformBuffer,\n 0,\n state.staging.bytes,\n 0,\n state.staging.bytes.byteLength,\n );\n}\n\n/** The binding's `@group(0)` bind group, rebuilt when a decode replaced one of its views and reused\n * otherwise. User samplers are matched BY NAME against the WGSL sampler list rather than by the\n * order `resolveSamplers` happened to produce, so the two front-ends' sampler ordering never has to\n * be assumed equal. A sampler with no resolvable source gets the node TEXTURE — WGSL bindings are\n * static and every slot must be filled, and the node texture is exactly what an unset `sampler2D`\n * uniform reads on the WebGL path (an unwritten sampler uniform defaults to texture unit 0). */\nfunction ensureBindGroup(\n shared: WebgpuShared,\n state: WebgpuShaderSurface,\n binding: NodeBinding,\n): GPUBindGroup {\n const { transpiled, bindGroupLayout } = state.program;\n const { bindings } = transpiled;\n const node = gpuEntry(binding.texture);\n const byName = new Map(\n binding.samplers.map((sampler) => [sampler.name, gpuEntry(sampler.entry)]),\n );\n\n const entries: GPUBindGroupEntry[] = [\n {\n binding: bindings.uniform,\n resource: { buffer: state.uniformBuffer },\n },\n { binding: bindings.texture, resource: node.view },\n { binding: bindings.textureSampler, resource: node.sampler },\n ];\n const views: unknown[] = [node.view];\n transpiled.samplers.forEach((sampler, i) => {\n const entry = byName.get(sampler.name) ?? node;\n entries.push({\n binding: bindings.userSamplersBase + 2 * i,\n resource: entry.view,\n });\n entries.push({\n binding: bindings.userSamplersBase + 2 * i + 1,\n resource: entry.sampler,\n });\n views.push(entry.view);\n });\n\n const stale =\n state.boundViews.length !== views.length ||\n views.some((view, i) => state.boundViews[i] !== view);\n if (state.bindGroup && !state.bindGroupDirty && !stale)\n return state.bindGroup;\n\n state.bindGroup = createWebgpuShaderBindGroup(\n shared.device,\n \"gsw-shader-bindings\",\n bindGroupLayout,\n entries,\n );\n state.boundViews = views;\n state.bindGroupDirty = false;\n return state.bindGroup;\n}\n\n// ---- readback --------------------------------------------------------------\n\n/**\n * Re-render this binding's CURRENT frame into an offscreen texture and read it back.\n *\n * NEVER `drawImage`/`toDataURL` FROM THE CANVAS. Both read a WebGPU canvas through its presentation\n * path, which is blank under headless SwiftShader and pathological on Android Chrome (S7 measured\n * 23 Hz against 87 for direct presentation). `copyTextureToBuffer` + `mapAsync` — what `./readback`\n * does — is the one path verified to work fully headless, which is why this hook exists at all\n * rather than the parity harness simply reading the canvas.\n *\n * The pipeline is the live one re-created against `rgba8unorm`: a pipeline's fragment target format\n * must match its attachment, and the canvas format is usually `bgra8unorm`. Everything else — module,\n * entry points, blend state, uniforms, bind group — is shared with the live draw, so what comes back\n * is the frame the canvas is showing, not a second interpretation of it.\n */\nasync function captureSurfacePixels(\n shared: WebgpuShared,\n binding: NodeBinding,\n time: number,\n staticMode: boolean,\n getRootRect: () => ViewportRect,\n): Promise<Uint8Array | null> {\n void staticMode;\n const state = surfaces.get(binding.canvas);\n if (!state) return null;\n const w = Math.max(1, Math.floor(binding.canvas.width));\n const h = Math.max(1, Math.floor(binding.canvas.height));\n if (binding.canvas.width < 1 || binding.canvas.height < 1) return null;\n\n const program = state.program;\n if (!program.capture) {\n const capture = await createPipeline(\n shared.device,\n pipelineDescriptor(\n program.transpiled,\n program.module,\n program.pipelineLayout,\n CAPTURE_FORMAT,\n \"gsw-shader-capture\",\n ),\n \"gsw-shader-capture\",\n );\n if (!capture) return null;\n program.capture = capture;\n }\n try {\n writeUniforms(shared, state, binding, time, getRootRect, w, h);\n const bindGroup = ensureBindGroup(shared, state, binding);\n return await state.executor.capture({\n pipeline: program.capture,\n bindGroup,\n uniformBuffer: state.uniformBuffer,\n uniformBytes: state.staging.bytes,\n width: w,\n height: h,\n read: (texture, width, height) =>\n readTexturePixels(shared, texture, width, height),\n });\n } catch {\n // A capture is a diagnostic, never a render: a device that refuses it reports nothing and the\n // live path is untouched.\n latchWebgpuFallbackReason(\"pipeline-error\");\n return null;\n }\n}\n\n/** TEST-ONLY: drop the device-scope programs so a suite can re-probe with a fresh stub device. */\nexport function __resetWebgpuShaderProgramsForTest(): void {\n programsMemo = undefined;\n programsSettled = undefined;\n programsDevice = null;\n}\n","// Live WebGL2 runtime that renders supported Godot `ShaderMaterial` nodes by\n// executing the ACTUAL shader (transpiled to GLSL ES 3.00), instead of the\n// CSS/SVG approximation. It runs only in the live DOM/Vue renderers (the static\n// html-string renderer never calls it, so it keeps the SVG fallback).\n//\n// Design:\n// - ONE shared WebGL2 context (an offscreen canvas) renders every node, so we\n// never hit the browser's ~16-live-context limit even with many cards. Each\n// node owns a cheap 2D `<canvas>` that the shared GL output is blitted onto.\n// The context, texture cache, and monotonic clock live in `./shared-gl`,\n// shared with the particle runtime.\n// - Programs (transpile+compile) are cached at module scope, keyed by shader id,\n// so re-renders (state changes) reuse them.\n// - A single rAF clock feeds `TIME`; only shaders that read `TIME` re-render per\n// frame, static ones render once (and on resize). Under an FPS cap the loop PARKS on a timer\n// to the next cap boundary instead of arming a rAF per display frame — see\n// `../effects-loop-pacing` (and `effectsLoopPacing: \"raf\"` to restore the spin).\n// - Everything the runtime needs is read from the mounted DOM + `options`\n// (the shader id/params/modulate are data-attributes set by `material.ts`;\n// the texture URL + fit come from the self-layer's background paint), so the\n// caller holds a persistent runtime and reconciles it after DOM updates.\n// - NO forced layout in the render loop: the viewport rects a SCREEN_UV/SCREEN_TEXTURE shader\n// needs (the scene root's, and the node self-layer's) are CACHED and refreshed in ONE batched\n// read at tick start, only when something invalidated them (see \"rect cache\" below).\n// - Occlusion: a binding under a `data-godot-effects-suspended` ancestor is SUSPENDED — skipped\n// by the loop and not counted as animated, so a covered subtree costs nothing per frame. See\n// `../effects-suspend` for the full contract.\n// - Dormancy: a node carrying `data-godot-shader-dormant` keeps its binding but parks it (hidden\n// canvas, skipped by the loop AND the rect batch, `syncCanvasSize` deferred to the wake), so a\n// node flipping in and out of a shader-off state no longer pays a create-time forced layout per\n// flip. A binding dormant for ~30s is disposed by ONE per-runtime sweep. See `../shader-dormant`.\n// - Image swap: a binding whose surface has been observed not to move is shown as an `<img>` of\n// its own frame instead of its canvas (the canvas stays, hidden), which drops its compositor\n// layer, its render surface and its per-frame GPU fill. The MECHANISM lives in\n// `../surface-image-swap` (generic, no WebGL); the POLICY — which gate, what an invalidation\n// does, encode pacing, a host veto — comes from the `staticShaderImages` option, whose default\n// (`true`) is the frozen-mode content-key gate that shipped first. `false` is the kill switch.\n\nimport {\n compileProgramAsync,\n createWebglTexture,\n uploadWebglTexture,\n} from \"@godot-scene-web/canvas-effects/webgl\";\nimport {\n expandGodotShaderIncludes,\n type GodotBlendMode,\n type ShaderSampler,\n type ShaderUniform,\n type TranspiledShader,\n transpileGodotShader,\n UnsupportedShaderError,\n} from \"@godot-scene-web/effects/shaders\";\nimport {\n reportUnsupportedRender,\n type UnsupportedRenderReporter,\n} from \"../diagnostics\";\nimport { createEffectsLoopPacer } from \"../effects-loop-pacing\";\nimport { isEffectsSuspended } from \"../effects-suspend\";\nimport { ownSelfLayer, SELF_LAYER_CLASS } from \"../render-structure\";\nimport type { GodotHtmlRuntimeOptions } from \"../runtime-options\";\nimport { DORMANT_DISPOSE_SECONDS, isShaderDormant } from \"../shader-dormant\";\nimport {\n applySurfaceVisibility,\n createStaticImageSwapCounters,\n createStaticSurfaceSwapper,\n disposeStaticImage,\n liveStaticImageUrlCount,\n noteStaticImageReconcile,\n noteStaticSurfaceWake,\n onStaticFrameEvicted,\n revertStaticImage,\n type StaticImageState,\n type StaticImageSwapCounters,\n type StaticSurfaceCapture,\n type StaticSurfaceSwapper,\n staticStillPoolStats,\n} from \"../surface-image-swap\";\nimport type { GodotEffectRenderInfo } from \"../types\";\nimport {\n acquireWebgpuDevice,\n latchWebgpuFallbackReason,\n onWebgpuDeviceLost,\n peekWebgpuDevice,\n type WebgpuFallbackReason,\n type WebgpuShared,\n webgpuFallbackReason,\n} from \"../webgpu/device\";\nimport {\n createWebgpuShaderBackend,\n peekWebgpuShaderBackend,\n type WebgpuShaderBackend,\n} from \"../webgpu/render-shader\";\nimport { canvasFromPremultipliedRgba } from \"../webgpu/still-capture\";\nimport {\n createWebglShaderBackend,\n layerRect,\n memoStaticFrameKey,\n parseAtlasRegion,\n quantizeForKey,\n type ShaderRenderBackend,\n type ShaderTextureHandle,\n type StaticSamplerIdentity,\n texturesLoaded,\n} from \"./shader-backend\";\nimport {\n backingStoreSize,\n effectivePixelRatio,\n getShared,\n MAX_PINNED_BACKING_DIM,\n nodeTextureRepeats,\n normalizeStaticPixelRatio,\n nowSeconds,\n onTextureLoaded,\n parseSurfacePixelRatio,\n type SharedGl,\n SURFACE_PIXEL_RATIO_ATTR,\n} from \"./shared-gl\";\n\n// The GL-typed helpers the render backend now owns, re-exported so their long-standing import\n// specifier (`./webgl/runtime`) keeps working for consumers and tests.\nexport { parseAtlasRegion } from \"./shader-backend\";\n\n/** A transpiled+compiled shader and everything a render needs to know about it. Exported as a TYPE\n * for the render backend (`./shader-backend`); the program cache itself stays here. */\nexport interface CompiledProgram {\n program: WebGLProgram;\n uniformLocations: Map<string, WebGLUniformLocation | null>;\n uniforms: ShaderUniform[];\n samplers: ShaderSampler[];\n usesTime: boolean;\n usesTexturePixelSize: boolean;\n usesScreenUv: boolean;\n /** The shader samples SCREEN_TEXTURE → the runtime maintains a per-binding screen capture. */\n usesScreenTexture: boolean;\n usesScreenPixelSize: boolean;\n /** render_mode blend (mix/add/sub/mul/premul_alpha) → applied as a CSS mix-blend-mode on the node. */\n blend: GodotBlendMode;\n}\n\n// Programs are cached at module scope, keyed by shader id, so re-renders reuse them.\nconst programCache = new Map<string, CompiledProgram | \"unsupported\">();\n\n// In-flight compiles, keyed exactly like `programCache` and holding only the promise (every SETTLED\n// verdict, \"unsupported\" included, still lands in the cache). Building a program is asynchronous now\n// — the status query is deferred until the driver says it will not block (see `compileProgramAsync`)\n// — so the window in which a shader is \"being compiled\" is real, and N nodes that want the same\n// shader inside it must share ONE compile rather than each kicking the driver with its own copy.\n// Same shape, and the same settle-cleanup, as `shaderSourceRequests` below.\nconst programRequests = new Map<string, Promise<CompiledProgram | null>>();\n\n// The program-cache key. Screen-capture-enabled runtimes compile under their own\n// namespace, so an option-off runtime's \"unsupported\" verdict for a SCREEN_TEXTURE\n// shader (the gate below) never poisons an option-on runtime sharing the module\n// cache — and vice versa. For the default (option off) everything is keyed exactly\n// as before.\nfunction programCacheKey(shaderKey: string, screenCapture: boolean): string {\n return screenCapture ? `screen-capture:${shaderKey}` : shaderKey;\n}\n\n// Static-frame cache (frozen-TIME mode ONLY): a node renders its shader once, so identical inputs across nodes\n// (e.g. 12 playable cards sharing the same glow, or the same node re-created across remounts) can reuse one\n// rendered bitmap and SKIP the GL draw entirely — one render + N cheap 2D blits. Keyed by the shader + canvas\n// size + fit + texture identity + the EXACT uv-window + QUANTIZED params/modulate (so the producer's per-delta\n// param jitter doesn't bust it). The window is deliberately NOT quantized: it is geometry — which sub-rect of\n// the node the frame's pixels COVER — so serving a frame across any window delta paints the wrong pixels (the\n// UNDERDOCKS widened-background band bug: a frame cached under the old clip window was blitted for the widened\n// one whenever the quantized keys collided). NEVER consulted for live TIME shaders (their output changes every\n// frame). Module-scoped + LRU-bounded, like programCache/textureCache; survives remounts, never cleared on\n// dispose.\nconst STATIC_FRAME_CACHE_LIMIT = 64;\nconst staticFrameCache = new Map<string, HTMLCanvasElement>();\n\n// `quantizeForKey`/`staticFrameKey` live in `./shader-backend` (both backends name a frozen frame\n// with the same string — the WebGPU one has no frame CACHE but does feed the image swap, and the two\n// must agree on what \"the same frame\" means). They are imported back here because this file owns the\n// cache the key addresses and the precomputed key halves below.\n\n// The attr-derived halves of `staticFrameKey`, PRECOMPUTED whenever the attribute they come from\n// is (re-)parsed instead of re-serialized on every render. Two `JSON.stringify` passes over the\n// param maps per node per frame was pure waste: the parsed values only ever change when their\n// data-attr string does, which `updateBinding` already detects with a cheap string compare.\nfunction paramsFrameKey(params: Record<string, ShaderParamValue>): string {\n return JSON.stringify(params, (_k, v) =>\n typeof v === \"number\" ? quantizeForKey(v) : v,\n );\n}\n\nfunction paramKindsFrameKey(paramKinds: Record<string, string>): string {\n return JSON.stringify(paramKinds);\n}\n\nfunction modulateFrameKey(modulate: readonly number[]): string {\n return modulate.map(quantizeForKey).join(\",\");\n}\n\n// EXACT, not quantized (unlike params/modulate): the window decides WHICH pixels of the node the\n// frame contains, so two windows that differ at all must never share a cached frame — a canvas can\n// keep the same w/h across a real window change (a box growth compensating a du shrink, or a pure\n// u0/v0 pan), and pre-quantization those served a stale-clip blit.\nfunction windowFrameKey(window: readonly number[]): string {\n return window.join(\",\");\n}\n\n// Snapshot a just-rendered node canvas into the LRU cache (a fresh 2D canvas so it isn't overwritten by the\n// node's next render). Evicts the oldest entries past the cap.\nfunction storeStaticFrame(\n key: string,\n source: HTMLCanvasElement,\n w: number,\n h: number,\n): void {\n if (typeof document === \"undefined\") return;\n const canvas = document.createElement(\"canvas\");\n canvas.width = w;\n canvas.height = h;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n ctx.drawImage(source, 0, 0);\n staticFrameCache.set(key, canvas);\n while (staticFrameCache.size > STATIC_FRAME_CACHE_LIMIT) {\n const oldest = staticFrameCache.keys().next().value;\n if (oldest === undefined) break;\n staticFrameCache.delete(oldest);\n // The image swap keys its object URLs by the SAME key, so an eviction here retires that entry\n // too (see `../surface-image-swap`) — a leaked blob URL outlives the document otherwise.\n onStaticFrameEvicted(oldest);\n }\n}\n\n// A cached frozen frame for `key`, LRU-bumped so the eviction above always drops the coldest entry.\n// The backend's render path asks through here (see `WebglShaderBackendDeps`) instead of reaching\n// into the Map: the bump is cache bookkeeping, not rendering, and the cache keeps its one home.\nfunction lookupStaticFrame(key: string): HTMLCanvasElement | undefined {\n const hit = staticFrameCache.get(key);\n if (!hit) return undefined;\n staticFrameCache.delete(key);\n staticFrameCache.set(key, hit); // LRU bump\n return hit;\n}\n\n/** TEST-ONLY: clear the static-frame cache so a test starts from an empty cache. */\nexport function __resetStaticShaderFrameCacheForTest(): void {\n staticFrameCache.clear();\n}\n\n// IN-FLIGHT shader-source resolves, keyed by shader id (the `BAKED_CACHE` shape in `../tint-bake`).\n// WHY: `programCache` only dedupes AFTER a source has been fetched, expanded and transpiled, so a\n// cold cache with N nodes sharing one shader (a hand of cards) fired N identical fetches and N\n// identical `expandGodotShaderIncludes` passes concurrently — the create-burst cost we're paying on\n// phones. They now share ONE promise.\n//\n// The entry is dropped once it settles, so this is purely an in-flight window (a later create hits\n// `programCache` instead) — which also means a FAILED fetch is never cached as a verdict and the\n// next binding retries.\nconst shaderSourceRequests = new Map<string, Promise<string | undefined>>();\n\nfunction resolveExpandedShaderSource(\n shaderKey: string,\n path: string | undefined,\n uid: string | undefined,\n resolveShaderSource: NonNullable<\n GodotHtmlRuntimeOptions[\"resolveShaderSource\"]\n >,\n): Promise<string | undefined> {\n const inFlight = shaderSourceRequests.get(shaderKey);\n if (inFlight) return inFlight;\n const request = (async () => {\n const source = await resolveShaderSource(path, uid);\n if (source === undefined) return undefined;\n return await expandGodotShaderIncludes(source, (includePath) =>\n resolveShaderSource(includePath, undefined),\n );\n })();\n // Settle-cleanup on a DERIVED promise, so a rejection is handled here (no unhandled-rejection\n // warning) while still propagating to every awaiting caller through `request` itself.\n const forget = (): void => {\n if (shaderSourceRequests.get(shaderKey) === request) {\n shaderSourceRequests.delete(shaderKey);\n }\n };\n request.then(forget, forget);\n shaderSourceRequests.set(shaderKey, request);\n return request;\n}\n\n/** TEST-ONLY: drop any in-flight shader-source requests so a test starts from a cold cache. */\nexport function __resetShaderSourceRequestsForTest(): void {\n shaderSourceRequests.clear();\n}\n\n// Transpile + compile a shader once, caching by id. Returns null when the shader is unsupported or\n// fails to compile (caller leaves the node on CSS/SVG). ASYNC because the compile is: the driver is\n// given time to finish before anything asks it a blocking question (see `compileProgramAsync`), so a\n// cold shader now settles a few milliseconds later WITHOUT the main thread standing still for it.\n// A cached program still resolves on the first microtask, and the synchronous `programCache` peek\n// callers use to decide whether they need a source at all is untouched.\nfunction getProgramAsync(\n gl: WebGL2RenderingContext,\n shaderKey: string,\n source: string,\n onUnsupported?: UnsupportedRenderReporter,\n screenCapture = false,\n): Promise<CompiledProgram | null> {\n const cacheKey = programCacheKey(shaderKey, screenCapture);\n const cached = programCache.get(cacheKey);\n if (cached === \"unsupported\") return Promise.resolve(null);\n if (cached) return Promise.resolve(cached);\n const inFlight = programRequests.get(cacheKey);\n if (inFlight) return inFlight;\n const request = buildProgram(\n gl,\n cacheKey,\n shaderKey,\n source,\n onUnsupported,\n screenCapture,\n );\n // Settle-cleanup on a DERIVED promise, so a rejection is handled here while still propagating to\n // every awaiting caller through `request` itself (`resolveExpandedShaderSource`'s pattern).\n const forget = (): void => {\n if (programRequests.get(cacheKey) === request) {\n programRequests.delete(cacheKey);\n }\n };\n request.then(forget, forget);\n programRequests.set(cacheKey, request);\n return request;\n}\n\nasync function buildProgram(\n gl: WebGL2RenderingContext,\n cacheKey: string,\n shaderKey: string,\n source: string,\n onUnsupported: UnsupportedRenderReporter | undefined,\n screenCapture: boolean,\n): Promise<CompiledProgram | null> {\n let program: WebGLProgram | null = null;\n let transpiled: TranspiledShader;\n try {\n transpiled = transpileGodotShader(source);\n } catch (error) {\n // Fail loud (deduped): the node keeps its CSS/SVG paint, but which shader gsw couldn't run — an\n // unsupported GLSL construct vs an unexpected transpile bug — is now surfaced, not silently dropped.\n reportUnsupportedRender(\n {\n kind: \"shader\",\n id: shaderKey,\n reason:\n error instanceof UnsupportedShaderError\n ? \"unsupported shader construct\"\n : \"shader transpile error\",\n error,\n },\n onUnsupported,\n );\n programCache.set(cacheKey, \"unsupported\");\n return null;\n }\n // Screen reads transpile, but the capture machinery is OPT-IN (`enableScreenTextureCapture`).\n // Without it, take the exact unsupported/CSS-fallback path such shaders took before support\n // existed, so every option-off consumer is unaffected.\n if (\n (transpiled.usesScreenTexture || transpiled.usesScreenPixelSize) &&\n !screenCapture\n ) {\n reportUnsupportedRender(\n {\n kind: \"shader\",\n id: shaderKey,\n reason: \"screen-texture capture not enabled\",\n },\n onUnsupported,\n );\n programCache.set(cacheKey, \"unsupported\");\n return null;\n }\n // The await is the point: the compile+link are kicked here and the main thread is released while\n // the driver works. Everything below — the status query inside `compileProgramAsync`, and the\n // `getUniformLocation` walk, which blocks on an incomplete link just as hard — happens only once\n // the driver reports it can answer without stalling.\n program = await compileProgramAsync(\n gl,\n transpiled.vertexGlsl,\n transpiled.fragmentGlsl,\n (p) => gl.bindAttribLocation(p, 0, \"a_pos\"),\n );\n if (!program) {\n reportUnsupportedRender(\n { kind: \"shader\", id: shaderKey, reason: \"shader failed to compile\" },\n onUnsupported,\n );\n programCache.set(cacheKey, \"unsupported\");\n return null;\n }\n const uniformLocations = new Map<string, WebGLUniformLocation | null>();\n const names = [\n \"TIME\",\n \"TEXTURE\",\n \"TEXTURE_PIXEL_SIZE\",\n \"SCREEN_TEXTURE\",\n \"SCREEN_PIXEL_SIZE\",\n \"MODULATE\",\n \"_godot_uv_fit\",\n \"_godot_uv_window\",\n \"_godot_screen_origin\",\n \"_godot_screen_size\",\n ...transpiled.uniforms.map((u) =>\n u.arrayLength ? `${u.name}[0]` : u.name,\n ),\n ...transpiled.samplers.map((s) => s.name),\n ];\n for (const name of names) {\n const key = name.endsWith(\"[0]\") ? name.slice(0, -3) : name;\n uniformLocations.set(key, gl.getUniformLocation(program, name));\n }\n const compiled: CompiledProgram = {\n program,\n uniformLocations,\n uniforms: transpiled.uniforms,\n samplers: transpiled.samplers,\n usesTime: transpiled.usesTime,\n usesTexturePixelSize: transpiled.usesTexturePixelSize,\n usesScreenUv: transpiled.usesScreenUv,\n usesScreenTexture: transpiled.usesScreenTexture,\n usesScreenPixelSize: transpiled.usesScreenPixelSize,\n blend: transpiled.blend,\n };\n programCache.set(cacheKey, compiled);\n return compiled;\n}\n\n/** The subset of DOMRect the screen-space paths read. Lets a CACHED rect (a plain snapshot) stand\n * in for a live `getBoundingClientRect()` everywhere. */\nexport interface ViewportRect {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\n/** A resolved sampler uniform: its baked texture + the slot it binds to. */\nexport interface SamplerBinding {\n name: string;\n entry: ShaderTextureHandle;\n /** WHERE the backend binds it, in that backend's own numbering: a GL texture UNIT (1..N, unit 0\n * being the node TEXTURE) on WebGL, and the user-sampler INDEX (0..N-1, which the WGSL binding\n * table maps to `@binding(3 + 2i)`) on WebGPU. Only the backend that produced this reads it. */\n unit: number;\n /** The source this sampler actually binds. This is deliberately independent of the backend's\n * texture-cache key: it names the shader input for the renderer-agnostic frozen-frame key. */\n frameIdentity: StaticSamplerIdentity;\n}\n\n// Minimum seconds between screen-capture refreshes for one binding. The capture is a\n// throttled snapshot — a TIME shader animates every frame over a ~3Hz-refreshed\n// background — NEVER a per-rAF DOM walk.\nconst SCREEN_CAPTURE_MIN_INTERVAL_S = 0.3;\n// Default longest-edge cap for the capture canvas (see `maxScreenCaptureDim`).\nconst DEFAULT_MAX_SCREEN_CAPTURE_DIM = 1024;\n\n/** Per-binding SCREEN_TEXTURE capture: the offscreen 2D composite + its own GL texture.\n * Binding-OWNED (created/deleted with the binding) — NOT a url-keyed textureCache entry,\n * since the content is volatile and unique to the node's position in draw order. */\nexport interface ScreenCaptureState {\n canvas: HTMLCanvasElement;\n ctx: CanvasRenderingContext2D;\n texture: WebGLTexture;\n /** `nowSeconds()` of the last composite+upload (throttle timestamp). */\n capturedAt: number;\n width: number;\n height: number;\n}\n\nexport interface NodeBinding {\n /** The keyed shader node element (the reconcile Map key); its DOM identity is stable\n * across re-renders for an unchanged node (Stage B), so its binding is reused. */\n node: HTMLElement;\n /** `uid ?? path` — a change means the shader itself swapped → recreate, not update. */\n shaderKey: string;\n selfLayer: HTMLElement;\n /** Scene root, for the SCREEN_UV viewport rect (node-rect ÷ root-rect). */\n root: HTMLElement;\n /** The renderer this binding draws through (see `./shader-backend`). PER-BINDING, not\n * per-runtime: a shader that cannot run on the runtime's preferred backend must fall back on its\n * own rather than veto its neighbours. Today every binding gets the WebGL backend. */\n backend: ShaderRenderBackend;\n canvas: HTMLCanvasElement;\n /** The blit target — null on a backend that renders STRAIGHT into the node canvas (WebGPU), which\n * is also what every 2D-only feature keys off. Always present on a WebGL binding. */\n ctx2d: CanvasRenderingContext2D | null;\n /** ASYNC ENCODE SOURCE for the frozen-surface image swap, set ONLY on a binding whose canvas\n * cannot be read back (a WebGPU one). Produces a fresh 2D canvas of this binding's current frame\n * through the backend's capture hook; the swap module owns and releases it. Absent on a WebGL\n * binding, whose canvas the swap reads directly. See `../surface-image-swap`'s\n * `StaticImageSwapBinding.captureCanvas`. */\n captureCanvas?: () => Promise<StaticSurfaceCapture>;\n /** The GL-compiled shader. Present on EVERY binding, WebGPU ones included: it is the source of the\n * renderer-agnostic facts the runtime reads (`usesTime`, `usesScreenUv`, `blend`, the uniform and\n * sampler NAME lists), and it is what a device loss falls back onto without a second compile. The\n * WGSL twin of a WebGPU binding lives in `../webgpu/render-shader`, keyed by the same shader id. */\n program: CompiledProgram;\n texture: ShaderTextureHandle;\n /** Canonical node TEXTURE inputs, kept alongside the resolved handle so the shared frozen-frame\n * key does not have to read DOM attributes (and remains pure). */\n textureRepeat: boolean;\n textureRegion: { x: number; y: number; width: number; height: number } | null;\n /** Procedural sampler-uniform textures (see `SamplerBinding.unit` for the numbering). */\n samplers: SamplerBinding[];\n /** background-size mode for UV fit: \"contain\" | \"cover\" | \"fill\". */\n fit: \"contain\" | \"cover\" | \"fill\";\n /** UV/canvas window [u0,v0,du,dv] in node-local top-left fractions: the runtime renders only this SUB-RECT of\n * the node into a smaller, repositioned canvas (clamping an off-screen-overflowing background to the visible\n * region), and the shader samples the matching portion via `_godot_uv_window`. [0,0,1,1] = full node. */\n window: [number, number, number, number];\n windowAttr: string | null;\n /** Per-binding backing-density MULTIPLIER (see `SURFACE_PIXEL_RATIO_ATTR`): how much bigger this\n * surface is on screen than its own CSS box, as the host states it. Multiplied into whichever\n * density term applies (the live `devicePixelRatio × renderScale`, or the frozen pin) by\n * `syncCanvasSize`. Always a finite positive number — `parseSurfacePixelRatio` resolves every\n * malformed/absent attribute to exactly `1`, which is the un-stamped, byte-identical case. */\n pixelRatioScale: number;\n /** …and the raw attribute string it came from, so a sweep tells \"unchanged\" from \"moved\" with one\n * string compare and no parse (the `windowAttr` idiom, for the same reason). */\n pixelRatioAttr: string | null;\n /** Last-measured self-layer content-box size in CSS px. Seeded by the one create-time layout read and kept\n * current by the ResizeObserver's contentRect — so a later resize driven by a WINDOW change (or a renderScale\n * step), where the box itself is unchanged, reuses this instead of forcing another clientWidth/Height reflow.\n * Meaningless until `boxMeasured`. */\n boxW: number;\n boxH: number;\n /** Has this binding's box EVER been resolved (by a layout read or a delivered `contentRect`)? The reuse gate,\n * and it is a flag rather than `boxW > 0` because a self-layer can legitimately measure 0x0 — a hidden\n * ancestor, a Node2D with no rect — and a size test would then never latch, sending EVERY later\n * `syncCanvasSize` back through the forced `clientWidth`/`clientHeight` layout for as long as the node\n * lives. (`../particles/runtime`'s `boxMeasured` is the same flag, for the same reason.) A latched 0x0 is\n * refreshed for free the moment it stops being 0x0: only a resize can do that, and a resize is what the\n * shared ResizeObserver reports. */\n boxMeasured: boolean;\n params: Record<string, ShaderParamValue>;\n paramKinds: Record<string, string>;\n modulate: [number, number, number, number];\n /** Precomputed `staticFrameKey` parts, rebuilt only when the attribute they derive from is\n * re-parsed (see the `…FrameKey` helpers) instead of re-serialized per render. */\n paramsKey: string;\n paramKindsKey: string;\n modulateKey: string;\n windowKey: string;\n textureLoadDisposers: Array<() => void>;\n /** Binding-local cache for the otherwise pure staticFrameKey calculation. Every effective\n * writer of a keyed input advances this epoch through invalidateStaticFrameKey. */\n staticKeyEpoch: number;\n staticKeyMemo: {\n epoch: number;\n w: number;\n h: number;\n /** quantizeForKey(TIME), matching staticFrameKey rather than the caller's raw clock value. */\n time: number;\n key: string;\n } | null;\n loadingFallback: boolean;\n loadingFallbackCleared: boolean;\n /** Whether a static (non-TIME) node still needs its one-shot render. */\n dirty: boolean;\n /** Occlusion suspend (see `../effects-suspend`): the node sits under a\n * `data-godot-effects-suspended` ancestor → the loop skips it entirely and it does NOT keep the\n * rAF loop alive. Recomputed on every `reconcile()`, never polled per frame. */\n suspended: boolean;\n /** Dormant (see `../shader-dormant`): the node carries `data-godot-shader-dormant` → the binding\n * is KEPT but parked — canvas hidden, skipped by the loop AND the batched rect read, and\n * `syncCanvasSize` deferred to the wake. Recomputed on every `reconcile()`. */\n dormant: boolean;\n /** A `syncCanvasSize` that was skipped while dormant; run once on wake. */\n canvasSyncDeferred: boolean;\n /** Ordinal of the moment this binding went dormant, from the runtime's monotonic counter (0 while\n * awake). The dormant-expiry sweep compares ordinals rather than a wall clock, so it needs no\n * per-binding timer and no clock reading. */\n dormantSeq: number;\n /** Cached self-layer viewport rect for the SCREEN_UV / SCREEN_TEXTURE paths, refreshed by the\n * runtime's batched rect read (see the rect cache below). null until first measured. Reading it\n * per frame was a forced layout PER SCREEN_UV NODE PER FRAME. */\n layerRect: ViewportRect | null;\n // Raw data-attr signatures captured at create/update, so `reconcile` can detect a real\n // change with cheap attribute reads (NO layout) and skip work for unchanged nodes.\n paramsAttr: string | null;\n paramKindsAttr: string | null;\n modulateAttr: string | null;\n samplersAttr: string | null;\n samplerUrlsAttr: string | null;\n /** Resolved texture url (self-layer attr or backgroundImage). */\n textureUrl: string | null;\n /** Memo of the last `selfLayer.style.backgroundImage` string and the url parsed out of it, so an\n * unchanged paint skips the regex on every reconcile. `null` = nothing memoized yet (the style\n * property is always a string, so it can never collide with a real value). */\n backgroundImageStyle: string | null;\n backgroundImageParsed: string | null;\n /** Raw `data-godot-atlas-region` string (atlas sub-rect crop) so a change re-uploads. */\n textureRegionAttr: string | null;\n textureRepeatAttr: string | null;\n /** SCREEN_TEXTURE capture state; created lazily on the first render of a\n * `usesScreenTexture` program, null for every other shader. */\n screenCapture: ScreenCaptureState | null;\n /** Frozen-surface image-swap state (see `../surface-image-swap`), or null when the runtime's\n * `staticShaderImages` option is off — in which case every swap call site is a no-op and the\n * binding takes exactly the path it took before the swap existed. */\n staticImage: StaticImageState | null;\n /** The static-frame key of the frame this canvas HOLDS, or null when the frame is not a frozen,\n * content-addressed one (live mode, a screen-space shader, textures still loading). Written by\n * the backend on every paint and reported to the consumer through `GodotEffectRenderInfo.staticKey`\n * — see there for why a host that composites these canvases wants it. It is the same string the\n * static-frame cache and the image swap name that bitmap by; nothing here derives a second one. */\n lastStaticKey: string | null;\n}\n\n/** Invalidate the binding-local memo around staticFrameKey. Keep this separate from\n * `lastStaticKey`: that field describes the pixels the canvas currently holds, while this only\n * says that the inputs to a future key calculation changed. */\nexport function invalidateStaticFrameKey(binding: NodeBinding): void {\n binding.staticKeyEpoch += 1;\n binding.staticKeyMemo = null;\n}\n\n/** OPT-IN per-frame cost attribution for ONE shader runtime (`effectsProfiling`, see `../types`),\n * read from `stats().profile`; NULL when the option is off. The particle runtime's `ParticleProfile`\n * with the two CPU buckets removed — a shader frame has no simulation and no instance buffer, so\n * everything a shader tick does on the main thread is submit + blit. */\nexport interface ShaderProfile {\n /** Loop ticks that rendered at least one binding. A tick deferred by the FPS cap (`capDeferrals`)\n * or one where every binding was clean/parked/suspended books none. */\n ticks: number;\n /** Bindings the tick handed to `renderNode`, summed across those ticks — static-frame cache hits\n * included, whether they blit or simply confirm their canvas already presents the keyed frame.\n * Clean, parked and suspended bindings are skipped by the loop before this and never counted. */\n bindings: number;\n /** GL SUBMIT: `renderNode`'s viewport/clear/program preamble, its texture binds and uniform\n * writes, and the `drawArrays`. SUBMIT ONLY — the GPU runs asynchronously, so this never contains\n * GPU execution time. Deliberately EXCLUDES the throttled SCREEN_TEXTURE capture (a 2D composite\n * of the DOM plus an upload, on its own ~300 ms cadence), which would otherwise spike this bucket\n * on the frames that happen to refresh it. */\n glMs: number;\n /** GL→2D BLIT: the node canvas's `clearRect` + the `drawImage` copying the shared GL canvas onto\n * it — and a cross-canvas static-frame cache-hit blit, which is the cost that cache trades the GL draw for.\n * Fill cost, so it scales with backing-store area (`renderScale` × devicePixelRatio), not with\n * shader complexity. */\n blitMs: number;\n}\n\n/** A zeroed `ShaderProfile`. Allocated ONCE per runtime and only when `effectsProfiling` is on; the\n * render path mutates it in place, so measuring adds no per-frame allocation. */\nfunction createShaderProfile(): ShaderProfile {\n return { ticks: 0, bindings: 0, glMs: 0, blitMs: 0 };\n}\n\n/** Live, monotonically-increasing counters for ONE shader runtime (see `WebglShaderRuntime.stats`).\n * Plain fields bumped with `++` on the hot paths — no allocation, no behavior change; a probe\n * snapshots the object and diffs across a gesture window. Never reset for the runtime's lifetime. */\nexport interface WebglShaderRuntimeStats extends StaticImageSwapCounters {\n /** Actual GL draws of a shader frame (`renderNode` reaching `gl.drawArrays`). */\n draws: number;\n /** Frozen-mode static-frame cache hits: the GL draw was skipped and a cached frame was handled. */\n cacheHits: number;\n /** Cache hits whose target canvas already held the named frame, so the 2D clear/blit was skipped.\n * `onBindingRendered` still fires: the binding presented/handled its current frame even though no\n * pixels changed. */\n blitSkips: number;\n /** SEAM — 0 today: static re-renders skipped because the quantized frame key was unchanged.\n * Future consumer: the `staticDirtyByFrameKey` dirty gating (Fix B) in `updateBinding`. */\n dirtySkips: number;\n /** Ticks pushed past the fps-cap boundary (the `!pacer.isDue` re-arm in `tick`). Today only capped\n * ANIMATED loops take this path — frozen mode bypasses the cap entirely (`capRemaining` returns 0),\n * so this stays 0 in static mode until `capStaticRerenders` (Fix A) routes static dirty re-renders\n * through the cap too. */\n capDeferrals: number;\n /** Node-canvas backing-store reallocations: `syncCanvasSize` calls that actually re-assigned\n * `canvas.width`/`height` (one event per call, however many dimensions moved). */\n canvasReallocs: number;\n /** Synchronous out-of-loop renders (`renderBindingNow`): the uv-window/realloc anti-flicker path\n * that draws immediately instead of waiting for the next tick. */\n syncRenders: number;\n /** `syncCanvasSize` calls that sized a binding at the PINNED static ratio\n * (`staticShaderPixelRatio`) instead of `devicePixelRatio × renderScale`. A device probe reads\n * this to confirm the pin is actually in force — it stays 0 when the option is unset OR the\n * runtime never entered frozen mode, which `canvasReallocs` alone cannot distinguish (that\n * counter is about the OPPOSITE thing: how much re-sizing churn is still happening). */\n pinnedCanvasSyncs: number;\n /** Per-frame cost attribution (see `ShaderProfile`), or NULL when `effectsProfiling` is off — the\n * default, and the no-op handle always. NULL rather than a zeroed object ON PURPOSE: \"not\n * measured\" must never read as \"measured, cost nothing\". The same object every `stats()` call. */\n profile: ShaderProfile | null;\n /** GAUGE — which renderer NEW bindings of this runtime are created on RIGHT NOW, re-derived on\n * each `stats()` read (a runtime can change backend mid-life, in one direction: a WebGPU device\n * that is lost is rebuilt on WebGL).\n *\n * `\"pending\"` is a real state: with `effectsRenderer: \"auto\"`/`\"webgpu\"` on a browser that HAS\n * `navigator.gpu`, the device arrives from a promise, and until it does no binding is created at\n * all — every opted-in node simply keeps its CSS/SVG paint, exactly as it does while its shader\n * source is still being fetched. `\"none\"` is the no-op handle (no WebGL2, or no shader resolver).\n *\n * It is the RUNTIME's answer, not a binding's: individual bindings can be on WebGL under a\n * `\"webgpu\"` runtime (see `webgpuBindingFallbacks`). */\n renderer: \"pending\" | \"webgpu\" | \"webgl\" | \"none\";\n /** COUNTER. Times this runtime adopted WebGL after being asked for `\"auto\"`/`\"webgpu\"` — the\n * SYNCHRONOUS \"this browser has no navigator.gpu\" decline included, which is the common case and\n * the reason a plain WebGL page reports 1 here rather than 0. Stays 0 for `effectsRenderer:\n * \"webgl\"` (nothing was ever asked for) and for a runtime that adopted WebGPU and kept it. */\n webgpuFallbacks: number;\n /** COUNTER — bindings that took the WebGL backend under a runtime whose renderer is WebGPU,\n * because THEIR shader cannot run there: it samples SCREEN_TEXTURE, its WGSL transpile refused a\n * construct, or its module/pipeline failed to build. This is the counter that says the\n * per-binding fallback is doing its job — one screen-reading shader must not veto N WebGPU\n * canvases, and without this the only symptom would be a node that is quietly slower. */\n webgpuBindingFallbacks: number;\n /** The FIRST reason this runtime declined WebGPU (later ones cannot un-explain it), or null while\n * it never has. THE diagnostic for a silent fallback: `renderer: \"webgl\"` under\n * `effectsRenderer: \"webgpu\"` says something went wrong, and only this says what. */\n webgpuFallbackReason: WebgpuFallbackReason | null;\n /** COUNTER — `queue.submit` calls this runtime's WebGPU backend has made, sampled on read. ONE per\n * tick that drew anything, whatever the binding count: that batching is the measured win (S7), so\n * a probe that finds it climbing with N nodes has found the win being given back. 0 on WebGL,\n * where the question is meaningless. */\n webgpuSubmits: number;\n /** GAUGE — `device.lost` resolutions seen by the page-wide device (see `../webgpu/device`). A lost\n * device stops producing frames, so a non-zero value here next to `renderer: \"webgl\"` is the\n * device-loss rebuild having happened. */\n webgpuDeviceLosses: number;\n /** GAUGE — `uncapturederror` events on the page-wide device. Non-zero means a frame was silently\n * WRONG: WebGPU reports most command-level mistakes this way and nothing else says so. */\n webgpuErrors: number;\n}\n\nfunction createWebglShaderRuntimeStats(): WebglShaderRuntimeStats {\n return {\n draws: 0,\n cacheHits: 0,\n blitSkips: 0,\n dirtySkips: 0,\n capDeferrals: 0,\n canvasReallocs: 0,\n syncRenders: 0,\n pinnedCanvasSyncs: 0,\n // OFF unless the runtime turns it on at create: the never-measured state, and the only one a\n // no-op handle can ever report.\n profile: null,\n // \"none\" is the no-op handle's permanent answer; a real runtime overwrites this on its first\n // `stats()` read (and the gate has usually settled it before anyone can look).\n renderer: \"none\",\n webgpuFallbacks: 0,\n webgpuBindingFallbacks: 0,\n webgpuFallbackReason: null,\n webgpuSubmits: 0,\n webgpuDeviceLosses: 0,\n webgpuErrors: 0,\n // See `../surface-image-swap` (`StaticImageSwapCounters`) for what these mean. A device probe\n // reads swaps/reverts/encodes to confirm the mechanism WITHOUT console logs, `staticImagesLive`\n // to confirm how much of the set is engaged, and `staticImageUrlsLive` to confirm it doesn't leak.\n ...createStaticImageSwapCounters(),\n };\n}\n\n// Is there a WebGPU API on this page AT ALL? The gate's synchronous short-circuit (see `openGate`):\n// no `navigator.gpu` means no promise is created, no microtask is scheduled and no create is ever\n// deferred — which is what keeps jsdom and every non-WebGPU browser on the byte-identical path under\n// the default `effectsRenderer: \"auto\"`. Deliberately NOT `acquireWebgpuDevice`, which would answer\n// the same question one turn of the event loop later.\nfunction hasWebgpuApi(): boolean {\n return Boolean(\n (globalThis.navigator as (Navigator & { gpu?: unknown }) | undefined)?.gpu,\n );\n}\n\n/**\n * One shader to compile AHEAD of the node that will need it. See {@link WebglShaderRuntime.warmPrograms}.\n *\n * The same three values the create path derives from a node's `data-godot-shader-*` attributes, so a caller that\n * knows its scene's shader set can name them without a node existing yet.\n */\nexport interface WebglWarmSpec {\n /** The shader id — the `programCache` key, and what `resolveShaderSource` is ultimately asked about. */\n shaderKey: string;\n /** Resource path passed to `resolveShaderSource`. Omit when the consumer resolves by uid alone. */\n path?: string;\n /** Resource uid passed to `resolveShaderSource`. */\n uid?: string;\n}\n\n/** A persistent WebGL shader runtime for a mounted scene root (see createWebglShaderRuntime). */\nexport interface WebglShaderRuntime {\n /** Diff the live `[data-godot-shader-webgl]` set against the bindings: keep+update\n * unchanged nodes (no forced layout), create new ones, dispose gone ones. */\n reconcile(): void;\n /** Live retune the backing-store resolution (devicePixelRatio × clamped `scale`) without a\n * dispose+recreate — for an adaptive-quality consumer that lowers fill cost under load. While a\n * pin is in force (see `setStaticShaderPixelRatio`) AND the runtime is in frozen mode this\n * re-sizes nothing: the frozen backing stores (and their cached frames) are deliberately held\n * still. The new scale still takes effect for live bindings the moment frozen mode is left. */\n setRenderScale(scale: number): void;\n /** Live set/clear the pinned FROZEN backing-store ratio (see `staticShaderPixelRatio`). Pass\n * `undefined` (or a non-positive value) to un-pin, restoring `devicePixelRatio × renderScale`\n * sizing for frozen bindings too. Only frozen bindings are affected — a live binding is never\n * sized by the pin. */\n setStaticShaderPixelRatio(ratio: number | undefined): void;\n /** Live retune the animated re-render FPS cap (0 = uncapped). */\n setFps(fps: number): void;\n /** Live toggle frozen-TIME (single-shot) mode: render each shader ONCE at a pinned TIME and stop the loop\n * (true), or resume per-frame animation (false). The low-end fallback that renders a correct still frame\n * instead of a per-frame loop — see the `staticShaders` option. */\n setStaticShaders(value: boolean): void;\n /** Live toggle of the frozen-surface image swap (see the `staticShaderImages` option and\n * `../surface-image-swap`). Turning it OFF reverts every live swap immediately and revokes its\n * object URLs — the kill switch, safe to throw at any time. Turning it back ON re-arms the\n * runtime's CONFIGURED policy (a boolean here never replaces it) and every binding re-earns. */\n setStaticShaderImages(value: boolean): void;\n /** HOST-DRIVEN revert of the surface image swap, WITHOUT blocking: with no argument every swapped\n * surface is handed back to its canvas; with a node list, only the bindings at (or under) those\n * elements. Each affected surface restarts its gate and re-earns the swap on its own. This is how\n * a host that knows something the runtime cannot see — it is about to re-parent a subtree, it\n * just re-themed, its own occlusion pass changed its mind — un-shadows a stale stand-in\n * immediately instead of waiting for a watchdog window. */\n invalidateStaticSurfaces(nodes?: Iterable<HTMLElement>): void;\n /** The runtime's live counters (see `WebglShaderRuntimeStats`). Returns the SAME live object every\n * call — read-only by convention; snapshot (spread) it to diff. All-zero on the no-op handle.\n * `.profile` carries the opt-in per-frame cost attribution (`effectsProfiling`) and is NULL\n * whenever it was not measured, the no-op handle included. */\n stats(): WebglShaderRuntimeStats;\n /**\n * TEST / DIAGNOSTIC HOOK: the binding at (or under) `node` re-rendered into an offscreen texture\n * and read back as tightly-packed RGBA (PREMULTIPLIED, top-down, at the canvas's BACKING-STORE\n * size), or null when there is nothing to read — no binding there, a zero-sized canvas, or a\n * binding rendering on WebGL.\n *\n * WEBGL RETURNS NULL ON PURPOSE, and it is not a gap: that canvas holds readable 2D pixels, so a\n * caller who wants them uses `getImageData` on it. This exists because a WebGPU canvas has no such\n * path — `drawImage`/`toDataURL` from one are blank under headless Chrome and pathological on\n * Android (docs/perf-harness.md S7) — so the frame has to be produced a SECOND time, into a\n * texture that `copyTextureToBuffer` can reach (`../webgpu/readback`). It renders the binding's\n * CURRENT state at the runtime's current TIME, which for a frozen binding is exactly the frame on\n * screen.\n *\n * This is the WebGL↔WebGPU image-parity harness's capture path.\n */\n captureNodePixels?(node: HTMLElement): Promise<Uint8Array | null>;\n /**\n * Resolve, transpile and LINK these shaders now, so the node that eventually wants one finds it cached.\n * Resolves to how many of `specs` ended with a usable program (cached ones included).\n *\n * WHY THIS EXISTS. `compileProgramAsync` yields to the driver before asking a blocking question, which on a\n * device with `KHR_parallel_shader_compile` makes a cold link nearly free. WITHOUT that extension — and it is\n * absent on real Android hardware; a Moto G86 answers null for it — `ready()` reports true immediately and\n * `finish()` then blocks on `LINK_STATUS` for as long as the driver needs. One measured link cost 90.3 ms of\n * main thread, and it landed mid-combat because that was the first frame a node asked for that shader.\n *\n * THIS CANNOT MAKE THE LINK FREE, and does not pretend to: the driver's work is the driver's work. It only\n * lets a consumer choose WHEN to pay — behind a loading screen, where 90 ms is invisible, instead of on the\n * frame a card is played. That is the whole claim, and it is the one to verify.\n *\n * PURELY ADDITIVE. Nothing about the lazy create path changes; this only populates the cache it already\n * consults. A spec whose source will not resolve, or will not compile, is counted as a miss and leaves that\n * shader exactly where it was — the node that needs it later keeps its CSS/SVG paint, as it does today. Safe\n * to call more than once and safe to ignore the result.\n */\n warmPrograms(specs: Iterable<WebglWarmSpec>): Promise<number>;\n /** Tear everything down (cancel the rAF loop, disconnect observers, remove canvases). */\n dispose(): void;\n}\n\n/**\n * Create a PERSISTENT WebGL runtime for a mounted scene root. Unlike a teardown+reattach\n * per render, the caller keeps this handle and calls `reconcile()` on each re-render:\n * unchanged shader nodes KEEP their binding (a cheap attribute-only update — no\n * `syncCanvasSize`/`clientWidth` forced layout), new nodes are created, gone nodes\n * disposed. One shared rAF loop renders the live binding set. A no-op handle when WebGL2\n * is unavailable or no shader resolver is configured.\n */\nexport function createWebglShaderRuntime(\n root: HTMLElement,\n options: GodotHtmlRuntimeOptions,\n): WebglShaderRuntime {\n const sharedGl = getShared();\n if (!sharedGl || typeof options.resolveShaderSource !== \"function\") {\n // The no-op handle still carries (all-zero, never-incremented) stats so probes need no null case.\n const noopStats = createWebglShaderRuntimeStats();\n return {\n reconcile() {},\n setRenderScale() {},\n setStaticShaderPixelRatio() {},\n setFps() {},\n setStaticShaders() {},\n setStaticShaderImages() {},\n invalidateStaticSurfaces() {},\n stats: () => noopStats,\n // No WebGL2 and/or no resolver: there is nothing to warm, and a caller must not have to care.\n warmPrograms: () => Promise.resolve(0),\n dispose() {},\n };\n }\n const resolveShaderSource = options.resolveShaderSource;\n // Optional per-binding render notification (see `GodotHtmlRuntimeOptions.onBindingRendered`): fired\n // right after a renderNode HANDLED this binding's current frame — a real GL draw, a cache-hit\n // blit, or a same-canvas cache hit that simply confirms it remains presented. Absent ⇒\n // byte-identical behavior.\n const onBindingRendered = options.onBindingRendered;\n // The third argument (see `GodotEffectRenderInfo`), read off the binding's compiled program — which\n // is fixed for the binding's life (a shader swap recreates the binding, see `NodeBinding.shaderKey`),\n // so this is a pure restatement of facts the runtime already holds. A fresh object per firing, and\n // it can afford to be: the callback fires only behind a handled/presented frame, normally a full-canvas\n // blit and usually a GL submit too, next to which one small literal is nothing — and a shared mutable one would hand a\n // consumer that keeps the reference somebody else's blend mode.\n const renderInfoOf = (binding: NodeBinding): GodotEffectRenderInfo => ({\n usesScreenTexture: binding.program.usesScreenTexture,\n usesScreenUv: binding.program.usesScreenUv,\n blend: binding.program.blend,\n // The frame this canvas holds, as the backend just named it (null for anything that is not a\n // frozen, content-addressed frame) — see `GodotEffectRenderInfo.staticKey`.\n staticKey: binding.lastStaticKey,\n });\n const { gl } = sharedGl;\n // Backing-store pixel ratio for every LIVE node canvas (devicePixelRatio × clamped renderScale). <1\n // is the low-end resolution knob. MUTABLE so an adaptive consumer can retune it live via\n // setRenderScale (the canvases re-size on the next tick) without a dispose+recreate. `sizeCanvas`\n // is what every sizing path (incl. the ResizeObserver) reads, so a resize after a live change uses\n // the CURRENT ratio.\n let pixelRatio = effectivePixelRatio(options.renderScale);\n // OPT-IN pinned backing ratio for FROZEN bindings (see `staticShaderPixelRatio`). `undefined` =\n // not pinned, which is every consumer that doesn't set it: the sizing path below then resolves to\n // `pixelRatio` in both modes, exactly as it always did. MUTABLE via setStaticShaderPixelRatio.\n let staticPixelRatio = normalizeStaticPixelRatio(\n options.staticShaderPixelRatio,\n );\n // Is the pin in force RIGHT NOW? Only in frozen mode: a live binding is animating against the\n // current fit and must keep tracking `devicePixelRatio × renderScale`.\n const pinnedRatio = (): number | undefined =>\n staticShaders ? staticPixelRatio : undefined;\n // Optional FPS cap for the animated re-render loop (options.shaderFps). 0/undefined → uncapped.\n // MUTABLE for live setFps (adaptive quality keeps fps high but can lower it toward the floor).\n let minFrameTime =\n options.shaderFps && options.shaderFps > 0 ? 1 / options.shaderFps : 0;\n // Frozen-TIME (single-shot) mode: render each shader ONCE at a pinned representative TIME, then stop the loop,\n // instead of re-rendering TIME-driven shaders every frame. A single frozen frame is correct for any shader\n // (the render_mode blend is a node CSS mix-blend-mode, applied regardless of frame count, so an additive glow\n // still glows) and costs ~zero ongoing GPU — the low-end fallback that replaces a per-shader CSS approximation.\n // MUTABLE via setStaticShaders so an adaptive consumer can drop into it as a downgrade rung. `staticShaderTime`\n // is the pinned TIME in seconds (default 1; the consumer tunes it to land animated loops on a representative\n // phase).\n let staticShaders = options.staticShaders ?? false;\n // Frozen-surface image swap (see `../surface-image-swap`). The option carries the POLICY (a plain\n // `true` = the content-key gate that shipped first); the runtime only decides WHEN to consult it.\n // Ships ON — a consumer that never enters frozen mode is unaffected either way under the default\n // policy, since a content-key swap only ever acts on a cacheable frozen frame — and\n // `staticShaderImages: false` / `setStaticShaderImages(false)` is the kill switch, which takes the\n // pre-existing code path exactly (a binding with no swap state is untouched by it).\n const staticSurfacePolicy = options.staticShaderImages ?? true;\n const staticTime =\n typeof options.staticShaderTime === \"number\" ? options.staticShaderTime : 1;\n // Cap for GL texture uploads (longest edge); a larger image is downscaled before texImage2D to avoid the\n // main-thread upload spike. Undefined ⇒ native size.\n const maxTextureDim = options.maxTextureDimension;\n // Opt-in SCREEN_TEXTURE capture. Off (default) ⇒ screen-reading shaders take the same\n // unsupported/CSS-fallback path as before support existed.\n const enableScreenCapture = options.enableScreenTextureCapture === true;\n const maxScreenCaptureDim =\n options.maxScreenCaptureDim && options.maxScreenCaptureDim > 0\n ? options.maxScreenCaptureDim\n : DEFAULT_MAX_SCREEN_CAPTURE_DIM;\n // The WebGL backend (see `./shader-backend`). Built at create and kept for the runtime's whole\n // life whatever `backend` currently is, because it is what every failure path adopts — no adapter,\n // a rejected pipeline, a device lost mid-run, a single shader WebGPU cannot express — and a\n // fallback constructed at the moment it was needed would be a second way to fail, on the\n // device-loss path, which is the worst possible moment to discover the shared GL context cannot be\n // had either. The DOM half of a create, the program cache, the static-frame cache and the\n // SCREEN_TEXTURE composite stay here and are reached back through these deps.\n const glBackend = createWebglShaderBackend(sharedGl, {\n maxTextureDim,\n captureScreenTexture: (binding, rootRect) =>\n captureScreenTexture(sharedGl, binding, rootRect, maxScreenCaptureDim),\n staticFrameKey: memoStaticFrameKey,\n lookupStaticFrame,\n storeStaticFrame,\n });\n // The renderer NEW bindings are created on. NULL means PENDING: this runtime asked for WebGPU and\n // the device has not arrived yet (see the renderer gate below). Nothing is created in that state —\n // an opted-in node keeps its CSS/SVG paint, exactly as it does while its shader source is still\n // being fetched — and it is never null again once a backend has been adopted.\n let backend: ShaderRenderBackend | null = null;\n // The WebGPU backend once adopted, kept separately from `backend` because it survives a\n // per-binding decision: a runtime on WebGPU still hands SOME bindings to `glBackend`.\n let gpuBackend: WebgpuShaderBackend | null = null;\n let lastTime = nowSeconds();\n let disposed = false;\n // Instrumentation counters (see WebglShaderRuntimeStats): plain `++` writes on the hot paths,\n // exposed live via the handle's `stats()`. Purely observational — no render decision reads them.\n const runtimeStats = createWebglShaderRuntimeStats();\n // OPT-IN per-frame cost attribution (see `ShaderProfile`), or null forever. ONE object for the\n // runtime's life, mutated in place and published by reference through `stats().profile`, so an off\n // runtime carries a null field and the render path takes no clock reading at all.\n const profile: ShaderProfile | null =\n options.effectsProfiling === true ? createShaderProfile() : null;\n runtimeStats.profile = profile;\n // The surface image swap for THIS runtime: it owns the encode queue, the gate timers, the\n // watchdog and the swapped set, under `staticSurfacePolicy`. `null` IS the kill switch — no\n // binding is ever given swap state, and every swap call site is a no-op on a stateless binding.\n let surfaceSwapper: StaticSurfaceSwapper | null = createStaticSurfaceSwapper(\n staticSurfacePolicy,\n runtimeStats,\n );\n // Keyed by the shader NODE element — its DOM identity is stable across re-renders for an\n // unchanged node (Stage B keyed reconcile), so its binding is reused, not recreated.\n const bindings = new Map<HTMLElement, NodeBinding>();\n // Per-node generation guard for the async source-resolve race: a node removed (or its\n // shader re-keyed) before resolveShaderSource settles must not create a stale binding.\n const pending = new Map<HTMLElement, symbol>();\n\n // Size ONE binding's backing store at the ratio that applies to it right now: the PIN while\n // frozen (see `staticShaderPixelRatio`), else the live `devicePixelRatio × renderScale`. The\n // longest-edge clamp rides the pinned path only — the live path is bounded by the device and\n // has never been clamped, so an un-pinned runtime sizes byte-identically to before. Every\n // in-runtime `syncCanvasSize` goes through here so the two paths can't drift.\n //\n // This is the RUNTIME-WIDE half of the density only. The per-binding half — how magnified THIS\n // surface is (`SURFACE_PIXEL_RATIO_ATTR`) — is folded in by `syncCanvasSize` itself, so it applies\n // to whichever of the two ratios above won and to the create-time sizing alike.\n const sizeCanvas = (\n binding: NodeBinding,\n contentRect?: { width: number; height: number },\n ): void => {\n const pin = pinnedRatio();\n if (pin !== undefined) runtimeStats.pinnedCanvasSyncs++;\n syncCanvasSize(\n binding,\n pin ?? pixelRatio,\n contentRect,\n runtimeStats,\n backingDimLimit(\n pin === undefined ? undefined : MAX_PINNED_BACKING_DIM,\n // The backend's OWN ceiling, where it has one (WebGPU: `maxTextureDimension2D`). Absent on\n // WebGL — undefined here keeps an un-pinned GL runtime sizing byte-identically to before.\n binding.backend.maxBackingDim?.(),\n ),\n );\n };\n\n // ---- first sizing: MUTATE all → MEASURE all → WRITE all -------------------------------------\n //\n // THE layout read of this runtime, and the only one that is not served from a cache: a brand-new\n // binding's self-layer box. It cannot be avoided (each binding has its own self-layer) and it\n // cannot be taken BEFORE `createBinding` either — that function zeroes the self-layer's border,\n // and these layers are `box-sizing: border-box`, so the box a pre-measure would read is not the\n // box the canvas has to cover.\n //\n // What CAN be avoided is paying a style+layout FLUSH per node. A create finishes inside its own\n // microtask continuation (see `scheduleCreate`), so building + measuring in one step made a\n // reconcile that mounts N shader nodes interleave N writes with N reads — N forced layouts inside\n // one task. Measured on a moto g86 5G combat trace: 53.6 ms of `get clientWidth` self time inside\n // `UpdateLayoutTree`/`performLayout` across two ~12 s traces, 14.8 ms of it in a single 68.4 ms\n // long task; and on this repo's own `effects-runtime --mechanism shaders-live` run, 12 of the\n // trace's 14 `Layout` events were one per shader node, 0.16–0.35 ms apart.\n //\n // So the create burst is phased, exactly as `../particles/runtime`'s reconcile is: every canvas is\n // built first (that is `createBinding`), then every box is read back-to-back with no write between\n // them — one flush for the whole run — then every backing store is sized from the cache. The drain\n // is a MICROTASK, so nothing is deferred by a frame and no rAF tick can see an unsized canvas.\n const awaitingFirstSize: NodeBinding[] = [];\n let firstSizeDrainArmed = false;\n // PASS 2 — MEASURE. The one `clientWidth`/`clientHeight` read, in the one place that performs it.\n const readBoxInto = (binding: NodeBinding): void => {\n binding.boxW = binding.selfLayer.clientWidth;\n binding.boxH = binding.selfLayer.clientHeight;\n binding.boxMeasured = true;\n };\n const drainFirstSizes = (): void => {\n firstSizeDrainArmed = false;\n if (disposed || awaitingFirstSize.length === 0) return;\n const queued = awaitingFirstSize.splice(0, awaitingFirstSize.length);\n const live: NodeBinding[] = [];\n for (const binding of queued) {\n // Disposed, or its node re-bound, between the create and this drain: it owes nothing.\n if (bindings.get(binding.node) !== binding) continue;\n // Parked in that window: reading its box is exactly the forced layout dormancy exists to\n // avoid, so the sizing is handed to the wake — `syncCanvasSizeOrDefer`'s contract, and the\n // reason this is not a silent skip. (Not reachable today: only a host `reconcile()` can park\n // a binding, and a task cannot interleave with the microtask this drain runs in. It is\n // written this way because a binding that quietly never gets sized draws at the canvas\n // element's 300x150 default, which is a wrong picture rather than a missing one.)\n if (binding.dormant) {\n binding.canvasSyncDeferred = true;\n continue;\n }\n live.push(binding);\n }\n for (const binding of live) {\n if (!binding.boxMeasured) readBoxInto(binding);\n }\n // PASS 3 — WRITE. Every box is cached now, so this run resolves at tier 2 and reads nothing.\n for (const binding of live) sizeCanvas(binding);\n if (live.length > 0) scheduleRender();\n };\n const queueFirstSize = (binding: NodeBinding): void => {\n // Born parked: the create pays no sizing at all (`createBinding` already marked the sync as\n // deferred), and the wake pays exactly one.\n if (binding.dormant) return;\n awaitingFirstSize.push(binding);\n if (firstSizeDrainArmed) return;\n firstSizeDrainArmed = true;\n if (typeof queueMicrotask === \"function\") {\n queueMicrotask(drainFirstSizes);\n } else {\n void Promise.resolve().then(drainFirstSizes);\n }\n };\n\n // ---- rect cache ----------------------------------------------------------------------------\n //\n // The screen-space paths (SCREEN_UV, SCREEN_TEXTURE, SCREEN_PIXEL_SIZE) need viewport rects: the\n // scene root's, and each such node's self-layer. Measuring them inside the render loop is a\n // FORCED LAYOUT per frame (and, for the self-layer, per SCREEN_UV node per frame) — a fixed\n // main-thread cost on screens where nothing moves, which is what phone traces showed. So the\n // rects are cached and re-read in ONE batch at tick start, and only when something could have\n // moved them:\n // - `reconcile()` (the host re-rendered → the DOM may have moved),\n // - a binding's ResizeObserver (its own box changed),\n // - a window resize,\n // - a new binding appearing,\n // - and a conservative TTL, so a CSS-transition-driven move (which fires none of the above)\n // can't leave a SCREEN_UV sample badly stale — worst case it lags by RECT_CACHE_MAX_AGE_S.\n // Nothing is read at all when no live binding actually needs a rect (the common case).\n const RECT_CACHE_MAX_AGE_S = 0.12;\n let cachedRootRect: ViewportRect | null = null;\n let rectsReadAt = Number.NEGATIVE_INFINITY;\n let rectsStale = true;\n const invalidateRects = (): void => {\n rectsStale = true;\n };\n // The root rect is the SAME for every shader node: served from the cache, read lazily only if a\n // render needs it before any batch ran.\n const getRootRect = (): ViewportRect =>\n (cachedRootRect ??= root.getBoundingClientRect());\n // ONE batched layout flush: read the root rect and every rect-consuming binding's self-layer\n // back-to-back (no interleaved writes ⇒ the browser flushes layout once), or nothing at all when\n // the cache is still fresh.\n const refreshRects = (): void => {\n const now = nowSeconds();\n if (!rectsStale && now - rectsReadAt < RECT_CACHE_MAX_AGE_S) return;\n let read = false;\n for (const binding of bindings.values()) {\n // Dormant (parked) and suspended (occluded) bindings measure nothing — the whole point of\n // both states is that they cost no layout.\n if (binding.dormant || binding.suspended) continue;\n if (!readsScreenRect(binding.program)) continue;\n if (!read) {\n cachedRootRect = root.getBoundingClientRect();\n read = true;\n }\n if (needsLayerRect(binding.program)) {\n binding.layerRect = binding.selfLayer.getBoundingClientRect();\n }\n }\n // Only mark the cache fresh once it actually holds a measurement; with no rect consumer there\n // is nothing to cache (and nothing was read).\n if (read) {\n rectsStale = false;\n rectsReadAt = now;\n }\n };\n const onWindowResize = (): void => invalidateRects();\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"resize\", onWindowResize);\n }\n\n // ---- shared ResizeObserver -----------------------------------------------------------------\n //\n // ONE observer for the whole runtime, dispatching by observed target, instead of one per binding.\n // Each `new ResizeObserver` is its own registration + closure and its own callback slot in the\n // browser's observation loop; a create-burst (a re-keyed hand of cards) built dozens at once. The\n // per-binding semantics are preserved exactly: a target's LAST entry in a delivery wins (an\n // intermediate size would only be overwritten anyway), and one `invalidateRects` per delivery.\n const observedBindings = new Map<Element, NodeBinding>();\n const sharedObserver =\n typeof ResizeObserver === \"undefined\"\n ? null\n : new ResizeObserver((entries) => {\n const latest = new Map<Element, ResizeObserverEntry>();\n for (const entry of entries) latest.set(entry.target, entry);\n let changed = false;\n let liveChanged = false;\n for (const [target, entry] of latest) {\n const binding = observedBindings.get(target);\n if (!binding) continue;\n // The observer already measured the new size — use it instead of forcing a\n // clientWidth/clientHeight reflow. Reads the CURRENT ratio (`sizeCanvas`) so a resize\n // after a live setRenderScale — or under a frozen-mode pin — uses that ratio rather\n // than the create-time one. A DORMANT binding defers the sync (its box read is\n // exactly what dormancy exists to avoid) but still\n // CACHES the delivered box: the wake's deferred syncCanvasSize passes no contentRect,\n // so without this it would size the canvas from the stale pre-park box (a 0×0 box —\n // a hidden ancestor — is left out; the wake falls back to a fresh clientWidth read).\n if (binding.dormant) {\n if (entry.contentRect.width > 0 && entry.contentRect.height > 0) {\n binding.boxW = entry.contentRect.width;\n binding.boxH = entry.contentRect.height;\n binding.boxMeasured = true;\n }\n binding.canvasSyncDeferred = true;\n } else {\n sizeCanvas(binding, entry.contentRect);\n liveChanged = true;\n }\n binding.dirty = true;\n changed = true;\n }\n // A box changed ⇒ this node's (and possibly every node's) cached viewport rect is stale.\n if (changed) invalidateRects();\n // A LIVE binding was resized: the realloc CLEARED its backing store, so the frozen-mode\n // (or non-TIME) loop — which self-stops — must be re-kicked or the node stays blank/stale\n // until an unrelated event schedules a tick (the \"widened window never drawn\" half of the\n // UNDERDOCKS band bug). Dormant-only deliveries schedule nothing: their work runs on wake.\n if (liveChanged) scheduleRender();\n });\n const observeBinding = (binding: NodeBinding): void => {\n if (!sharedObserver) return;\n observedBindings.set(binding.selfLayer, binding);\n sharedObserver.observe(binding.selfLayer);\n };\n const unobserveBinding = (binding: NodeBinding): void => {\n if (!sharedObserver) return;\n observedBindings.delete(binding.selfLayer);\n sharedObserver.unobserve(binding.selfLayer);\n };\n\n // ---- dormancy ------------------------------------------------------------------------------\n //\n // See `../shader-dormant` for the contract. Expiry is ONE per-runtime timer, armed only while at\n // least one binding is dormant. It compares monotonic ORDINALS, not a clock: the sweep disposes\n // every binding that was already dormant when the timer was armed (so ≥ one full interval), then\n // re-arms if any remain. No per-binding timer, no `nowSeconds()` reading, no drift.\n let dormantSeq = 0;\n let dormantSweepArmedAt = 0;\n let dormantSweepTimer: ReturnType<typeof setTimeout> | null = null;\n const armDormantSweep = (): void => {\n if (disposed || dormantSweepTimer !== null) return;\n dormantSweepArmedAt = dormantSeq;\n dormantSweepTimer = setTimeout(\n sweepDormant,\n DORMANT_DISPOSE_SECONDS * 1000,\n );\n };\n function sweepDormant(): void {\n dormantSweepTimer = null;\n if (disposed) return;\n let remaining = false;\n const expired: HTMLElement[] = [];\n for (const [node, binding] of bindings) {\n if (!binding.dormant) continue;\n if (binding.dormantSeq <= dormantSweepArmedAt) expired.push(node);\n else remaining = true;\n }\n for (const node of expired) {\n const binding = bindings.get(node);\n if (!binding) continue;\n disposeBinding(binding);\n bindings.delete(node);\n }\n if (remaining) armDormantSweep();\n }\n\n // ONE shared loop over the LIVE binding set. `animated` is recomputed each tick (the\n // set changes across reconciles) and the loop self-stops when nothing is animated/dirty;\n // create/update re-kick it via scheduleRender (so adding the first TIME shader restarts it).\n // Wakeups go through the pacer: under an FPS cap it PARKS on a timer to the cap boundary\n // instead of spinning a rAF per display frame (see ../effects-loop-pacing).\n const pacer = createEffectsLoopPacer(() => tick(), options.effectsLoopPacing);\n // Seconds until the cap allows the next animated re-render. 0 = now: uncapped, or frozen mode\n // (whose one-shot render is never deferred).\n const capRemaining = (now: number): number =>\n staticShaders || minFrameTime <= 0 ? 0 : minFrameTime - (now - lastTime);\n const tick = (): void => {\n if (disposed) return;\n // In frozen mode TIME is pinned (a deterministic, cacheable still frame); otherwise it advances live.\n const time = staticShaders ? staticTime : nowSeconds();\n // FPS cap only throttles ANIMATED re-renders; a frozen-mode one-shot must not be deferred.\n // (TIME still advances from the shared clock, so live animations stay time-correct — just sampled lower.)\n const remaining = capRemaining(time);\n if (!pacer.isDue(remaining)) {\n runtimeStats.capDeferrals++;\n pacer.arm(remaining);\n return;\n }\n lastTime = time;\n // Batch every layout read this tick could need UP FRONT (usually: none at all).\n refreshRects();\n let animated = false;\n // Bindings this tick actually rendered, so a tick that found nothing to draw books no `ticks`\n // and stays out of the buckets' denominator (see `ShaderProfile`).\n let profBindings = 0;\n // The tick's frame bracket (see `ShaderRenderBackend`). A no-op on WebGL, where every draw is\n // submitted as it is issued; on WebGPU it opens the ONE command encoder whose single submit per\n // tick is exactly the win measured in S7. BOTH backends are bracketed, always: under a WebGPU\n // runtime some bindings are on WebGL (see `webgpuBindingFallbacks`), and either set may be the\n // one that draws this tick.\n glBackend.beginFrame();\n gpuBackend?.beginFrame();\n for (const binding of bindings.values()) {\n // Parked binding (see ../shader-dormant): the canvas is hidden and the box unmeasured, so\n // there is nothing to draw; `dirty` is left set for the wake.\n if (binding.dormant) continue;\n // Occluded subtree (see ../effects-suspend): render nothing and don't keep the loop alive.\n // `dirty` is left set, so the resume reconcile re-renders it at the current TIME.\n if (binding.suspended) continue;\n // A TIME shader keeps the loop alive ONLY in live mode; frozen, it renders once (when dirty) and stops.\n const animatedShader = binding.program.usesTime && !staticShaders;\n if (animatedShader) animated = true;\n if (animatedShader || binding.dirty) {\n if (profile) profBindings++;\n const handled = binding.backend.renderNode(\n binding,\n time,\n staticShaders,\n getRootRect,\n runtimeStats,\n profile,\n );\n clearLoadingFallbackIfReady(binding);\n binding.dirty = false;\n if (handled && onBindingRendered)\n onBindingRendered(\n binding.node,\n binding.canvas,\n renderInfoOf(binding),\n );\n }\n }\n gpuBackend?.endFrame();\n glBackend.endFrame();\n if (profile && profBindings > 0) {\n profile.ticks++;\n profile.bindings += profBindings;\n }\n if (animated) scheduleRender();\n };\n // Kick the loop from a wake path (a create/update, a reconcile, a live quality retune). A wakeup\n // already in flight is left alone — including a PARK, which fires within one cap interval, exactly\n // the worst case of the pre-pacing skip-and-re-arm.\n const scheduleRender = (): void => {\n if (disposed || pacer.isArmed()) return;\n pacer.arm(capRemaining(nowSeconds()));\n };\n\n // Render ONE binding right now (not on the next rAF). Used after a window/size change resizes the canvas —\n // assigning canvas.width CLEARS it, so without an immediate re-render the canvas would be blank until the next\n // tick (a one-frame flicker). Cheap: a single node draw + (only for SCREEN_UV) one getBoundingClientRect.\n const renderBindingNow = (binding: NodeBinding): void => {\n if (disposed) return;\n // A suspended (occluded) or dormant (parked, hidden canvas) binding has nothing to flicker —\n // leave it dirty so the resume/wake renders it at the current TIME instead of painting now.\n if (binding.suspended || binding.dormant) {\n binding.dirty = true;\n return;\n }\n const time = staticShaders ? staticTime : nowSeconds();\n // A synchronous out-of-loop render is about to happen (the parked/suspended early-return above\n // renders nothing, so it deliberately does not count).\n runtimeStats.syncRenders++;\n // Serve the rects from the cache (refreshed here if this is the first read since an\n // invalidation), so a burst of window-change renders inside one reconcile still costs at most\n // one layout flush.\n refreshRects();\n // NO profile: this is an out-of-loop anti-flicker render, not a frame the loop paid for, and\n // folding it into the per-frame buckets would inflate a tick that never happened (`ticks` is\n // their denominator). `syncRenders` above is how a probe sees this path at all.\n // Bracketed like a tick: a batching backend must still submit the work this render just\n // recorded, or the anti-flicker frame would never reach the screen.\n binding.backend.beginFrame();\n const handled = binding.backend.renderNode(\n binding,\n time,\n staticShaders,\n getRootRect,\n runtimeStats,\n );\n binding.backend.endFrame();\n binding.dirty = false;\n if (handled && onBindingRendered)\n onBindingRendered(binding.node, binding.canvas, renderInfoOf(binding));\n };\n\n const wireTextureListeners = (binding: NodeBinding): void => {\n for (const dispose of binding.textureLoadDisposers) dispose();\n binding.textureLoadDisposers.length = 0;\n const markDirty = (): void => {\n // A decoded texture can settle with the same dimensions as its placeholder. Its loaded\n // state still changes whether a frozen frame is eligible, and a decode can replace the\n // backend view without touching any DOM attribute, so it is a real key-input writer.\n invalidateStaticFrameKey(binding);\n binding.dirty = true;\n scheduleRender();\n };\n binding.textureLoadDisposers.push(\n onTextureLoaded(binding.texture, markDirty),\n );\n for (const sampler of binding.samplers) {\n binding.textureLoadDisposers.push(\n onTextureLoaded(sampler.entry, markDirty),\n );\n }\n };\n\n const disposeBinding = (binding: NodeBinding): void => {\n unobserveBinding(binding);\n // Drop the stand-in `<img>` and release this binding's object-URL refcount BEFORE the canvas\n // goes: a leaked blob URL outlives the node, the runtime and the scene.\n disposeStaticImage(binding);\n for (const dispose of binding.textureLoadDisposers) dispose();\n binding.textureLoadDisposers.length = 0;\n binding.canvas.remove();\n // Revert the blend we set on the node (it's the consumer's element; clear it in case the node\n // persists after it stops being a shader node, e.g. a reused element).\n binding.node.style.mixBlendMode = \"\";\n // Whatever renderer-owned state this binding holds goes back to its backend (see\n // `ShaderRenderBackend.disposeSurface`) — on GL, its SCREEN_TEXTURE capture texture.\n binding.backend.disposeSurface(binding);\n };\n\n // `syncCanvasSize`, DEFERRED while the binding is dormant. A parked binding's box read is exactly\n // the forced layout dormancy exists to avoid, and however many syncs pile up while it sleeps, the\n // wake pays for ONE.\n const syncCanvasSizeOrDefer = (binding: NodeBinding): void => {\n if (binding.dormant) {\n binding.canvasSyncDeferred = true;\n return;\n }\n sizeCanvas(binding);\n };\n\n // Re-read a KEPT binding's data-attrs (cheap attribute/style reads — NEVER a clientWidth/Height forced\n // reflow; a window change resizes the canvas from the cached box) and update only what changed. This is what\n // makes a re-render with unchanged shader nodes do ZERO forced layout — the fix for the per-render reflow storm.\n const updateBinding = (binding: NodeBinding): void => {\n const node = binding.node;\n const selfLayer = binding.selfLayer;\n let changed = false;\n let rewire = false;\n\n const paramsAttr = node.getAttribute(\"data-godot-shader-params\");\n if (paramsAttr !== binding.paramsAttr) {\n binding.params = parseParams(paramsAttr);\n binding.paramsKey = paramsFrameKey(binding.params);\n binding.paramsAttr = paramsAttr;\n invalidateStaticFrameKey(binding);\n changed = true;\n }\n const kindsAttr = node.getAttribute(\"data-godot-shader-param-kinds\");\n if (kindsAttr !== binding.paramKindsAttr) {\n binding.paramKinds = parseParamKinds(kindsAttr);\n binding.paramKindsKey = paramKindsFrameKey(binding.paramKinds);\n binding.paramKindsAttr = kindsAttr;\n invalidateStaticFrameKey(binding);\n changed = true;\n }\n const modAttr = node.getAttribute(\"data-godot-shader-modulate\");\n if (modAttr !== binding.modulateAttr) {\n binding.modulate = parseModulate(modAttr);\n binding.modulateKey = modulateFrameKey(binding.modulate);\n binding.modulateAttr = modAttr;\n invalidateStaticFrameKey(binding);\n changed = true;\n }\n const fit = backgroundFit(selfLayer);\n if (fit !== binding.fit) {\n binding.fit = fit;\n invalidateStaticFrameKey(binding);\n changed = true;\n }\n const windowAttr = selfLayer.getAttribute(\"data-godot-shader-uv-window\");\n if (windowAttr !== binding.windowAttr) {\n binding.window = parseWindow(windowAttr);\n binding.windowKey = windowFrameKey(binding.window);\n binding.windowAttr = windowAttr;\n invalidateStaticFrameKey(binding);\n placeCanvasWindow(binding.canvas, binding.window);\n // The canvas just MOVED. A stand-in `<img>` copied the old placement, so it is retired here\n // rather than left mis-placed for however long it takes the re-render below to notice the new\n // (window-derived) frame key — which is deferred outright while parked/occluded. Not blocked:\n // an animating window can never reach the stability gate in the first place.\n revertStaticImage(binding, runtimeStats);\n // The canvas sub-rect changed → resize its backing to box×window. The box itself is UNCHANGED by a pure\n // window move, so syncCanvasSize reuses the cached box size (no forced reflow — this was the per-frame\n // clientWidth read that showed up as the #1 main-thread symbol on phones when a large overflowing\n // background's visible window animated). The resize CLEARS the canvas, so re-render synchronously to avoid\n // a one-frame blank (flicker). Dormant ⇒ both are deferred to the wake.\n syncCanvasSizeOrDefer(binding);\n renderBindingNow(binding);\n changed = true;\n }\n const ratioAttr = selfLayer.getAttribute(SURFACE_PIXEL_RATIO_ATTR);\n if (ratioAttr !== binding.pixelRatioAttr) {\n binding.pixelRatioScale = parseSurfacePixelRatio(ratioAttr);\n binding.pixelRatioAttr = ratioAttr;\n // The uv-window block above, for the same three reasons and in the same order. A stand-in\n // `<img>` is showing pixels rendered at the OLD density, so it is retired HERE — deliberately,\n // by the swap's own `revertStaticImage`, and not left for the watchdog to catch as an\n // unexplained re-allocation on some later sweep (which would also reset the surface's gate and\n // make it re-earn a freeze it never lost). The re-size itself reuses the cached box: a density\n // change moves no element box, so this costs NO forced reflow. And the re-size CLEARS the\n // backing store, so re-render synchronously rather than show one blank frame. Dormant ⇒ both\n // are deferred to the wake.\n //\n // Cheap when nothing moved, which is the case that matters: an UNCHANGED attribute is one\n // string compare per sweep and takes none of this.\n revertStaticImage(binding, runtimeStats);\n syncCanvasSizeOrDefer(binding);\n renderBindingNow(binding);\n changed = true;\n }\n const url =\n selfLayer.getAttribute(\"data-godot-shader-texture-url\") ||\n memoizedBackgroundImageUrl(binding, selfLayer);\n const regionAttr = selfLayer.getAttribute(\"data-godot-atlas-region\");\n const repeatAttr = node.getAttribute(\"data-godot-texture-repeat\");\n if (\n url !== binding.textureUrl ||\n regionAttr !== binding.textureRegionAttr ||\n repeatAttr !== binding.textureRepeatAttr\n ) {\n binding.textureUrl = url;\n binding.textureRegionAttr = regionAttr;\n binding.textureRepeatAttr = repeatAttr;\n binding.textureRepeat = nodeTextureRepeats(node);\n binding.textureRegion = parseAtlasRegion(regionAttr);\n binding.texture = binding.backend.resolveNodeTexture({\n node,\n selfLayer,\n textureUrl: url,\n });\n invalidateStaticFrameKey(binding);\n changed = true;\n rewire = true;\n }\n const samplersAttr = node.getAttribute(\"data-godot-shader-samplers\");\n const samplerUrlsAttr = node.getAttribute(\"data-godot-shader-sampler-urls\");\n if (\n samplersAttr !== binding.samplersAttr ||\n samplerUrlsAttr !== binding.samplerUrlsAttr\n ) {\n binding.samplers = binding.backend.resolveSamplers(node, binding.program);\n binding.samplersAttr = samplersAttr;\n binding.samplerUrlsAttr = samplerUrlsAttr;\n invalidateStaticFrameKey(binding);\n changed = true;\n rewire = true;\n }\n if (rewire) wireTextureListeners(binding);\n if (changed) {\n binding.dirty = true;\n scheduleRender();\n }\n };\n\n const scheduleCreate = (\n node: HTMLElement,\n path: string | undefined,\n uid: string | undefined,\n shaderKey: string,\n ): void => {\n const generation = Symbol();\n pending.set(node, generation);\n // Still want this binding? (not disposed, not superseded by a newer create on the same\n // node, the node is still in the DOM, and it hasn't already been created).\n const stillWanted = (): boolean =>\n !disposed &&\n pending.get(node) === generation &&\n node.isConnected &&\n !bindings.has(node);\n\n void (async () => {\n const selfLayer = ownSelfLayer(node);\n if (!selfLayer) {\n pending.delete(node);\n return;\n }\n const url =\n selfLayer.getAttribute(\"data-godot-shader-texture-url\") ||\n backgroundImageUrl(selfLayer);\n\n // THE RENDERER GATE, awaited (see `openGate`). Settled synchronously in every case that\n // matters — a `\"webgl\"` runtime, a browser with no `navigator.gpu`, a second runtime on a page\n // whose device already arrived — so this awaits a real promise only while a WebGPU device is\n // in flight. A create that waits is why there is no surface-less binding state in this\n // runtime: a shader binding SUPPRESSES the node's CSS/SVG paint at create, so one that cannot\n // draw yet would be a visible hole, and this path is already asynchronous for the shader\n // source.\n const runtimeBackend = backend ?? (await settledBackend());\n if (!runtimeBackend || !stillWanted()) {\n pending.delete(node);\n return;\n }\n\n let source: string | undefined;\n const cached = programCache.get(\n programCacheKey(shaderKey, enableScreenCapture),\n );\n if (cached === \"unsupported\") {\n clearLoadingFallbackStyles(selfLayer);\n pending.delete(node);\n return;\n }\n // A WebGPU runtime needs the SOURCE to build this shader's WGSL twin — even when the GL\n // program is already cached, which is what a second node with the same shader (or a WebGL\n // runtime elsewhere on the page) leaves behind. The decision, once made, is cached per shader,\n // so this is at most one extra resolve per shader per page.\n const needsWgsl =\n runtimeBackend.kind === \"webgpu\" &&\n gpuBackend?.peekShader(shaderKey) === undefined;\n if (!cached || needsWgsl) {\n try {\n // Shared across every node with this shader id (see `resolveExpandedShaderSource`):\n // a cold cache with N same-shader nodes now does ONE fetch + ONE include expansion.\n source = await resolveExpandedShaderSource(\n shaderKey,\n path,\n uid,\n resolveShaderSource,\n );\n } catch {\n source = undefined;\n }\n if (!stillWanted()) {\n pending.delete(node);\n return;\n }\n // Only fatal when there is no compiled program either. With one cached, an unresolvable\n // source costs this binding its WebGPU twin (it falls back below), not its render.\n if (source === undefined && !cached) {\n reportUnsupportedRender(\n {\n kind: \"shader\",\n id: shaderKey,\n reason: \"shader source unresolved\",\n },\n options.onUnsupported,\n );\n clearLoadingFallbackStyles(selfLayer);\n pending.delete(node);\n return;\n }\n }\n const program = await getProgramAsync(\n gl,\n shaderKey,\n source ?? \"\",\n options.onUnsupported,\n enableScreenCapture,\n );\n if (!program || !stillWanted()) {\n if (stillWanted()) clearLoadingFallbackStyles(selfLayer);\n pending.delete(node);\n return;\n }\n // PER-BINDING, not per-runtime: a shader WebGPU cannot express falls back BY ITSELF.\n const bindingBackend = await chooseBindingBackend(\n runtimeBackend,\n shaderKey,\n source,\n );\n if (!stillWanted()) {\n pending.delete(node);\n return;\n }\n const binding = createBinding(\n bindingBackend,\n root,\n node,\n selfLayer,\n url,\n shaderKey,\n program,\n );\n pending.delete(node);\n if (!binding) {\n clearLoadingFallbackStyles(selfLayer);\n return;\n }\n bindings.set(node, binding);\n // The canvas is built but not sized yet: this hands the one box read to the batched\n // measure→write drain (see `queueFirstSize`), which runs before any frame can paint.\n queueFirstSize(binding);\n observeBinding(binding);\n // Swap state exists only while the mechanism is on; NO swapper IS the kill switch (every\n // image-swap call site is a no-op on a stateless binding), so an off runtime keeps the old\n // code exactly. `attach` also registers the binding, which is what lets the swapper's own\n // gate/watchdog timers enumerate it without the runtime handing them anything.\n //\n // TWO ENCODE SOURCES. A WebGL binding's canvas holds readable 2D pixels, so the swap reads it\n // directly — the path that shipped first. A WebGPU binding's canvas cannot be read at all\n // (`drawImage`/`toDataURL`/`toBlob` go through presentation: blank headless, pathological on\n // Android — S7), which is why v1 simply never attached one. v2 attaches it with a CAPTURE HOOK\n // instead: the backend re-renders this binding's current frame into an offscreen texture and\n // copies it back (`../webgpu/readback`), and the swap encodes THAT. The canvas is never read.\n //\n // WHY THE FROZEN CONTENT KEY STAYS VALID ACROSS THAT INDIRECTION. The key names a set of pure\n // node-local inputs (shader, size, fit, textures, window, modulate, params, pinned time), and\n // `captureSurface` re-renders at the pinned static time from the binding's CURRENT params — so\n // it reproduces the frame the key names, not merely a frame. If any of those inputs moves, the\n // next render reports a different key and the swap reverts before the stand-in is stale;\n // that is the same invariant the WebGL path has, and it is why the extra render is sound\n // rather than a second interpretation of the state.\n //\n // A binding with neither (no 2D context and no capture hook) is left unattached — the\n // documented kill switch applied per binding, with every swap counter staying 0 for it.\n if (binding.ctx2d) {\n surfaceSwapper?.attach(binding);\n } else if (binding.backend.captureSurface) {\n binding.captureCanvas = async () => {\n // Read the size BEFORE the await: the capture is produced at the backing-store size the\n // render used, and a re-size landing mid-capture must not re-interpret those bytes.\n const w = binding.canvas.width;\n const h = binding.canvas.height;\n // THE BLANK GUARD's assertion, read at the same instant and for the same reason (see\n // `../surface-image-swap`'s BLANK CAPTURES). What this runtime can honestly say about a\n // capture is that it encodes a FULL-VIEWPORT QUAD — `captureSurfacePixels` runs the live\n // pipeline over the whole surface, so there is always draw work — multiplied by the node's\n // MODULATE, which is the one input from outside the fragment program that can zero the\n // frame on its own. Modulated to invisible ⇒ an invisible capture is the truth and is\n // encoded as one.\n // THE RESIDUAL, stated: what happens INSIDE the program is not knowable from here, so a\n // shader that really outputs alpha 0 everywhere reads as a blank capture and is held on\n // its live canvas instead of frozen. That costs one surface — one that paints nothing —\n // its layer saving, it is booked as `staticImageBlankCaptures` rather than inferred, and\n // it is the deliberate side of the trade against a surface that WAS painting silently\n // vanishing behind a PNG of nothing.\n const expectCoverage = binding.modulate[3] > 0;\n const pixels = await captureBindingPixels(binding);\n return pixels\n ? canvasFromPremultipliedRgba(pixels, w, h, expectCoverage)\n : null;\n };\n surfaceSwapper?.attach(binding);\n }\n binding.dirty = true;\n // Born dormant (the host stamped the node before it was ever bound): park it immediately so\n // the create pays NO `syncCanvasSize` — the whole point of the contract for a node that flips\n // in and out of shader-off states.\n if (binding.dormant) {\n binding.dormantSeq = ++dormantSeq;\n armDormantSweep();\n }\n // A new binding has no measured rect yet → let the next tick's batch pick it up.\n invalidateRects();\n wireTextureListeners(binding);\n scheduleRender();\n })();\n };\n\n // Re-evaluate a kept binding's occlusion state (attribute-only, no layout). Resuming re-arms the\n // render: the canvas still holds the pre-suspend frame, so it must be redrawn at the CURRENT\n // time, and its cached rect is almost certainly stale (the layout changed under it).\n const syncSuspended = (binding: NodeBinding): void => {\n const suspended = isEffectsSuspended(binding.node);\n if (suspended === binding.suspended) return;\n binding.suspended = suspended;\n if (!suspended) {\n binding.dirty = true;\n invalidateRects();\n scheduleRender();\n }\n };\n\n // Re-evaluate a kept binding's dormancy (attribute-only, no layout — see `../shader-dormant`).\n // Going dormant hides the canvas and parks the binding; WAKING pays the one deferred\n // `syncCanvasSize` (however many resizes/renderScale steps piled up while asleep collapse into\n // it), unhides, and re-arms the render at the current TIME.\n const syncDormant = (binding: NodeBinding): void => {\n const dormant = isShaderDormant(binding.node);\n if (dormant === binding.dormant) return;\n binding.dormant = dormant;\n if (dormant) {\n // Hides the canvas AND any stand-in `<img>`: a parked node paints nothing either way, and the\n // swap must not resurrect the canvas on the wake below. See `../surface-image-swap`.\n applySurfaceVisibility(binding);\n binding.dormantSeq = ++dormantSeq;\n armDormantSweep();\n return;\n }\n binding.dormantSeq = 0;\n applySurfaceVisibility(binding);\n // The wake re-arms a render this binding's gate may not be able to attribute — and a DEFERRED\n // canvas re-size below reallocates (and CLEARS) the backing store under any stand-in. Hand the\n // decision to the swap module, which reverts (without blocking) exactly when it cannot vouch\n // for the stand-in, instead of leaving a stale `<img>` up until the watchdog polls.\n noteStaticSurfaceWake(binding, runtimeStats, binding.canvasSyncDeferred);\n if (binding.canvasSyncDeferred) {\n binding.canvasSyncDeferred = false;\n sizeCanvas(binding);\n }\n binding.dirty = true;\n invalidateRects();\n scheduleRender();\n };\n\n const reconcile = (): void => {\n if (disposed) return;\n // The host re-rendered, so anything may have moved: the cached viewport rects are stale.\n // (Invalidated at the END too — this pass itself writes canvas sizes/styles.)\n invalidateRects();\n const present = new Set<HTMLElement>(\n root.querySelectorAll<HTMLElement>(\"[data-godot-shader-webgl]\"),\n );\n // Dispose bindings whose node left the DOM.\n for (const [node, binding] of bindings) {\n if (!present.has(node)) {\n disposeBinding(binding);\n bindings.delete(node);\n }\n }\n // Keep+update existing bindings (unchanged shader); create new ones.\n for (const node of present) {\n const path = node.getAttribute(\"data-godot-shader-path\") ?? undefined;\n const uid = node.getAttribute(\"data-godot-shader-uid\") ?? undefined;\n const shaderKey = uid ?? path;\n if (!shaderKey) continue;\n const existing = bindings.get(node);\n if (existing) {\n if (existing.shaderKey === shaderKey) {\n // Dormancy FIRST: `updateBinding` may want a `syncCanvasSize`, which must defer while\n // parked and must run against a woken binding.\n syncDormant(existing);\n syncSuspended(existing);\n updateBinding(existing);\n // The image swap's stability clock: a frozen node renders once and is never visited by\n // the loop again, so \"nothing asked this binding to re-render\" is what evidence of a\n // frozen surface looks like. Attribute-only, no layout — see `../surface-image-swap`.\n if (existing.staticImage) {\n noteStaticImageReconcile(existing, staticShaders, runtimeStats);\n }\n continue;\n }\n // The shader itself swapped on the same element → recreate the program + binding.\n disposeBinding(existing);\n bindings.delete(node);\n }\n // Don't double-schedule while a create is already in flight for this node.\n if (!pending.has(node)) scheduleCreate(node, path, uid, shaderKey);\n }\n // updateBinding may have resized canvases / moved windows; re-measure on the next tick.\n invalidateRects();\n };\n\n // ---- THE RENDERER GATE (see `effectsRenderer` in ../types) ----------------------------------\n //\n // Factories are SYNCHRONOUS and a `GPUDevice` is not, so the gate's whole job is to make the\n // asynchronous case rare and the synchronous case exact:\n //\n // \"webgl\" → adopt WebGL here, having probed nothing.\n // no `navigator.gpu` → adopt WebGL here too. This is the branch that keeps every jsdom\n // test and every non-WebGPU browser on the byte-identical path\n // they were on before this existed, even though the DEFAULT is\n // \"auto\" — no promise, no microtask, no deferred create.\n // device already settled → adopt (or decline) here, synchronously. Page-wide memos make\n // this the answer for every runtime after the first.\n // otherwise → PENDING: creates WAIT (see `scheduleCreate`), so the nodes keep\n // their CSS/SVG paint until the device answers instead of being\n // stripped and left blank.\n //\n // Every failure is SILENT and lands in the stats: `webgpuFallbacks`, `webgpuFallbackReason`,\n // `webgpuBindingFallbacks`.\n let gpuShared: WebgpuShared | null = null;\n let unsubscribeDeviceLost: (() => void) | null = null;\n // THIS runtime's first fallback reason. The module-level latch in `../webgpu/device` is page-wide\n // (one device, one story), but a runtime pinned to `\"webgl\"` must not report a reason it never\n // hit, so the stat is sourced from here.\n let fallbackReason: WebgpuFallbackReason | null = null;\n // The in-flight resolution, awaited by a create that arrived while the gate was still open.\n let gatePromise: Promise<void> | null = null;\n\n const settledBackend = async (): Promise<ShaderRenderBackend | null> => {\n if (backend) return backend;\n await gatePromise;\n return backend;\n };\n\n // WHICH backend renders ONE binding. A WebGPU runtime still hands a binding to WebGL when its\n // shader cannot run there — a `hint_screen_texture` sampler, a construct the WGSL emitter refuses,\n // a module or pipeline that failed to build. That decision is the shader's, is cached per shader\n // by the backend, and is counted here so a silent per-binding fallback stays visible.\n const chooseBindingBackend = async (\n runtimeBackend: ShaderRenderBackend,\n shaderKey: string,\n source: string | undefined,\n ): Promise<ShaderRenderBackend> => {\n const gpu = gpuBackend;\n if (!gpu || runtimeBackend.kind !== \"webgpu\") return runtimeBackend;\n if (await gpu.prepareShader(shaderKey, source)) return gpu;\n runtimeStats.webgpuBindingFallbacks++;\n return glBackend;\n };\n\n // Adopt WebGL, silently, counting it. THE one place a whole-runtime fallback happens, so the\n // counter and the reason can never disagree. (A PER-BINDING fallback is a different event with its\n // own counter — the runtime is still on WebGPU.)\n const fallbackToWebgl = (reason: WebgpuFallbackReason): void => {\n if (disposed || backend?.kind === \"webgl\") return;\n runtimeStats.webgpuFallbacks++;\n if (fallbackReason === null) fallbackReason = reason;\n latchWebgpuFallbackReason(reason);\n backend = glBackend;\n gpuBackend = null;\n scheduleRender();\n };\n\n const adoptWebgpu = (\n shared: WebgpuShared,\n gpu: WebgpuShaderBackend,\n ): void => {\n if (disposed) return;\n gpuShared = shared;\n gpuBackend = gpu;\n backend = gpu;\n unsubscribeDeviceLost = onWebgpuDeviceLost(handleDeviceLost);\n // Creates that parked on the gate resume on their own; this kicks the loop for anything that did\n // not (a runtime whose reconcile ran before the gate settled has nothing else to wake it).\n scheduleRender();\n };\n\n // A lost device takes every WebGPU surface with it — and the canvas ELEMENTS too, because a canvas\n // that has held a webgpu context can never yield a 2d one, so the WebGL rebuild cannot reuse them.\n // Disposing each binding removes its canvas and restores its self-layer paint; `reconcile()` then\n // builds every node again from scratch, on WebGL, with NEW canvas elements.\n const handleDeviceLost = (): void => {\n if (disposed || backend?.kind !== \"webgpu\") return;\n for (const binding of bindings.values()) disposeBinding(binding);\n bindings.clear();\n fallbackToWebgl(\"device-lost\");\n reconcile();\n };\n\n // The PENDING resolution: await the device, then its programs, then adopt — or fall back with\n // whatever `../webgpu/device` classified the failure as. A runtime disposed while this was in\n // flight does nothing at all.\n const resolveWebgpu = async (): Promise<void> => {\n const shared = await acquireWebgpuDevice();\n if (disposed) return;\n if (!shared) {\n fallbackToWebgl(webgpuFallbackReason() ?? \"no-adapter\");\n return;\n }\n const gpu = await createWebgpuShaderBackend(shared, { maxTextureDim });\n if (disposed) return;\n if (!gpu) {\n fallbackToWebgl(webgpuFallbackReason() ?? \"pipeline-error\");\n return;\n }\n adoptWebgpu(shared, gpu);\n };\n\n const openGate = (): void => {\n const wanted = options.effectsRenderer ?? \"auto\";\n if (wanted === \"webgl\") {\n // Adopted DIRECTLY, not through `fallbackToWebgl`: nothing was ever asked for, so nothing\n // fell back, and `webgpuFallbacks` stays 0 — which is how a pinned reference arm reads as one.\n backend = glBackend;\n return;\n }\n if (!hasWebgpuApi()) {\n fallbackToWebgl(\"no-navigator-gpu\");\n return;\n }\n const device = peekWebgpuDevice();\n if (device === null) {\n // Already tried and unavailable (or poisoned by an earlier device loss) — no second probe.\n fallbackToWebgl(webgpuFallbackReason() ?? \"no-adapter\");\n return;\n }\n if (device === undefined) {\n gatePromise = resolveWebgpu();\n return;\n }\n const gpu = peekWebgpuShaderBackend(device, { maxTextureDim });\n if (gpu) {\n adoptWebgpu(device, gpu);\n return;\n }\n if (gpu === null) {\n fallbackToWebgl(webgpuFallbackReason() ?? \"pipeline-error\");\n return;\n }\n // Device in hand, device-scope state still being built (this runtime is the second one on the\n // page, in the same turn as the first): finish asynchronously.\n gatePromise = resolveWebgpu();\n };\n openGate();\n\n // The binding at `node`, or the first one UNDER it — a host that owns a subtree should not have to\n // know which of its descendants gsw bound (the `invalidateStaticSurfaces` convention).\n const bindingAt = (node: HTMLElement): NodeBinding | null => {\n const exact = bindings.get(node);\n if (exact) return exact;\n for (const binding of bindings.values()) {\n if (node.contains(binding.node)) return binding;\n }\n return null;\n };\n\n // ONE binding's pixels, through its backend's capture hook. The body of the handle's\n // `captureNodePixels` (which is now this plus a `bindingAt` lookup), factored out because the\n // frozen-surface image swap needs the SAME production for a WebGPU binding it cannot read: the\n // hook re-renders the binding's current frame into an offscreen texture and copies it back, which\n // is the only sanctioned way pixels leave a WebGPU canvas (`../webgpu/readback`).\n //\n // Null on WebGL (its `captureSurface` is absent — that canvas holds readable 2D pixels), on a\n // zero-sized backing store, and on any capture the device refuses. PER BINDING, not per runtime: a\n // screen-texture node under a WebGPU runtime renders on WebGL and answers null, honestly.\n const captureBindingPixels = async (\n binding: NodeBinding,\n ): Promise<Uint8Array | null> => {\n const capture = binding.backend.captureSurface;\n if (!capture) return null;\n if (binding.canvas.width < 1 || binding.canvas.height < 1) return null;\n // The SAME time the render paths use, so what is captured is the frame the canvas is showing and\n // not a second interpretation of the same state. Rects served from the cache (refreshed here if\n // stale) exactly as `renderBindingNow` does, since a SCREEN_UV shader reads them.\n refreshRects();\n const time = staticShaders ? staticTime : nowSeconds();\n return capture(binding, time, staticShaders, getRootRect);\n };\n\n // Live resolution retune (adaptive quality): change the backing-store pixel ratio and re-size every\n // node canvas to match, then re-render. The shared backbuffer is grow-only so a SMALLER ratio never\n // shrinks it (cheap); the per-binding `syncCanvasSize` reuses each cached box (the box is unchanged by a\n // ratio step), so a retune no longer forces a per-binding reflow.\n const setRenderScale = (scale: number): void => {\n if (disposed) return;\n const next = effectivePixelRatio(scale);\n if (next === pixelRatio) return;\n pixelRatio = next;\n // PINNED + frozen: the pin, not the scale, decides the backing store, so there is nothing to\n // re-size and nothing to re-dirty — which is the point. Re-sizing here would clear every frozen\n // canvas and change every static-frame cache key, i.e. re-render the whole static set on a\n // device that is stepping quality DOWN. The new `pixelRatio` is still recorded above, so live\n // bindings get it as soon as frozen mode is left (or the pin is cleared).\n if (pinnedRatio() !== undefined) return;\n for (const binding of bindings.values()) {\n syncCanvasSizeOrDefer(binding);\n // A runtime-wide re-size changes every frozen frame key. That is the runtime's own decision,\n // not this node churning, so the swap is undone WITHOUT blocking — the binding re-earns the\n // stability gate rather than being disqualified by an adaptive-quality step.\n revertStaticImage(binding, runtimeStats);\n binding.dirty = true;\n }\n scheduleRender();\n };\n\n // Live set/clear of the frozen-mode pin. Takes effect immediately when the runtime is ALREADY\n // frozen (every binding re-sizes to the new pinned ratio and re-renders); otherwise it is just\n // recorded and applied by the next `setStaticShaders(true)`.\n const setStaticShaderPixelRatio = (ratio: number | undefined): void => {\n if (disposed) return;\n const next = normalizeStaticPixelRatio(ratio);\n if (next === staticPixelRatio) return;\n staticPixelRatio = next;\n if (!staticShaders) return;\n for (const binding of bindings.values()) {\n syncCanvasSizeOrDefer(binding);\n revertStaticImage(binding, runtimeStats); // same reasoning as setRenderScale\n binding.dirty = true;\n }\n scheduleRender();\n };\n\n // Live FPS-cap retune. 0/undefined → uncapped. A change just re-kicks the loop so a now-lower cap\n // takes effect (or a raised one resumes animation that had self-stopped).\n const setFps = (fps: number): void => {\n if (disposed) return;\n minFrameTime = fps > 0 ? 1 / fps : 0;\n // A pending park targets the OLD cap boundary — drop it so the new cap arms from here.\n pacer.cancelPark();\n scheduleRender();\n };\n\n // Live frozen-TIME toggle (adaptive quality): entering static renders a single frozen frame per binding then\n // self-stops; leaving it re-dirties every binding so the animation loop restarts (a usesTime shader re-arms\n // `animated`). A no-op if already in the requested mode.\n const setStaticShaders = (value: boolean): void => {\n if (disposed || value === staticShaders) return;\n staticShaders = value;\n // With a pin configured, the mode flip IS a backing-store change (pinned ratio ⇄ live\n // devicePixelRatio × renderScale), so each binding re-sizes here. With no pin the effective\n // ratio is identical in both modes and no sizing pass runs at all — as before.\n const resize = staticPixelRatio !== undefined;\n for (const binding of bindings.values()) {\n if (resize) syncCanvasSizeOrDefer(binding);\n // Leaving frozen mode: the shader animates again, so the still `<img>` must come down NOW\n // rather than at the next tick (which a parked/occluded binding may not reach). Entering it:\n // nothing is swapped yet, so this is a no-op.\n revertStaticImage(binding, runtimeStats);\n binding.dirty = true;\n }\n // The cap does not apply in frozen mode (and a resume wants the full rate back), so a park\n // armed under the previous mode is stale.\n pacer.cancelPark();\n scheduleRender();\n };\n\n // Live kill switch for the frozen-surface image swap. OFF disposes the swapper: every live swap\n // reverts, every object URL is released and every timer is cancelled immediately (the state\n // objects are dropped, so every swap call site goes back to being a no-op). ON builds a fresh\n // swapper under the runtime's CONFIGURED policy — the boolean is a switch, never a policy — and\n // each binding must earn its swap again.\n const setStaticShaderImages = (value: boolean): void => {\n if (disposed || value === (surfaceSwapper !== null)) return;\n if (!value) {\n // Revert BEFORE the swapper goes: a revert un-hides the canvas and books the counter, while\n // the disposal that follows only drops state (a torn-down surface is gone, not handed back).\n for (const binding of bindings.values()) {\n revertStaticImage(binding, runtimeStats);\n }\n surfaceSwapper?.dispose();\n surfaceSwapper = null;\n return;\n }\n surfaceSwapper = createStaticSurfaceSwapper(\n staticSurfacePolicy,\n runtimeStats,\n );\n for (const binding of bindings.values()) {\n surfaceSwapper?.attach(binding);\n // Fresh state knows no frame key (only a render can name one), so the binding is re-dirtied\n // — in frozen mode that is a cache-hit blit, not a GL draw — and the gate starts from the\n // key that render reports. Without this a runtime switched ON after its set had already\n // rendered could never swap anything.\n binding.dirty = true;\n }\n scheduleRender();\n };\n\n // Host-driven revert-without-block (see `WebglShaderRuntime.invalidateStaticSurfaces`). With no\n // argument the swapper hands its whole set back; with elements, every binding AT or UNDER one of\n // them — a host that owns a subtree should not have to know which of its descendants gsw bound.\n const invalidateStaticSurfaces = (nodes?: Iterable<HTMLElement>): void => {\n if (disposed || !surfaceSwapper) return;\n if (!nodes) {\n surfaceSwapper.invalidate();\n return;\n }\n const targets: NodeBinding[] = [];\n for (const element of nodes) {\n const exact = bindings.get(element);\n if (exact) {\n targets.push(exact);\n continue;\n }\n for (const binding of bindings.values()) {\n if (element.contains(binding.node)) targets.push(binding);\n }\n }\n if (targets.length > 0) surfaceSwapper.invalidate(targets);\n };\n\n /**\n * See `WebglShaderRuntime.warmPrograms`. Deliberately built out of the SAME two calls the create path makes —\n * `resolveExpandedShaderSource` then `getProgramAsync` — so a warmed shader and a lazily-created one cannot\n * diverge, and so both dedupe through the same two in-flight maps whichever gets there first.\n *\n * SEQUENTIAL, not `Promise.all`. Each link is a blocking `finish()` on a device without\n * `KHR_parallel_shader_compile`, and firing them together would concatenate those blocks into one long task —\n * the exact shape being moved off the frame path. One at a time leaves a gap for anything else to run in.\n */\n const warmPrograms = async (\n specs: Iterable<WebglWarmSpec>,\n ): Promise<number> => {\n let warmed = 0;\n for (const spec of specs) {\n if (disposed) break;\n // A cached program is already the answer, and asking for its source again would be a fetch for nothing.\n if (\n programCache.get(programCacheKey(spec.shaderKey, enableScreenCapture))\n ) {\n warmed++;\n continue;\n }\n let source: string | undefined;\n try {\n source = await resolveExpandedShaderSource(\n spec.shaderKey,\n spec.path,\n spec.uid,\n resolveShaderSource,\n );\n } catch {\n source = undefined;\n }\n // A miss is not an error: warming is an optimisation, and the node that wants this shader later takes\n // exactly the path it takes today, including reporting its own unsupported-render if it comes to that.\n if (source === undefined || disposed) continue;\n const program = await getProgramAsync(\n gl,\n spec.shaderKey,\n source,\n options.onUnsupported,\n enableScreenCapture,\n );\n if (program) warmed++;\n }\n return warmed;\n };\n\n const dispose = (): void => {\n disposed = true;\n pacer.cancel();\n // Stop listening for a device loss this runtime can no longer act on (the subscription is\n // page-wide and would otherwise outlive every binding it exists to rebuild).\n unsubscribeDeviceLost?.();\n unsubscribeDeviceLost = null;\n if (dormantSweepTimer !== null) {\n clearTimeout(dormantSweepTimer);\n dormantSweepTimer = null;\n }\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"resize\", onWindowResize);\n }\n for (const binding of bindings.values()) disposeBinding(binding);\n // After every binding has released its URL ref: cancels the swapper's gate/watchdog/encode\n // timers, so a disposed runtime leaves nothing armed.\n surfaceSwapper?.dispose();\n surfaceSwapper = null;\n bindings.clear();\n observedBindings.clear();\n sharedObserver?.disconnect();\n pending.clear();\n // A create that landed in the same task as this dispose still has its sizing queued; the drain\n // checks `disposed` too, but dropping the references here keeps a disposed runtime from pinning\n // every binding it just tore down until the microtask runs.\n awaitingFirstSize.length = 0;\n };\n\n return {\n reconcile,\n setRenderScale,\n setStaticShaderPixelRatio,\n setFps,\n setStaticShaders,\n setStaticShaderImages,\n invalidateStaticSurfaces,\n warmPrograms,\n stats: () => {\n // The two GAUGES among the counters, sampled on read: object URLs alive MODULE-wide (across\n // every runtime in the document — the leak probe), and surfaces swapped right now in THIS\n // runtime. The live count is re-derived from the swapper rather than trusted incrementally, so\n // the \"is it engaged?\" measurement (72/72) cannot drift if a revert path is ever missed.\n runtimeStats.staticImageUrlsLive = liveStaticImageUrlCount();\n runtimeStats.staticImagesLive = surfaceSwapper?.liveSwapCount() ?? 0;\n // The still pool's two gauges, on the same contract and DOCUMENT-wide for the same reason\n // (the pool is shared by every swapper in the page). Nothing tells a runtime when the pool\n // evicts, so they are read here rather than tracked — and left unsampled they would report 0\n // forever, which a dashboard cannot tell from \"the budget is off\".\n const stillPool = staticStillPoolStats();\n runtimeStats.staticStillRetainedEntries = stillPool.entries;\n runtimeStats.staticStillRetainedBytes = stillPool.bytes;\n // The renderer gauge, re-derived rather than remembered: a runtime can change backend once\n // (WebGPU → WebGL, on a device loss), and \"pending\" is a state a reader must be able to see.\n runtimeStats.renderer = backend?.kind ?? \"pending\";\n runtimeStats.webgpuFallbackReason = fallbackReason;\n // Counters that live on the BACKEND (submits) and on the page-wide DEVICE (losses, errors),\n // sampled here so a fallback leaves the last real reading standing instead of zeroing it.\n const submits = gpuBackend?.submits();\n if (submits !== undefined) runtimeStats.webgpuSubmits = submits;\n if (gpuShared) {\n runtimeStats.webgpuDeviceLosses = gpuShared.counters.deviceLosses;\n runtimeStats.webgpuErrors = gpuShared.counters.gpuErrors;\n }\n return runtimeStats;\n },\n captureNodePixels: async (\n node: HTMLElement,\n ): Promise<Uint8Array | null> => {\n if (disposed) return null;\n const binding = bindingAt(node);\n if (!binding) return null;\n return captureBindingPixels(binding);\n },\n dispose,\n };\n}\n\nfunction createBinding(\n // The renderer this binding will draw through. It decides the node canvas's context type (a\n // canvas can hold exactly ONE for its lifetime) and resolves the textures the shader samples;\n // every other line here is DOM work that no renderer changes.\n backend: ShaderRenderBackend,\n root: HTMLElement,\n node: HTMLElement,\n selfLayer: HTMLElement,\n url: string | null,\n shaderKey: string,\n program: CompiledProgram,\n): NodeBinding | null {\n const canvas = document.createElement(\"canvas\");\n canvas.setAttribute(\"data-godot-shader-canvas\", \"true\");\n // NB: no `inset:0` — the window placement sets left/top/width/height (full-bleed by default, a sub-rect when\n // the node overflows the viewport), and inset's right/bottom would fight an explicit width/height.\n Object.assign(canvas.style, {\n position: \"absolute\",\n pointerEvents: \"none\",\n });\n const windowAttr = selfLayer.getAttribute(\"data-godot-shader-uv-window\");\n const uvWindow = parseWindow(windowAttr);\n placeCanvasWindow(canvas, uvWindow);\n // The host's magnification for this surface, read at create so the FIRST sizing already has it —\n // a binding that had to discover it on a later sweep would allocate once at the wrong density and\n // then re-allocate, which for a freeze-at-mount surface is the difference between claiming a\n // cached still and minting a new one. An attribute read, not a layout read: it costs nothing here.\n const pixelRatioAttr = selfLayer.getAttribute(SURFACE_PIXEL_RATIO_ATTR);\n // No context ⇒ no binding, before ANY of the DOM mutation below: the node keeps its CSS/SVG\n // paint, exactly as when this was a bare `getContext(\"2d\")`.\n const surface = backend.createSurface({\n canvas,\n node,\n selfLayer,\n textureUrl: url,\n program,\n shaderKey,\n });\n if (!surface) return null;\n // The canvas replaces the node's CSS/SVG paint with the live shader. For a\n // NinePatchRect, the fill's SHAPE (incl. slanted end caps like the doom bar's\n // diagonal end) lives in the `border-image`; the `inset:0` canvas only covers the\n // content box. So: zero the border (box-sizing is border-box → the node keeps its\n // size) so the canvas covers the FULL node, and clip it to the fill's nine-patch\n // alpha via `mask-box-image` (published as `data-godot-shader-fill-mask` — stable,\n // unlike the live border-image which we clear). The live shader then fills the\n // whole segment, caps included, with the diagonal cap; the layer behind (amber\n // HpMiddleground) is covered by the canvas where it paints and by the overlapping\n // neighbour segment / clip Mask elsewhere.\n const fillMask = selfLayer.getAttribute(\"data-godot-shader-fill-mask\");\n if (fillMask) {\n canvas.style.setProperty(\"-webkit-mask-box-image\", fillMask);\n canvas.style.setProperty(\"mask-box-image\", fillMask);\n }\n const loadingFallback = hasLoadingFallback(selfLayer);\n if (!loadingFallback) {\n selfLayer.style.backgroundImage = \"none\";\n } else {\n selfLayer.setAttribute(\"data-godot-shader-loading\", \"1\");\n }\n selfLayer.style.borderImageSource = \"none\";\n selfLayer.style.filter = \"none\";\n selfLayer.style.borderWidth = \"0\";\n selfLayer.insertBefore(canvas, selfLayer.firstChild);\n\n // Honor the shader's render_mode blend via a CSS mix-blend-mode on the NODE element. It must be the node, not\n // the canvas: the canvas lives inside the node's own transform stacking context, so a canvas-level blend\n // can't reach the DOM painted behind the node. An additive shader (blend_add, e.g. card_ripple's frame glow)\n // then adds to the content behind instead of compositing source-over (which would wash it into a flat sheet).\n node.style.mixBlendMode = blendToMixBlendMode(program.blend);\n\n // Capture the raw data-attr strings so `reconcile`'s updateBinding can detect a real\n // change with a cheap string compare (no re-parse, no layout) on a kept binding.\n const paramsAttr = node.getAttribute(\"data-godot-shader-params\");\n const paramKindsAttr = node.getAttribute(\"data-godot-shader-param-kinds\");\n const modulateAttr = node.getAttribute(\"data-godot-shader-modulate\");\n const samplersAttr = node.getAttribute(\"data-godot-shader-samplers\");\n const samplerUrlsAttr = node.getAttribute(\"data-godot-shader-sampler-urls\");\n const textureRegionAttr = selfLayer.getAttribute(\"data-godot-atlas-region\");\n const textureRepeatAttr = node.getAttribute(\"data-godot-texture-repeat\");\n\n // Born dormant? Then the canvas starts hidden and the create-time `syncCanvasSize` — the forced\n // `clientWidth`/`clientHeight` layout this contract exists to skip — is deferred to the wake.\n const dormant = isShaderDormant(node);\n if (dormant) {\n canvas.style.display = \"none\";\n }\n\n const params = parseParams(paramsAttr);\n const paramKinds = parseParamKinds(paramKindsAttr);\n const modulate = parseModulate(modulateAttr);\n\n const binding: NodeBinding = {\n node,\n shaderKey,\n selfLayer,\n root,\n backend,\n canvas,\n ctx2d: surface.ctx2d,\n program,\n texture: surface.texture,\n textureRepeat: nodeTextureRepeats(node),\n textureRegion: parseAtlasRegion(textureRegionAttr),\n samplers: surface.samplers,\n fit: backgroundFit(selfLayer),\n params,\n paramKinds,\n modulate,\n paramsKey: paramsFrameKey(params),\n paramKindsKey: paramKindsFrameKey(paramKinds),\n modulateKey: modulateFrameKey(modulate),\n windowKey: windowFrameKey(uvWindow),\n window: uvWindow,\n windowAttr,\n pixelRatioScale: parseSurfacePixelRatio(pixelRatioAttr),\n pixelRatioAttr,\n boxW: 0,\n boxH: 0,\n boxMeasured: false,\n textureLoadDisposers: [],\n staticKeyEpoch: 0,\n staticKeyMemo: null,\n loadingFallback,\n loadingFallbackCleared: false,\n dirty: true,\n suspended: isEffectsSuspended(node),\n dormant,\n canvasSyncDeferred: dormant,\n dormantSeq: 0,\n layerRect: null,\n paramsAttr,\n paramKindsAttr,\n modulateAttr,\n samplersAttr,\n samplerUrlsAttr,\n textureUrl: url,\n backgroundImageStyle: null,\n backgroundImageParsed: null,\n textureRegionAttr,\n textureRepeatAttr,\n screenCapture: null,\n // The runtime's swapper fills this in right after `createBinding`, when one exists.\n staticImage: null,\n // Nothing painted yet, so this canvas holds no named frame.\n lastStaticKey: null,\n };\n // NOTHING IS MEASURED HERE. This function is now pure MUTATION: the caller queues the binding's\n // first sizing (`queueFirstSize`) so a burst of creates measures in ONE contiguous run instead of\n // one write→read interleave — and one forced style+layout flush — per node. The canvas is\n // unsized until that drain, which no frame can observe: the drain is a microtask and rAF cannot\n // run before the microtask queue empties.\n // The runtime observes this binding through its ONE shared ResizeObserver (see `observeBinding`).\n return binding;\n}\n\nfunction hasLoadingFallback(selfLayer: HTMLElement): boolean {\n return selfLayer.getAttribute(\"data-godot-shader-loading-fallback\") === \"1\";\n}\n\nfunction clearLoadingFallbackIfReady(binding: NodeBinding): void {\n if (\n !binding.loadingFallback ||\n binding.loadingFallbackCleared ||\n !texturesLoaded(binding)\n ) {\n return;\n }\n\n binding.loadingFallbackCleared = true;\n clearLoadingFallbackStyles(binding.selfLayer);\n}\n\nfunction clearLoadingFallbackStyles(selfLayer: HTMLElement): void {\n if (\n !hasLoadingFallback(selfLayer) &&\n !selfLayer.hasAttribute(\"data-godot-shader-loading\")\n ) {\n return;\n }\n selfLayer.removeAttribute(\"data-godot-shader-loading\");\n selfLayer.removeAttribute(\"data-godot-shader-loading-fallback\");\n selfLayer.style.background = \"none\";\n selfLayer.style.backgroundImage = \"none\";\n selfLayer.style.backgroundColor = \"\";\n selfLayer.style.clipPath = \"\";\n selfLayer.style.borderRadius = \"\";\n}\n\n// The tightest of the longest-edge ceilings that apply (the pinned-static cap, the backend's own),\n// or undefined when neither does — which is the un-pinned WebGL case, i.e. every consumer before\n// WebGPU existed, and must stay indistinguishable from it.\nfunction backingDimLimit(\n ...limits: Array<number | undefined>\n): number | undefined {\n let out: number | undefined;\n for (const limit of limits) {\n if (limit === undefined || limit <= 0) continue;\n out = out === undefined ? limit : Math.min(out, limit);\n }\n return out;\n}\n\n// THE SIZING. Its inputs, in full, so the contract is readable in one place:\n//\n// BOX — the self-layer's content-box in CSS px, from the cheapest tier that can answer (below).\n// WINDOW — `binding.window`'s du/dv: the canvas covers only that sub-rect of the box.\n// DENSITY — `dpr` (the runtime-wide term: the live `devicePixelRatio × renderScale`, or the frozen\n// `staticShaderPixelRatio` pin, decided by the caller) TIMES `binding.pixelRatioScale`,\n// this ONE surface's magnification as the host stated it (`SURFACE_PIXEL_RATIO_ATTR`).\n// The two compose rather than replace: the pin answers \"how dense should a frozen\n// surface be on this device\", the attribute answers \"how much of the screen does THIS\n// surface actually cover\", and a frozen magnified surface needs both. An un-stamped\n// binding carries exactly `1`, so its density is the bare `dpr` it always was.\n// MAXDIM — the longest-edge ceiling, on the pinned path only.\n//\n// Deliberately NOT an input: the page's live fit/zoom transform. The box tiers above are all\n// layout-space reads (`clientWidth`, the observer's `contentRect`), which do not move when an\n// ancestor's CSS transform does — so folding a live fit in here would make every backing store\n// churn on a resize that changed no layout at all. The host's attribute is the transform-aware\n// term, and it is stamped at rest for that reason.\nfunction syncCanvasSize(\n binding: NodeBinding,\n dpr: number,\n contentRect?: { width: number; height: number },\n stats?: WebglShaderRuntimeStats,\n // Longest-edge ceiling for the resulting backing store, passed ONLY on the pinned static path\n // (see `staticShaderPixelRatio`). Undefined ⇒ unbounded, which is what the live path always was.\n maxDim?: number,\n): void {\n // Box (content-box) size, in priority order that AVOIDS a forced reflow after creation:\n // 1. the ResizeObserver-provided contentRect (already measured off the main path — no reflow), else\n // 2. the box we last measured (a WINDOW-only change or a renderScale step doesn't move the box, so its\n // size is still valid — reusing it is what keeps those paths reflow-free), else\n // 3. a single clientWidth/Height layout read — only when nothing has measured this binding yet, which\n // the runtime pays ONCE per create, in a batched measure pass (see `drainFirstSizes`).\n // Shader self-layers are full-bleed with no padding, so content-box width == clientWidth. The chosen size is\n // cached so the next window/renderScale resize can reuse it; the observer refreshes it on any real box change.\n let boxW: number;\n let boxH: number;\n if (contentRect) {\n boxW = contentRect.width;\n boxH = contentRect.height;\n } else if (binding.boxMeasured) {\n boxW = binding.boxW;\n boxH = binding.boxH;\n } else {\n boxW = binding.selfLayer.clientWidth;\n boxH = binding.selfLayer.clientHeight;\n }\n binding.boxW = boxW;\n binding.boxH = boxH;\n // Whatever tier answered, this binding now HAS a box (see `NodeBinding.boxMeasured`) — including a 0x0 one,\n // which is a real answer about a hidden/rect-less node and not a reason to re-read the layout forever.\n binding.boxMeasured = true;\n // The canvas covers only the window SUB-RECT of the self-layer (full node), so its backing store is the\n // box size scaled by the window's du/dv — a clamped background renders far fewer pixels.\n const cssW = boxW * binding.window[2];\n const cssH = boxH * binding.window[3];\n // (The applied `ratio` this also returns is only meaningful to a caller that draws geometry in CSS\n // px × ratio — the particle runtime. A shader fills its canvas from a clip-space quad, so only the\n // aspect matters here, and the clamp preserves it.)\n const { w, h } = backingStoreSize(\n cssW,\n cssH,\n dpr * binding.pixelRatioScale,\n maxDim,\n );\n const wChanged = binding.canvas.width !== w;\n const hChanged = binding.canvas.height !== h;\n if (wChanged) binding.canvas.width = w;\n if (hChanged) binding.canvas.height = h;\n // Re-assigning width/height REALLOCATES (and clears) the backing store — count one realloc EVENT\n // per call that actually re-assigned; a same-size sync costs (and counts) nothing.\n if (wChanged || hChanged) {\n // Canvas dimensions are part of the frame's pixel domain. The assignment also clears the\n // backing store, so a memo hit here would be wrong even if a later writer restored the old\n // dimensions before the next render.\n invalidateStaticFrameKey(binding);\n if (stats) stats.canvasReallocs++;\n }\n}\n\n/** TEST ONLY: drive `syncCanvasSize` (box caching → no post-create reflow) without a WebGL2 context, which\n * jsdom lacks (the real runtime is a no-op there, so the binding path is otherwise untestable). */\nexport function syncCanvasSizeForTest(\n binding: NodeBinding,\n dpr: number,\n contentRect?: { width: number; height: number },\n): void {\n syncCanvasSize(binding, dpr, contentRect);\n}\n\n/** Does this program need the node's own self-layer rect? (SCREEN_UV's node÷root mapping and the\n * SCREEN_TEXTURE composite's overlap test both do.) */\nfunction needsLayerRect(program: CompiledProgram): boolean {\n return program.usesScreenUv || program.usesScreenTexture;\n}\n\n/** Does this program need ANY viewport rect (adds SCREEN_PIXEL_SIZE, which needs only the root)? */\nfunction readsScreenRect(program: CompiledProgram): boolean {\n return needsLayerRect(program) || program.usesScreenPixelSize;\n}\n\n// ---- SCREEN_TEXTURE capture --------------------------------------------------\n//\n// APPROXIMATION SEMANTICS: Godot's SCREEN_TEXTURE is the real framebuffer of everything\n// drawn before the current item. The browser can't read the composited DOM back, so we\n// approximate it: walk the scene's self-layer elements in DOM (draw) order UP TO this\n// node and `drawImage` the ones that (a) spatially overlap the node's on-screen rect and\n// (b) have a drawable source — a runtime <canvas> (an earlier shader/particle node) or an\n// already-loaded texture image. Text, gradients, borders, CSS filters/blends and\n// background-size fitting are all skipped/simplified (each source is drawn stretched to\n// its element rect) — good enough for \"distort what's behind me\" effects. The composite\n// is in VIEWPORT coordinates (the whole root rect, downscaled to `maxScreenCaptureDim`),\n// so the shader samples it directly with SCREEN_UV. Refreshes are throttled\n// (`SCREEN_CAPTURE_MIN_INTERVAL_S`) and forced on a viewport resize — never per rAF.\n\n// Decoded capture image sources, keyed by url. The GL textureCache holds only uploaded\n// GL textures (which drawImage can't read), so the capture path keeps its own\n// HTMLImageElement cache; a not-yet-loaded image is skipped until a later refresh.\nconst captureImageCache = new Map<string, HTMLImageElement>();\n\nfunction getCaptureImage(url: string): HTMLImageElement | null {\n const cached = captureImageCache.get(url);\n if (cached) return cached;\n if (typeof Image === \"undefined\") return null;\n const image = new Image();\n image.crossOrigin = \"anonymous\";\n image.src = url;\n captureImageCache.set(url, image);\n return image;\n}\n\n// A self-layer's drawImage-able paint source, or null (text/plain-color/none — skipped).\nfunction drawableSource(layer: HTMLElement): CanvasImageSource | null {\n // A runtime canvas (an earlier WebGL shader/particle node) already holds the layer's\n // final pixels — blit it directly.\n for (const child of layer.children) {\n if (!(child instanceof HTMLCanvasElement)) continue;\n // …EXCEPT a WebGPU-backed one. `drawImage` from a WebGPU canvas is never a source here: it comes\n // back BLANK on SwiftShader and is pathologically slow on Android Chrome (measured — S7 in\n // docs/perf-harness.md, where the blit-shaped WebGPU arm collapsed 87 → 23 Hz). Skipping it\n // falls through to the layer's texture image below: a worse approximation of an already\n // approximate composite, which beats a blank hole and a stalled GPU process. Only a WebGPU\n // backend ever stamps this attribute; the GL backend does not.\n if (child.getAttribute(\"data-godot-effects-backend\") === \"webgpu\") continue;\n return child;\n }\n const url =\n layer.getAttribute(\"data-godot-shader-texture-url\") ||\n backgroundImageUrl(layer);\n if (!url) return null;\n const image = getCaptureImage(url);\n return image?.complete && image.naturalWidth > 0 ? image : null;\n}\n\n// Composite the content drawn BEFORE this node (see the approximation note above) onto\n// the capture canvas, in root-viewport coordinates scaled to the canvas size.\nfunction compositeScreenContent(\n binding: NodeBinding,\n rootRect: ViewportRect,\n state: ScreenCaptureState,\n): void {\n const { ctx, canvas } = state;\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n const sx = canvas.width / Math.max(1, rootRect.width);\n const sy = canvas.height / Math.max(1, rootRect.height);\n // The node's own rect comes from the cache (the per-layer rects below are unavoidable, but this\n // whole composite is throttled to SCREEN_CAPTURE_MIN_INTERVAL_S — never per rAF).\n const nodeRect = layerRect(binding);\n const nodeRight = nodeRect.left + nodeRect.width;\n const nodeBottom = nodeRect.top + nodeRect.height;\n const layers = binding.root.querySelectorAll<HTMLElement>(\n `.${SELF_LAYER_CLASS}`,\n );\n for (const layer of layers) {\n // Draw order stops at the node's own subtree: SCREEN_TEXTURE sees only content\n // painted BEFORE it (and never feeds the node's own output back into itself).\n if (layer === binding.selfLayer || binding.node.contains(layer)) break;\n const rect = layer.getBoundingClientRect();\n if (rect.width <= 0 || rect.height <= 0) continue; // hidden/empty\n if (\n rect.right <= nodeRect.left ||\n rect.left >= nodeRight ||\n rect.bottom <= nodeRect.top ||\n rect.top >= nodeBottom\n ) {\n continue; // no spatial overlap with the node's screen rect\n }\n const source = drawableSource(layer);\n if (!source) continue;\n try {\n ctx.drawImage(\n source,\n (rect.left - rootRect.left) * sx,\n (rect.top - rootRect.top) * sy,\n rect.width * sx,\n rect.height * sy,\n );\n } catch {\n // A detached/zero-sized source must not kill the node's render.\n }\n }\n}\n\n// Ensure + (throttled) refresh the binding's screen capture, returning it for the draw.\n// A viewport resize forces an immediate recapture; otherwise a refresh happens at most\n// every SCREEN_CAPTURE_MIN_INTERVAL_S (the throttle is wall-clock, independent of the\n// shader TIME, so frozen-TIME mode still captures once).\nfunction captureScreenTexture(\n sharedGl: SharedGl,\n binding: NodeBinding,\n rootRect: ViewportRect,\n maxDim: number,\n): ScreenCaptureState | null {\n if (typeof document === \"undefined\") return null;\n const { gl } = sharedGl;\n const rw = Math.max(1, rootRect.width);\n const rh = Math.max(1, rootRect.height);\n const scale = Math.min(1, maxDim / Math.max(rw, rh));\n const cw = Math.max(1, Math.round(rw * scale));\n const ch = Math.max(1, Math.round(rh * scale));\n let state = binding.screenCapture;\n if (!state) {\n const canvas = document.createElement(\"canvas\");\n canvas.width = cw;\n canvas.height = ch;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return null;\n const texture = createWebglTexture(gl);\n if (!texture) return null;\n state = {\n canvas,\n ctx,\n texture,\n capturedAt: Number.NEGATIVE_INFINITY,\n width: cw,\n height: ch,\n };\n binding.screenCapture = state;\n }\n const resized = state.width !== cw || state.height !== ch;\n if (\n !resized &&\n nowSeconds() - state.capturedAt < SCREEN_CAPTURE_MIN_INTERVAL_S\n ) {\n return state; // throttled: the previous composite/texture is reused as-is\n }\n if (resized) {\n state.canvas.width = cw;\n state.canvas.height = ch;\n state.width = cw;\n state.height = ch;\n }\n state.capturedAt = nowSeconds();\n compositeScreenContent(binding, rootRect, state);\n uploadWebglTexture(gl, state.texture, state.canvas);\n return state;\n}\n\n// ---- DOM/attribute readers -------------------------------------------------\n\nfunction backgroundImageUrl(el: HTMLElement): string | null {\n const value = el.style.backgroundImage;\n const match = /url\\((['\"]?)(.*?)\\1\\)/.exec(value);\n return match ? match[2] : null;\n}\n\n// Per-binding memo of the above: `updateBinding` runs for EVERY kept binding on EVERY reconcile,\n// and the paint almost never changes, so the regex over a (possibly long, data-URI) background\n// string was pure repeat work. Keyed on the raw style string, so a hit is exactly what a re-parse\n// would have produced.\nfunction memoizedBackgroundImageUrl(\n binding: NodeBinding,\n el: HTMLElement,\n): string | null {\n const raw = el.style.backgroundImage;\n if (raw === binding.backgroundImageStyle) {\n return binding.backgroundImageParsed;\n }\n binding.backgroundImageStyle = raw;\n binding.backgroundImageParsed = backgroundImageUrl(el);\n return binding.backgroundImageParsed;\n}\n\n// Map a Godot canvas_item render_mode blend to the closest CSS mix-blend-mode. `add` → `plus-lighter` (true\n// additive), `mul` → `multiply`. `mix` (default) plus `sub`/`premul_alpha` (no clean CSS equivalent) → `\"\"`\n// (normal source-over), which also clears any blend a previously-cached shader left on a reused node element.\nexport function blendToMixBlendMode(blend: GodotBlendMode): string {\n switch (blend) {\n case \"add\":\n return \"plus-lighter\";\n case \"mul\":\n return \"multiply\";\n default:\n return \"\";\n }\n}\n\nfunction backgroundFit(el: HTMLElement): \"contain\" | \"cover\" | \"fill\" {\n const size = el.style.backgroundSize;\n if (size === \"contain\") return \"contain\";\n if (size === \"cover\") return \"cover\";\n return \"fill\";\n}\n\nexport type ShaderParamValue = number | number[];\n\nfunction parseParams(value: string | null): Record<string, ShaderParamValue> {\n if (!value) return {};\n try {\n const parsed = JSON.parse(value);\n return typeof parsed === \"object\" && parsed ? parsed : {};\n } catch {\n return {};\n }\n}\n\nfunction parseParamKinds(value: string | null): Record<string, string> {\n if (!value) return {};\n try {\n const parsed = JSON.parse(value);\n if (!parsed || typeof parsed !== \"object\") return {};\n const out: Record<string, string> = {};\n for (const [key, raw] of Object.entries(parsed)) {\n if (typeof raw === \"string\" && raw !== \"\") out[key] = raw;\n }\n return out;\n } catch {\n return {};\n }\n}\n\nfunction parseModulate(value: string | null): [number, number, number, number] {\n if (!value) return [1, 1, 1, 1];\n const parts = value.split(\",\").map((p) => Number.parseFloat(p.trim()));\n return [parts[0] ?? 1, parts[1] ?? 1, parts[2] ?? 1, parts[3] ?? 1];\n}\n\nconst FULL_WINDOW: [number, number, number, number] = [0, 0, 1, 1];\n\n// Parse the `data-godot-shader-uv-window` attr (\"u0,v0,du,dv\", node-local top-left fractions) into a clamped\n// sub-rect, defaulting to the full node on absence/garbage.\nfunction parseWindow(value: string | null): [number, number, number, number] {\n if (!value) return [...FULL_WINDOW];\n const p = value.split(\",\").map((s) => Number.parseFloat(s.trim()));\n if (p.length !== 4 || p.some((n) => !Number.isFinite(n)))\n return [...FULL_WINDOW];\n const u0 = Math.min(Math.max(p[0], 0), 1);\n const v0 = Math.min(Math.max(p[1], 0), 1);\n const du = Math.min(Math.max(p[2], 0), 1 - u0);\n const dv = Math.min(Math.max(p[3], 0), 1 - v0);\n return [u0, v0, du > 0 ? du : 1, dv > 0 ? dv : 1];\n}\n\nfunction isFullWindow(w: readonly number[]): boolean {\n return w[0] <= 0.0005 && w[1] <= 0.0005 && w[2] >= 0.9995 && w[3] >= 0.9995;\n}\n\n// Position the per-node canvas over the window sub-rect within the (always full-size) self-layer. Full window\n// ⇒ full-bleed (the common case); a sub-rect ⇒ a smaller canvas so its (blend-mode) main-thread paint covers\n// only the visible region, not the off-screen overflow.\nfunction placeCanvasWindow(\n canvas: HTMLCanvasElement,\n w: readonly number[],\n): void {\n if (isFullWindow(w)) {\n Object.assign(canvas.style, {\n left: \"0\",\n top: \"0\",\n width: \"100%\",\n height: \"100%\",\n });\n } else {\n Object.assign(canvas.style, {\n left: `${w[0] * 100}%`,\n top: `${w[1] * 100}%`,\n width: `${w[2] * 100}%`,\n height: `${w[3] * 100}%`,\n });\n }\n}\n","import {\n createParticleRuntime,\n type ParticleRuntime,\n} from \"./particles/runtime\";\nimport {\n type GodotHtmlRuntimeOptions,\n RUNTIME_OPTION_KEYS,\n} from \"./runtime-options\";\nimport {\n createWebglShaderRuntime,\n type WebglShaderRuntime,\n} from \"./webgl/runtime\";\n\nexport type {\n ParticleProfile,\n ParticleRuntime,\n ParticleRuntimeStats,\n} from \"./particles/runtime\";\nexport { createParticleRuntime } from \"./particles/runtime\";\nexport type {\n GodotHtmlMountOptions,\n GodotHtmlRuntimeOptions,\n} from \"./runtime-options\";\nexport type { GodotEffectRenderInfo } from \"./types\";\nexport type {\n WebglShaderRuntime,\n WebglShaderRuntimeStats,\n WebglWarmSpec,\n} from \"./webgl/runtime\";\nexport { createWebglShaderRuntime } from \"./webgl/runtime\";\n\n/** Per-family overrides let an external host tune one binding without restarting the other. */\nexport interface HtmlEffectsHostOptions extends GodotHtmlRuntimeOptions {\n shaderOptions?: GodotHtmlRuntimeOptions;\n particleOptions?: GodotHtmlRuntimeOptions;\n}\n\n/** One DOM binding owner; the canvas renderer has its own stage/frame lifecycle. */\nexport interface HtmlEffectsHost {\n readonly shaders: WebglShaderRuntime | null;\n readonly particles: ParticleRuntime | null;\n reconcile(): void;\n updateOptions(options: HtmlEffectsHostOptions): void;\n dispose(): void;\n}\n\nexport function createHtmlEffectsHost(\n stage: HTMLElement,\n initialOptions: HtmlEffectsHostOptions = {},\n): HtmlEffectsHost {\n let shaders: WebglShaderRuntime | null = null;\n let particles: ParticleRuntime | null = null;\n let shaderOptions: GodotHtmlRuntimeOptions | null = null;\n let particleOptions: GodotHtmlRuntimeOptions | null = null;\n let disposed = false;\n const release = () => {\n shaders?.dispose();\n particles?.dispose();\n shaders = null;\n particles = null;\n };\n const host: HtmlEffectsHost = {\n get shaders() {\n return shaders;\n },\n get particles() {\n return particles;\n },\n reconcile() {\n if (disposed) return;\n shaders?.reconcile();\n particles?.reconcile();\n },\n updateOptions(next) {\n if (disposed) return;\n const nextShader = { ...next, ...next.shaderOptions };\n const nextParticle = { ...next, ...next.particleOptions };\n const shaderKeys = RUNTIME_OPTION_KEYS.filter(\n (key) =>\n !/^(particle|staticParticle|parkStaticParticle)/.test(key) &&\n key !== \"enableParticles\",\n );\n const particleKeys = RUNTIME_OPTION_KEYS.filter(\n (key) =>\n !/^(shader|staticShader)/.test(key) &&\n ![\n \"enableWebglShaders\",\n \"resolveShaderSource\",\n \"enableScreenTextureCapture\",\n \"maxScreenCaptureDim\",\n ].includes(key),\n );\n if (\n !shaderOptions ||\n shaderKeys.some((key) => shaderOptions?.[key] !== nextShader[key])\n ) {\n shaders?.dispose();\n shaders = null;\n shaderOptions = nextShader;\n if (!nextShader.externalRuntimes && nextShader.enableWebglShaders)\n shaders = createWebglShaderRuntime(stage, nextShader);\n }\n if (\n !particleOptions ||\n particleKeys.some((key) => particleOptions?.[key] !== nextParticle[key])\n ) {\n particles?.dispose();\n particles = null;\n particleOptions = nextParticle;\n if (!nextParticle.externalRuntimes && nextParticle.enableParticles)\n particles = createParticleRuntime(stage, nextParticle);\n }\n },\n dispose() {\n if (disposed) return;\n disposed = true;\n release();\n },\n };\n host.updateOptions(initialOptions);\n return host;\n}\n\nexport {\n normalizeParticleSpecConfig,\n type ParticleSpecConfig,\n parseParticleSpecConfig,\n} from \"./particles/spec\";\n"],"mappings":"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"}
|