@godot-scene-web/canvas 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.
@@ -0,0 +1,1085 @@
1
+ import { $ as DamageRect, A as DrawListPatchView, B as ScreenEffectDrawContext, C as DRAW_TEXTURED_MESH, D as DrawListFragment, E as DrawList, F as GlyphsView, G as createDrawListPatchView, H as createClipRectView, I as NinePatchView, J as createPolylineView, K as createGlyphsView, L as PolylineView, M as ExternalEffectDrawContext, N as FLIP_H, O as DrawListFragmentPatch, P as FLIP_V, Q as CommandBounds, R as QuadView, S as DRAW_SCREEN_EFFECT, T as DrawCommandName, U as createDrawList, V as TexturedMeshView, W as createDrawListFragment, X as createTexturedMeshView, Y as createQuadView, Z as setViewColorMatrix, _ as DRAW_EXTERNAL_EFFECT, a as StageCanvas, at as RETAINED_MAX_DAMAGE_COVERAGE, b as DRAW_POLYLINE, c as BLEND_ADD, ct as createDamageTiles, d as BLEND_SUB, dt as outsetDamageRect, et as DamageTile, f as BlendMode, ft as transformDamageRect, g as DRAW_COMMAND_NAMES, h as DRAW_CLIP_PUSH, i as STAGE_CONTEXT_ATTRIBUTES, it as RETAINED_DAMAGE_TILE_SIZE, j as ExternalEffectDrawCommand, k as DrawListOptions, l as BLEND_MIX, lt as damageIntersects, m as DRAW_CLIP_POP, n as CanvasStage, nt as DamageTransform, o as StageProjection, ot as commandDamageBounds, p as ClipRectView, pt as unionDamageRect, q as createNinePatchView, r as CanvasStageOptions, rt as DirtyRect, s as createCanvasStage, st as createDamageRect, t as GlyphPass, tt as DamageTiles, u as BLEND_MUL, ut as isDamageEmpty, v as DRAW_GLYPHS, w as DrawCommandKind, x as DRAW_QUAD, y as DRAW_NINE_PATCH, z as ScreenEffectDrawCommand } from "./glyph-pass-DfQlp_IH.js";
2
+ import { HeadlessGodotParticleDirectPass, HeadlessGodotParticleDirectRenderInput, HeadlessShaderProducer, HeadlessShaderRenderInput } from "@godot-scene-web/canvas-effects/webgl";
3
+
4
+ //#region src/batcher.d.ts
5
+ /**
6
+ * The quad batcher: everything the executor knows about MERGING draws, with no
7
+ * GL in it, so the flush decisions — the thing that decides whether a frame is 20
8
+ * draw calls or 200 — are unit-testable rather than inferred from a profiler.
9
+ *
10
+ * WHY MULTI-TEXTURE BATCHING IS THE WHOLE POINT. Probing recorded scenes for
11
+ * batch runs (a run = a maximal span of consecutive painting nodes sharing
12
+ * texture + blend + clip + colour-matrix state) found combat at 152 runs over 183
13
+ * painting nodes — 1.2 nodes per batch, i.e. essentially no batching — and 151 of
14
+ * those 152 breaks were the TEXTURE alone. The map is the same story: 174 runs
15
+ * over 825 nodes, 173 texture breaks. But the number of DISTINCT textures per
16
+ * screen is only 24-65. So a batch that can hold many textures at once collapses
17
+ * the run count towards the number of times the state that a batch CANNOT hold
18
+ * changes — blend (0-16 per screen) and clip (0-26) — plus one flush per
19
+ * texture-table refill. That is the difference between "tens of draws" and "one
20
+ * draw per node", and it is why the slot table below is not an optimization to
21
+ * add later.
22
+ *
23
+ * HOW A TEXTURE STOPS BREAKING A BATCH. Each batch binds up to
24
+ * `maxTextureSlots` textures to consecutive texture units and each quad carries
25
+ * the INDEX of the one it samples. The fragment shader turns that index back into
26
+ * a sampler with a compiled `if` ladder (GLSL ES 3.00 forbids indexing a sampler
27
+ * array with anything but a constant). A quad whose texture is not in the table
28
+ * takes the next free slot; when the table is full the batch flushes and starts a
29
+ * new table.
30
+ *
31
+ * COLOUR MATRICES GET THE SAME TREATMENT, for the same reason at a smaller scale:
32
+ * the card screens carry 30-48 HSV-transformed quads, and one draw call each
33
+ * would undo the texture win. A batch holds up to `maxColorMatrices` matrices in
34
+ * a uniform array with the IDENTITY pinned at slot 0, so "no matrix" costs
35
+ * nothing and needs no separate program. Identical matrices share a slot (the
36
+ * table is deduped by value), which matters because those 30-48 quads are usually
37
+ * a handful of distinct tints applied to many cards.
38
+ *
39
+ * ORDER IS NEVER REORDERED. Instances are drawn in the order they were pushed —
40
+ * `drawArraysInstanced` rasterizes instance N before instance N+1, which is the
41
+ * property a painter's-algorithm 2D renderer with no depth buffer depends on. So
42
+ * this batcher only ever MERGES CONSECUTIVE commands; it never sorts, and it can
43
+ * therefore be dropped in front of any draw list without changing what the frame
44
+ * looks like. Bucketing non-adjacent commands by texture is a separate,
45
+ * order-unsafe transformation that belongs to whoever BUILDS the list and knows
46
+ * which spans are safe to permute.
47
+ *
48
+ * FOUR EXPLICIT CORNERS, NOT AN AFFINE BASIS. An instance carries `p0..p3`
49
+ * outright (8 floats) rather than a 2x3 transform (6). The two extra floats buy
50
+ * arbitrary quadrilaterals, which is what lets `./polyline`'s stroke segments AND
51
+ * its join wedges (a triangle spelled as a quad with two coincident corners) ride
52
+ * in the same buffer, the same shader and the same batch as every sprite. The
53
+ * alternative — a second program and a mid-frame break for every polyline — costs
54
+ * far more than 8 bytes per quad. The consequence to know about: UV interpolates
55
+ * affinely per triangle, so a NON-parallelogram textured quad would show a seam
56
+ * along the split diagonal. Nothing produces one (sprites and nine-patch bands
57
+ * are affine images of a rect; the non-affine quads are untextured stroke
58
+ * geometry), and if something ever does, the fix is to split it rather than to
59
+ * make every quad pay for projective interpolation.
60
+ */
61
+ /** Floats per instance. See the field offsets below for the layout. */
62
+ declare const INSTANCE_FLOATS = 18;
63
+ /** Offset of `p0.x` — four `(x, y)` corners, in the unit-square order
64
+ * `(0,0)`, `(1,0)`, `(1,1)`, `(0,1)`. Design space. */
65
+ declare const INSTANCE_CORNERS_OFFSET = 0;
66
+ /** Offset of the normalized source rect `(u0, v0, uSpan, vSpan)`. A span is
67
+ * NEGATIVE for a flipped axis, which is how `FLIP_H`/`FLIP_V` are carried. */
68
+ declare const INSTANCE_UV_OFFSET = 8;
69
+ /** Offset of the PREMULTIPLIED tint, `(r, g, b, a)`. */
70
+ declare const INSTANCE_COLOR_OFFSET = 12;
71
+ /** Offset of `(textureSlot, colorMatrixSlot)`. Matrix slot 0 is the identity. */
72
+ declare const INSTANCE_SLOTS_OFFSET = 16;
73
+ /** Floats per colour-matrix slot. */
74
+ declare const COLOR_MATRIX_FLOATS = 9;
75
+ /** The most texture units this batcher will ever ask for, whatever the GPU
76
+ * reports. Sixteen is the WebGL2 (GLES 3.0) guaranteed minimum for
77
+ * `MAX_TEXTURE_IMAGE_UNITS`, so asking for more buys a shader that some
78
+ * conformant device cannot link, in exchange for a batch boundary the measured
79
+ * key counts (24-65 distinct textures per screen) would still hit. */
80
+ declare const MAX_TEXTURE_SLOTS = 16;
81
+ /** Default colour-matrix table size, including the identity at slot 0. */
82
+ declare const DEFAULT_COLOR_MATRIX_SLOTS = 16;
83
+ /** What a batch binds to a texture unit. Structural on purpose: the batcher only
84
+ * needs the handle's IDENTITY to slot it, and a test can hand it a stand-in. */
85
+ interface BatchTexture {
86
+ readonly texture: WebGLTexture;
87
+ }
88
+ /**
89
+ * The staging view a caller fills before {@link QuadBatcher.push}. Owned by the
90
+ * batcher and reused, so a frame's worth of quads allocates nothing.
91
+ */
92
+ interface QuadInstance {
93
+ /** Corner at unit-square `(0, 0)`, design space. */
94
+ x0: number;
95
+ y0: number;
96
+ /** Corner at `(1, 0)`. */
97
+ x1: number;
98
+ y1: number;
99
+ /** Corner at `(1, 1)`. */
100
+ x2: number;
101
+ y2: number;
102
+ /** Corner at `(0, 1)`. */
103
+ x3: number;
104
+ y3: number;
105
+ /** Normalized source origin (the texel under corner `(0, 0)`). */
106
+ u0: number;
107
+ v0: number;
108
+ /** Normalized source span; negative mirrors the axis. */
109
+ uSpan: number;
110
+ vSpan: number;
111
+ /** PREMULTIPLIED tint. */
112
+ r: number;
113
+ g: number;
114
+ b: number;
115
+ a: number;
116
+ }
117
+ declare function createQuadInstance(): QuadInstance;
118
+ type BatchFlushReason = /** The texture table was full and the next quad wanted a texture not in it. */"textureSlots" /** The colour-matrix table was full and the next quad wanted a new matrix. */ | "colorMatrices" /** The blend mode changed; GL blend state is per-draw, not per-instance. */ | "blend" /** The clip scope changed; the scissor box is per-draw too. */ | "clip"
119
+ /**
120
+ * A glyph run interrupted the quads.
121
+ *
122
+ * Not a state change this batcher could hold: the glyph pass replaces the program and the vertex
123
+ * array outright, so the pending batch has to be DRAWN under the executor's state before the
124
+ * pass runs. Counted separately from `blend` and `clip` because it is the one break a consumer
125
+ * can remove by moving text, and a frame whose batching regressed should say so by axis.
126
+ */
127
+ | "glyphs" /** A screen-dependent effect interrupted the instanced-quad program. */ | "effects" /** An indexed mesh interrupted the instanced-quad program. */ | "meshes" /** A retained compiled GPU run takes over after direct quad staging. */ | "compiled" /** End of the draw list (or an explicit flush by the caller). */ | "end";
128
+ /**
129
+ * One batch, handed to the sink. Every array is the batcher's own live storage —
130
+ * valid only for the duration of the call, and only over the stated prefix.
131
+ */
132
+ interface Batch {
133
+ /** Instance data: `quadCount * INSTANCE_FLOATS` floats from index 0. */
134
+ readonly instances: Float32Array;
135
+ readonly quadCount: number;
136
+ /** Texture table. Entries `0 .. textureCount - 1` are the units to bind;
137
+ * everything past that is a stale `null` slot and must not be read. */
138
+ readonly textures: readonly (BatchTexture | null)[];
139
+ readonly textureCount: number;
140
+ /** `colorMatrixCount * COLOR_MATRIX_FLOATS` floats; slot 0 is the identity. */
141
+ readonly colorMatrices: Float32Array;
142
+ readonly colorMatrixCount: number;
143
+ readonly blend: BlendMode;
144
+ /** The clip scope this batch was accumulated under (see `./clip-stack`). */
145
+ readonly clipEpoch: number;
146
+ readonly reason: BatchFlushReason;
147
+ }
148
+ interface BatcherStats {
149
+ /** Batches emitted, i.e. draw calls the executor issues for quads. */
150
+ batches: number;
151
+ /** Instances pushed. */
152
+ quads: number;
153
+ /** Texture-unit binds — `textureCount` summed over batches. */
154
+ textureBinds: number;
155
+ /** Largest single batch, in instances. */
156
+ maxBatchQuads: number;
157
+ /** Times the instance arena had to grow (should settle at zero). */
158
+ arenaGrowths: number;
159
+ flushes: Record<BatchFlushReason, number>;
160
+ }
161
+ interface QuadBatcherOptions {
162
+ /** Where a finished batch goes. Called synchronously from `push`/`flush`. */
163
+ draw(batch: Batch): void;
164
+ /** Texture units per batch; clamped to `[1, MAX_TEXTURE_SLOTS]`. */
165
+ maxTextureSlots?: number;
166
+ /** Colour-matrix slots per batch INCLUDING the identity; at least 1. */
167
+ maxColorMatrices?: number;
168
+ /** Initial instance-arena capacity, in quads. */
169
+ quadCapacity?: number;
170
+ }
171
+ interface QuadBatcher {
172
+ readonly maxTextureSlots: number;
173
+ readonly maxColorMatrices: number;
174
+ /** Instances accumulated in the OPEN batch. */
175
+ readonly quadCount: number;
176
+ /** Texture slots taken in the open batch. */
177
+ readonly textureCount: number;
178
+ /** Colour-matrix slots taken in the open batch, including the identity. */
179
+ readonly colorMatrixCount: number;
180
+ readonly blend: BlendMode;
181
+ readonly stats: BatcherStats;
182
+ /** The reusable staging instance; fill it, then call {@link QuadBatcher.push}. */
183
+ readonly quad: QuadInstance;
184
+ /** Start a frame: drop any open batch WITHOUT drawing it, and zero the stats. */
185
+ reset(): void;
186
+ /** Flush first if the mode differs, then adopt it. */
187
+ setBlend(blend: BlendMode): void;
188
+ /** Flush first if the scope differs, then adopt it. */
189
+ setClipEpoch(epoch: number): void;
190
+ /**
191
+ * Commit {@link QuadBatcher.quad}. `colorMatrix` is 9 row-major floats at
192
+ * `colorMatrixOffset`, or `null` for the identity (which costs no slot).
193
+ */
194
+ push(texture: BatchTexture, colorMatrix?: ArrayLike<number> | null, colorMatrixOffset?: number): void;
195
+ /** Emit the open batch, if it has anything in it. */
196
+ flush(reason?: BatchFlushReason): void;
197
+ }
198
+ declare function createQuadBatcher(options: QuadBatcherOptions): QuadBatcher;
199
+ //#endregion
200
+ //#region src/clip-stack.d.ts
201
+ /**
202
+ * The executor's clip scope: a stack of design-space rects, resolved to a GL
203
+ * scissor box and (for the rare rounded clip) to a pair of fragment uniforms.
204
+ *
205
+ * WHY DESIGN SPACE, INTERSECTED BEFORE INTEGERIZING. Each level's rect is
206
+ * intersected with its parent as FLOATS, in the scene's own coordinates, and only
207
+ * the final result is mapped to framebuffer pixels and snapped. Integerizing at
208
+ * every level instead would round the same edge repeatedly, and rounding OUTWARDS
209
+ * (which is what a clip must do — see below) compounds: three nested clips on the
210
+ * same edge would leak up to three pixels.
211
+ *
212
+ * WHY `floor(min)` / `ceil(max)`. A scissor box is whole pixels; the clip it
213
+ * approximates is not. Rounding outwards keeps every pixel the clip PARTIALLY
214
+ * covers, so content is never sheared off by a sub-pixel; the cost is that up to
215
+ * one pixel of overdraw survives on each edge. The other choice (round inwards)
216
+ * eats a visible line off the edge of every scrolling list, which is the failure
217
+ * a reader will actually notice.
218
+ *
219
+ * THE FLIPPED SCISSOR ORIGIN. `gl.scissor` measures Y from the BOTTOM of the
220
+ * drawing buffer; the design space here — like every 2D scene — measures it from
221
+ * the top. So the box's Y is `framebufferHeight - bottomEdge`, not `topEdge`, and
222
+ * getting it wrong produces a clip that is correct in size, correct in X, and
223
+ * mirrored about the middle of the screen — which looks like a layout bug rather
224
+ * than a scissor bug. `clip-stack.test.ts` pins the algebra and the pixel test
225
+ * `nested clips` pins it on a real GPU.
226
+ *
227
+ * ROUNDED CORNERS ARE FRAGMENT WORK, and only for the INNERMOST rounded clip. A
228
+ * scissor cannot express a radius, and a stencil pass per rounded scope would
229
+ * cost more than the feature is worth at the measured population (rounded clips
230
+ * are a handful per screen, nested rounded clips none). So the stack tracks the
231
+ * deepest rounded rect currently open and hands it to the shader as a rounded-rect
232
+ * distance test; every ancestor still clips squarely through the scissor, which is
233
+ * exact for all of them except an outer rounded one's four corners.
234
+ *
235
+ * NON-AXIS-ALIGNED CLIPS FALL BACK TO THEIR AABB. A scissor box is axis-aligned,
236
+ * so a clip can only be exact while the design->framebuffer transform is a scale
237
+ * and a translate. It always is today (see `./present`), and the measured
238
+ * population of rotated clips across the recorded scenes is zero — so rather than
239
+ * carry a stencil path for a case that does not occur, a transform with rotation
240
+ * or skew clips to the transformed rect's bounding box and increments
241
+ * {@link ClipStack.rotatedFallbacks}. A caller that ever sees that counter move
242
+ * has found the case that justifies the stencil.
243
+ */
244
+ /** A resolved clip rect in DESIGN space. */
245
+ interface ClipBounds {
246
+ minX: number;
247
+ minY: number;
248
+ maxX: number;
249
+ maxY: number;
250
+ }
251
+ /** The innermost rounded clip, in DESIGN space; `radius <= 0` means none is open. */
252
+ interface RoundedClip {
253
+ centerX: number;
254
+ centerY: number;
255
+ halfWidth: number;
256
+ halfHeight: number;
257
+ radius: number;
258
+ }
259
+ /** A GL scissor box: framebuffer pixels, origin BOTTOM-LEFT. */
260
+ interface ScissorBox {
261
+ x: number;
262
+ y: number;
263
+ width: number;
264
+ height: number;
265
+ }
266
+ /**
267
+ * A design->framebuffer-pixel affine, in the draw-list's `Transform2D` order
268
+ * `[xx, xy, yx, yy, originX, originY]`: `px = xx*x + yx*y + ox`, `py = xy*x +
269
+ * yy*y + oy`, with `py` measured DOWN from the top of the buffer.
270
+ */
271
+ type PixelTransform = ArrayLike<number>;
272
+ interface ClipStack {
273
+ /** Open clip scopes. */
274
+ readonly depth: number;
275
+ /** Bumped by every push/pop/reset: the batcher's cheap "did the scope change". */
276
+ readonly epoch: number;
277
+ /** Clips that could not be expressed as an axis-aligned scissor (see the module note). */
278
+ readonly rotatedFallbacks: number;
279
+ /** The intersected clip in design space, or `null` when nothing is clipped. */
280
+ bounds(): ClipBounds | null;
281
+ /** The innermost rounded clip, or `null`. */
282
+ rounded(): RoundedClip | null;
283
+ push(clip: ClipRectView): void;
284
+ pop(): void;
285
+ /** Start a frame: drop every scope (a list may end unbalanced) AND the frame's
286
+ * {@link ClipStack.rotatedFallbacks} count, which is per-frame like every
287
+ * other executor statistic. */
288
+ reset(): void;
289
+ /**
290
+ * The current clip as a scissor box against a `width`x`height` framebuffer,
291
+ * written into `out`. With nothing clipped this is the whole framebuffer.
292
+ */
293
+ scissor(transform: PixelTransform, width: number, height: number, out: ScissorBox): ScissorBox;
294
+ }
295
+ declare function createScissorBox(): ScissorBox;
296
+ /** True when `transform` is a pure scale + translate, i.e. a scissor can be exact. */
297
+ declare function isAxisAligned(transform: PixelTransform): boolean;
298
+ declare function createClipStack(): ClipStack;
299
+ //#endregion
300
+ //#region src/color.d.ts
301
+ /**
302
+ * Premultiplied colour composition — the arithmetic the batcher writes into an
303
+ * instance and the fragment shader repeats on the GPU, in one place so the two
304
+ * can be checked against each other (and against `html`'s CPU tint bake).
305
+ *
306
+ * TWO CONVENTIONS MEET HERE, and mixing them up is the classic renderer bug:
307
+ *
308
+ * - a **premultiplied** colour carries `(r·a, g·a, b·a, a)`. That is what the
309
+ * draw-list's tints are, what every texture in {@link ./textures} is uploaded
310
+ * as, what the fragment emits, and what the canvas is declared as. Composing
311
+ * two premultiplied colours is a plain componentwise multiply.
312
+ * - a **straight** colour carries `(r, g, b, a)` with the channels independent.
313
+ * The only thing that wants straight colour is the 3x3 colour matrix, because
314
+ * the matrix is defined on the texture's own RGB — multiply a premultiplied
315
+ * colour by it and a 50%-alpha pixel is transformed as if it were half as
316
+ * bright.
317
+ *
318
+ * Everything below is sRGB-domain: no linearization anywhere. That is not a
319
+ * shortcut, it is the contract — the matrices come from Godot HSV materials that
320
+ * Godot itself applies to sRGB texture bytes, and `html`'s
321
+ * `applyColorMatrixToPixels` (the CPU bake of the same transform, used by the DOM
322
+ * renderer) applies them to sRGB bytes too. Linearizing here would make the GPU
323
+ * path disagree with the DOM path it is meant to replace.
324
+ */
325
+ /** Row-major 3x3 identity, the value slot 0 of a batch's matrix table holds. */
326
+ declare const IDENTITY_COLOR_MATRIX: readonly number[];
327
+ /** A straight or premultiplied RGBA colour, 0..1 per channel. */
328
+ interface Rgba {
329
+ r: number;
330
+ g: number;
331
+ b: number;
332
+ a: number;
333
+ }
334
+ declare function createRgba(): Rgba;
335
+ declare function clamp01(value: number): number;
336
+ /**
337
+ * Straight `(r,g,b,a)` -> premultiplied, clamped. The shape a producer has (a
338
+ * Godot `modulate` is straight) turned into the shape {@link QuadView} wants.
339
+ */
340
+ declare function premultiply(r: number, g: number, b: number, a: number, out: Rgba): Rgba;
341
+ /**
342
+ * Premultiplied -> straight, with the `a === 0` hole filled with black. The
343
+ * inverse of {@link premultiply} up to that hole (a fully transparent
344
+ * premultiplied pixel has forgotten its colour, so nothing can recover it).
345
+ */
346
+ declare function unpremultiply(colour: Rgba, out: Rgba): Rgba;
347
+ /**
348
+ * Compose two PREMULTIPLIED colours — a texel times its quad's tint. A plain
349
+ * componentwise multiply, which is the whole reason the premultiplied form is
350
+ * worth keeping: with straight colours this would need the alpha handled apart
351
+ * from the channels it has already scaled.
352
+ */
353
+ declare function modulatePremultiplied(source: Rgba, tint: Rgba, out: Rgba): Rgba;
354
+ /**
355
+ * Apply a row-major 3x3 to STRAIGHT sRGB channels in 0..1, clamped — the float
356
+ * twin of `html`'s `applyColorMatrixToPixels` (which works in 0..255 bytes and
357
+ * clamps because it writes a `Uint8ClampedArray`) and of the colour-matrix branch
358
+ * in the executor's fragment shader. All three must agree; `colour.test.ts`
359
+ * asserts the first two against each other pixel for pixel.
360
+ *
361
+ * Alpha is untouched, exactly as `feColorMatrix` with only the RGB rows set.
362
+ */
363
+ declare function applyColorMatrix01(colour: Rgba, matrix: ArrayLike<number>, offset: number, out: Rgba): Rgba;
364
+ /**
365
+ * The full per-fragment colour law, on the CPU: a PREMULTIPLIED texel, its
366
+ * optional colour matrix, and the quad's PREMULTIPLIED tint, in the order the
367
+ * shader applies them (un-premultiply, transform, re-premultiply, modulate).
368
+ *
369
+ * This exists so a pixel test can state the number it expects from the same
370
+ * expression the GPU evaluates rather than from a second, hand-derived one.
371
+ */
372
+ declare function shadeQuadPixel(texel: Rgba, matrix: ArrayLike<number> | null, matrixOffset: number, tint: Rgba, out: Rgba): Rgba;
373
+ /** True when the 9 floats at `offset` are the identity, i.e. a no-op slot. */
374
+ declare function isIdentityColorMatrix(matrix: ArrayLike<number>, offset?: number): boolean;
375
+ /**
376
+ * True when the 9 floats at `a`/`b` are equal — the batcher's matrix-table
377
+ * dedupe. EXACT equality, deliberately: the table's job is to notice that many
378
+ * cards carry the same computed tint, and an epsilon would merge two tints a
379
+ * scene meant to differ. The corollary is that a `Float32Array` and a plain
380
+ * `number[]` holding "the same" value do not match (`0.3` and its f32 round-trip
381
+ * are different numbers), so a caller that wants dedupe should keep one storage
382
+ * width — which the draw-list and this package both do.
383
+ */
384
+ declare function colorMatricesEqual(a: ArrayLike<number>, aOffset: number, b: ArrayLike<number>, bOffset: number): boolean;
385
+ //#endregion
386
+ //#region src/replay.d.ts
387
+ /** A sparse ordered command selection. The executor walks the original list in
388
+ * order, so batching and painter ordering remain exactly the direct path's. */
389
+ interface CommandMask {
390
+ readonly count: number;
391
+ /** A screen-dependent command exists; partial retained replay must decline. */
392
+ readonly requiresFullReplay?: boolean;
393
+ includes(index: number): boolean;
394
+ indices(): readonly number[];
395
+ }
396
+ /** Retained replay must stay below this fraction of the complete painter list. */
397
+ declare const RETAINED_MAX_REPLAY_FRACTION = 0.4;
398
+ /**
399
+ * Largest command count that is strictly below the retained replay threshold.
400
+ * Clip pushes/pops are commands too: omitting them would not restore the
401
+ * original painter state, so they count against the same budget.
402
+ */
403
+ declare function maxPartialReplayCommands(commandCount: number): number;
404
+ declare const REPLAY_MASK_SCRATCH: unique symbol;
405
+ interface ReplaySelectionOptions {
406
+ /** Map design-space command bounds into the damage rectangle's space. */
407
+ transform?: DamageTransform;
408
+ /**
409
+ * Final-damage-coordinate raster/effect reach added after `transform`.
410
+ * Defaults to one final pixel so antialiasing/filtering still selects a
411
+ * command whose geometry falls just beyond a tile edge. Use this for effects
412
+ * whose reach is known in final coordinates; producer-local effects belong
413
+ * in the command's own local bounds, not both places.
414
+ */
415
+ rasterOutset?: number;
416
+ /** Optional cached bounds provider. `null` takes the unknown-bounds policy. */
417
+ boundsAt?: (index: number, out: CommandBounds) => CommandBounds | null;
418
+ /** Decline instead of returning an unboundedly large partial replay. */
419
+ maxCommands?: number;
420
+ /** Unknown visual bounds cannot safely seed a damaged tile. The default is a
421
+ * full/direct fallback; `select` is available for callers that knowingly
422
+ * prefer conservative overdraw. */
423
+ unknownBounds?: "fullReplay" | "select";
424
+ }
425
+ /** Reusable storage for a replay selection. Its mask and `indices()` array keep
426
+ * identity across calls; after high-water growth selecting a tile allocates no
427
+ * arrays. A threshold/screen-effect decline clears the selection. */
428
+ interface ReplayMaskScratch extends CommandMask {
429
+ readonly thresholdExceeded: boolean;
430
+ /** False until `select()` has validated this frame's complete list. */
431
+ readonly selected: boolean;
432
+ /** Internal nominal marker: only a validated selection may drive an FBO replay. */
433
+ readonly [REPLAY_MASK_SCRATCH]: true;
434
+ select<TTexture>(list: DrawList<TTexture>, damage: DamageRect, options?: ReplaySelectionOptions): ReplayMaskScratch;
435
+ }
436
+ /** True only for masks created by this module and safe for a partial FBO replay. */
437
+ declare function isPartialReplayMask(mask: ReplayMaskScratch, list?: DrawList<unknown>): mask is ReplayMaskScratch;
438
+ declare function createReplayMaskScratch(capacity?: number): ReplayMaskScratch;
439
+ /**
440
+ * Select just the commands that can change `damage`, preserving all clip pushes
441
+ * and pops needed to replay them from an empty clip stack. Unknown bounds are
442
+ * selected conservatively. This is deliberately a mask rather than a copied
443
+ * command list: all reads stay in the original retained arenas.
444
+ */
445
+ declare function createReplayMask<TTexture>(list: DrawList<TTexture>, damage: DamageRect, options?: ReplaySelectionOptions): ReplayMaskScratch;
446
+ //#endregion
447
+ //#region src/compiled-draw-list.d.ts
448
+ /** A maximal run that can stay in the quad executor without a painter-order
449
+ * barrier. Texture/matrix slots deliberately are not included: those limits are
450
+ * context-specific and the live batcher remains their authority. */
451
+ interface CompiledBatchDescriptor {
452
+ readonly start: number;
453
+ readonly end: number;
454
+ readonly blend: BlendMode;
455
+ readonly clipDepth: number;
456
+ }
457
+ interface CompiledDrawListDiagnostics {
458
+ planBuilds: number;
459
+ structuralInvalidations: number;
460
+ planReuses: number;
461
+ /** CPU template ranges refreshed after safe command patches. */
462
+ templateRangeUpdates: number;
463
+ reusedSelections: number;
464
+ reusedBatches: number;
465
+ }
466
+ interface CompiledRefreshResult {
467
+ readonly rebuilt: boolean;
468
+ readonly rangeUpdates: number;
469
+ /** Reused storage listing only commands patched since the prior refresh. */
470
+ readonly changedCommands: readonly number[];
471
+ /** Increments on every structural/overflow/context plan rebuild. */
472
+ readonly planGeneration: number;
473
+ /** Current DrawList content revision represented by this plan. */
474
+ readonly contentRevision: number;
475
+ /** Revision a cached consumer must match before applying this delta. */
476
+ readonly deltaBaseRevision: number;
477
+ }
478
+ /**
479
+ * A retained, allocation-free planning view of a stable draw list. It never
480
+ * owns GL objects: the executor's grow-only instance buffer remains the single
481
+ * GPU allocation authority. The WebGL executor may cache these templates in
482
+ * plan-keyed GPU buffers. Quad templates remove transform/source/tint decoding from replay;
483
+ * all non-quad commands deliberately take the direct executor path.
484
+ */
485
+ interface CompiledDrawList<TTexture = unknown> {
486
+ readonly list: DrawList<TTexture>;
487
+ readonly batches: readonly CompiledBatchDescriptor[];
488
+ readonly diagnostics: CompiledDrawListDiagnostics;
489
+ refresh(): CompiledRefreshResult;
490
+ invalidate(): void;
491
+ /** Fill the batcher's reusable staging instance from a cached quad template. */
492
+ fillQuad(index: number, textureWidth: number, textureHeight: number, out: QuadInstance): boolean;
493
+ /** Fill `out` with a cached conservative bound, or return null for unknown. */
494
+ commandBounds(index: number, out: DamageRect): DamageRect | null;
495
+ /** Reuse caller-owned selection storage and cached bounds. */
496
+ select(damage: DamageRect, scratch: ReplayMaskScratch, options?: ReplaySelectionOptions): ReplayMaskScratch;
497
+ }
498
+ declare function compileDrawList<TTexture>(list: DrawList<TTexture>): CompiledDrawList<TTexture>;
499
+ /** Convenience for consumers with no existing scratch. Retained loops should
500
+ * create one scratch once and call `plan.select` instead. */
501
+ declare function createCompiledReplayMask<TTexture>(plan: CompiledDrawList<TTexture>, damage: DamageRect, options?: ReplaySelectionOptions): ReplayMaskScratch;
502
+ //#endregion
503
+ //#region src/textures.d.ts
504
+ /**
505
+ * The executor's texture cache — PER CONTEXT, and its own rather than a
506
+ * generalization of `@godot-scene-web/html`'s.
507
+ *
508
+ * WHY NOT REUSE `html/webgl/shared-gl`'s CACHE. That module opens with a note
509
+ * saying its single context and its single module-scoped texture map are
510
+ * load-bearing: both live runtimes (shaders and particles) must share ONE
511
+ * WebGL2 context, because a page with many cards would otherwise walk into the
512
+ * browser's ~16-live-context limit, and ONE cache, so identical urls upload once.
513
+ * Threading a context parameter through it would turn a documented singleton into
514
+ * a keyed registry — a real change to an invariant two shipping runtimes rest on,
515
+ * to serve a consumer that wants DIFFERENT pixels anyway:
516
+ *
517
+ * - a `WebGLTexture` belongs to the context that made it and cannot be bound in
518
+ * another, so the stage's own context could not use those entries even if it
519
+ * could see them;
520
+ * - shared-gl uploads STRAIGHT alpha (`UNPACK_PREMULTIPLY_ALPHA_WEBGL` false),
521
+ * because its shaders do Godot's multiply themselves; this cache uploads
522
+ * PREMULTIPLIED (see below), which is a different byte in every texel;
523
+ * - shared-gl's entries are immortal and url-keyed with a load listener; a stage
524
+ * that paints hundreds of megabytes of card atlas needs refcounts and a
525
+ * `reset()` for context loss.
526
+ *
527
+ * So this is option (b) from the brief: a small cache of its own, ~150 lines, no
528
+ * change to a package two runtimes depend on.
529
+ *
530
+ * PREMULTIPLIED UPLOAD IS THE POINT, not a detail. `LINEAR` filtering blends
531
+ * texels; blending STRAIGHT colour weights a fully transparent texel's (usually
532
+ * black, or garbage) RGB equally with its opaque neighbour's, so every sprite
533
+ * edge gets a dark fringe and every atlas region bleeds its padding. Blending
534
+ * PREMULTIPLIED colour weights each texel's contribution by its own alpha, which
535
+ * is the arithmetic filtering is supposed to be doing. It also means a texel and
536
+ * the quad's tint compose with one componentwise multiply, and it is the same
537
+ * convention the stage canvas is declared with — one statement of the rule from
538
+ * the PNG to the compositor.
539
+ *
540
+ * SOURCES MUST BE READY. This module never decodes: it uploads what it is handed.
541
+ * A half-loaded `HTMLImageElement` uploads as nothing useful, so the caller
542
+ * decodes (`decode()`, `createImageBitmap`, a canvas it drew itself) and hands
543
+ * over a finished source. That keeps the whole package clear of DOM lifecycle —
544
+ * reading `.width` off an object someone else created is the only thing here that
545
+ * touches a DOM-shaped value.
546
+ */
547
+ /** Anything WebGL can upload directly. */
548
+ type CanvasTextureSource = TexImageSource;
549
+ /** A cached texture. `width`/`height` are the UPLOADED pixel dimensions, which is
550
+ * what normalizes a draw-list's page-pixel source rect into UVs. */
551
+ interface CanvasTextureHandle {
552
+ readonly texture: WebGLTexture;
553
+ readonly width: number;
554
+ readonly height: number;
555
+ }
556
+ interface TextureCacheStats {
557
+ /** Live entries. */
558
+ entries: number;
559
+ /** Pixel uploads made: `texImage2D` calls plus the `texSubImage2D` re-uploads
560
+ * {@link CanvasTextureCache.update} does into storage that already fits. */
561
+ uploads: number;
562
+ /** Of those, the ones that had to RE-SPECIFY the texture's storage — a first
563
+ * upload, or an `update` whose source changed size. The difference between
564
+ * this and `uploads` is how often the re-upload fast path was taken, which is
565
+ * the number a consumer that streams into one key wants to watch. */
566
+ respecs: number;
567
+ /** Entries deleted because their last reference was released. */
568
+ evictions: number;
569
+ /** Resident RGBA bytes, including every generated mip level. */
570
+ bytes: number;
571
+ }
572
+ /** Sampling and source-alpha options shared by texture acquisition methods.
573
+ *
574
+ * Mipmaps make minification stable, but their filtered levels can sample pixels
575
+ * outside an atlas rect. Use them only for standalone images or padded atlases.
576
+ */
577
+ interface CanvasTextureOptions {
578
+ /** `true` when the source already contains `rgb * a`. Defaults to false. */
579
+ premultiplied?: boolean;
580
+ /** Generate a complete mip chain. Not suitable for unpadded atlas pages. */
581
+ mipmap?: boolean;
582
+ /** Minification filter. Defaults to LINEAR, or LINEAR_MIPMAP_LINEAR with mips. */
583
+ minFilter?: number;
584
+ /** Magnification filter. Defaults to LINEAR; NEAREST is supported. */
585
+ magFilter?: number;
586
+ }
587
+ interface CanvasTextureCache {
588
+ readonly stats: TextureCacheStats;
589
+ /** The 1x1 opaque-white texel every untextured quad samples, so a solid fill
590
+ * needs no second shader and no special slot. */
591
+ white(): CanvasTextureHandle;
592
+ /** Look up without uploading or retaining. */
593
+ peek(key: string): CanvasTextureHandle | undefined;
594
+ /**
595
+ * Upload `source` under `key` and take a reference. A key already present is
596
+ * NOT re-uploaded (that is the decode-once guarantee) — only referenced again;
597
+ * use {@link CanvasTextureCache.update} to replace its pixels.
598
+ */
599
+ acquire(key: string, source: CanvasTextureSource, options?: CanvasTextureOptions): CanvasTextureHandle;
600
+ /**
601
+ * Same, from raw RGBA bytes. `premultiplied` says whether `pixels` already
602
+ * carries `rgb*a`; when it does not, the multiply happens HERE in JS rather
603
+ * than through `UNPACK_PREMULTIPLY_ALPHA_WEBGL`, whose behaviour over an
604
+ * `ArrayBufferView` is not worth depending on.
605
+ */
606
+ acquireBytes(key: string, pixels: Uint8Array | Uint8ClampedArray, width: number, height: number, options?: CanvasTextureOptions): CanvasTextureHandle;
607
+ /** Take another reference to an existing key. Throws when it is not present. */
608
+ retain(key: string): CanvasTextureHandle;
609
+ /** Drop a reference; the entry is deleted when the last one goes. */
610
+ release(key: string): void;
611
+ /**
612
+ * Replace an existing (or create a new) entry's pixels, keeping its refcount.
613
+ *
614
+ * A source the same size as what the entry already holds is re-uploaded with
615
+ * `texSubImage2D`, INTO the storage that is already there; only a size change
616
+ * re-specifies it with `texImage2D`. That matters for a consumer streaming a
617
+ * live surface into one key every frame, which is the shape this method exists
618
+ * for: a re-spec frees the old mip level and allocates a new one on every call,
619
+ * so a same-size stream would churn a few megabytes of driver allocation per
620
+ * frame to write the same number of texels. See `stats.respecs`.
621
+ */
622
+ update(key: string, source: CanvasTextureSource): CanvasTextureHandle;
623
+ /**
624
+ * Write `source` into an EXISTING entry's storage at `(x, y)`, leaving the rest
625
+ * of it untouched. Nothing is allocated, nothing is re-specified, the refcount
626
+ * does not move, and the sampler parameters are not re-set.
627
+ *
628
+ * This is the ATLAS PAGE case, and it is why {@link CanvasTextureCache.update}
629
+ * is not enough for it. A page is one texture that many small sources are
630
+ * written into over time; `update` can only replace the whole thing, so a
631
+ * consumer holding a 1024x1024 page would have to re-upload all four megabytes
632
+ * every time one 40x18 label changed — which on a phone is tens of milliseconds
633
+ * for a few kilobytes of new pixels. Writing just the region is proportional to
634
+ * what actually changed.
635
+ *
636
+ * REFUSED, with `null` and no GL call at all, when the key is unknown or when
637
+ * the rect does not lie wholly inside the entry's real storage (see
638
+ * `Entry.storageW`, which is not always the entry's claimed size). Both would
639
+ * otherwise be a silent `INVALID_VALUE` on the context — an error the caller
640
+ * cannot see and the next draw cannot explain. A refusal is a fact the caller
641
+ * is expected to handle (re-allocate, or fall back), not an exception.
642
+ *
643
+ * Counted as one `uploads` and never a `respec`, which is the distinction the
644
+ * stat exists to make.
645
+ */
646
+ updateRegion(key: string, source: CanvasTextureSource, x: number, y: number): CanvasTextureHandle | null;
647
+ /** The context was lost: forget every entry WITHOUT touching the dead driver. */
648
+ reset(): void;
649
+ /** Delete every texture and empty the cache. */
650
+ dispose(): void;
651
+ }
652
+ declare function createTextureCache(gl: WebGL2RenderingContext): CanvasTextureCache;
653
+ //#endregion
654
+ //#region src/executor-webgl.d.ts
655
+ /**
656
+ * The WebGL2 executor: a draw list in, GL draws out.
657
+ *
658
+ * ONE PROGRAM, ONE VERTEX FORMAT, ONE BUFFER for the whole scene. Every command
659
+ * kind is reduced to the same instanced quad — a sprite is one, a nine-patch is
660
+ * up to nine (`./nine-patch`), a polyline is one per segment plus one per join
661
+ * (`./polyline`), a solid fill is one sampling a 1x1 white texel. That is what
662
+ * lets `./batcher` merge across command kinds instead of only within them, and it
663
+ * is why there is no "solid" shader, no "line" shader and no per-kind draw path
664
+ * to keep in sync.
665
+ *
666
+ * WHAT BREAKS A BATCH, and what deliberately does not. GL state that lives on the
667
+ * DRAW rather than on the instance has to break one: the blend mode, the scissor
668
+ * box, and the rounded-clip uniforms. Everything else is per-instance data —
669
+ * transform, source rect, tint, texture (an index into the batch's slot table),
670
+ * colour matrix (an index into the batch's uniform table) — so it costs a few
671
+ * floats instead of a draw call. See `./batcher`'s note for the measurement that
672
+ * decided this.
673
+ *
674
+ * ORDER OF OPERATIONS AROUND A STATE CHANGE, which is the one genuinely subtle
675
+ * thing in this file. A flush DRAWS with whatever GL state is currently set, so
676
+ * the pending batch must be flushed BEFORE the new state is applied, never after.
677
+ * Every state change here therefore reads: tell the batcher (which flushes the
678
+ * old batch under the old GL state), then touch GL. Doing it the other way round
679
+ * is invisible in a screenshot of a static scene and produces a one-frame-late
680
+ * clip the moment anything moves.
681
+ *
682
+ * THE ONE COMMAND THAT IS NOT A QUAD. `glyphs` runs are outlines evaluated per fragment, which no
683
+ * amount of instancing turns into the program above, so they are delegated to an injected
684
+ * {@link GlyphPass} (`./glyph-pass`) and cost one draw call each. That makes them the only place
685
+ * this file hands the context to somebody else mid-frame, and the order-of-operations rule above is
686
+ * exactly what governs it: flush, then the pass, then rebind. `emitGlyphsCommand` is where that is
687
+ * written down.
688
+ *
689
+ * THE ALPHA CONTRACT, end to end: textures upload premultiplied (`./textures`),
690
+ * tints in the draw list are premultiplied, the fragment emits `vec4(rgb*a, a)`,
691
+ * the MIX blend is `(ONE, ONE_MINUS_SRC_ALPHA)`, and the canvas is declared
692
+ * `premultipliedAlpha: true` (`./present`). Any one of those five flipped on its
693
+ * own is silent — nothing errors, the picture is just wrong — which is why they
694
+ * are named together here.
695
+ */
696
+ /** A texture handle the executor can draw: the GL object and the size its source
697
+ * rects are measured against. `CanvasTextureHandle` satisfies it. */
698
+ type ExecutorTexture = CanvasTextureHandle;
699
+ /** GL blend state for a Godot blend mode, as ENUM NAMES so the mapping is pure
700
+ * and unit-testable (the same trick `html`'s `blendFactorsFor` uses). */
701
+ interface BlendState {
702
+ equationRgb: "FUNC_ADD" | "FUNC_REVERSE_SUBTRACT";
703
+ equationAlpha: "FUNC_ADD";
704
+ srcRgb: "ONE" | "DST_COLOR";
705
+ dstRgb: "ONE" | "ZERO" | "ONE_MINUS_SRC_ALPHA";
706
+ srcAlpha: "ONE" | "DST_ALPHA";
707
+ dstAlpha: "ONE" | "ZERO" | "ONE_MINUS_SRC_ALPHA";
708
+ }
709
+ /** The GL blend state a Godot `CanvasItemMaterial.BlendMode` maps to. */
710
+ declare function blendStateFor(blend: BlendMode): BlendState;
711
+ interface ExecutorStats {
712
+ /** Draw-list commands read. */
713
+ commands: number;
714
+ /** Quad instances pushed, including expanded nine-patch bands and stroke quads. */
715
+ quads: number;
716
+ /** `drawArraysInstanced` calls — the number this executor exists to keep small. */
717
+ batches: number;
718
+ /** Texture-unit binds summed over batches. */
719
+ textureBinds: number;
720
+ /** Times the scissor box actually changed. */
721
+ scissorChanges: number;
722
+ /** Times GL blend state actually changed. */
723
+ blendChanges: number;
724
+ ninePatches: number;
725
+ ninePatchQuads: number;
726
+ polylines: number;
727
+ polylineQuads: number;
728
+ /** Indexed textured-mesh commands executed. */
729
+ texturedMeshes: number;
730
+ /** Triangle-list primitives submitted by textured meshes. */
731
+ texturedMeshTriangles: number;
732
+ /** `drawElements` calls issued by textured meshes. */
733
+ texturedMeshDrawCalls: number;
734
+ /** `glyphs` commands handed to the installed {@link CanvasExecutorOptions.glyphs} pass. */
735
+ glyphRuns: number;
736
+ /** Glyphs the pass reported drawing, summed over runs. Excludes ones it skipped. */
737
+ glyphs: number;
738
+ /** Draw calls the pass reported. One per run, for the reason {@link GlyphPass.drawRun} gives. */
739
+ glyphDrawCalls: number;
740
+ glyphRunBatches: number;
741
+ glyphRunBatchFallbacks: number;
742
+ /**
743
+ * `glyphs` commands seen with NO pass installed.
744
+ *
745
+ * A NAMED NO-OP RATHER THAN A SILENT ONE. A list carrying text into an executor that cannot draw
746
+ * text is a wiring mistake — the consumer forgot to pass `glyphs` — and its symptom is a page
747
+ * that renders perfectly except for having no words on it. That is exactly the kind of failure
748
+ * a screenshot review passes and a counter catches.
749
+ */
750
+ glyphRunsDropped: number;
751
+ /** Screen-dependent passes executed at their recorded painter position. */
752
+ screenEffects: number;
753
+ /** Required screen passes that refused or failed at runtime. */
754
+ screenEffectFailures: number;
755
+ /** Direct external passes executed at their recorded painter position. */
756
+ externalEffects: number;
757
+ /** Required direct external passes that refused or failed at runtime. */
758
+ externalEffectFailures: number;
759
+ /** Clip rects that could not be an exact scissor (see `./clip-stack`). */
760
+ rotatedClipFallbacks: number;
761
+ /** `clipPop`s with nothing open — a malformed list, survived rather than thrown. */
762
+ unbalancedClipPops: number;
763
+ /**
764
+ * Commands whose kind this executor does not handle.
765
+ *
766
+ * The dispatch switch had no `default` for its first five kinds, so a sixth added to the IR fell
767
+ * through it silently while still counting in {@link ExecutorStats.commands} — a frame missing
768
+ * every command of the new kind, reported as a frame that drew everything.
769
+ */
770
+ unknownCommands: number;
771
+ /** Largest single batch, in instances. */
772
+ maxBatchQuads: number;
773
+ /** Why batches ended, so a regression in batching says which axis moved. */
774
+ flushes: Record<BatchFlushReason, number>;
775
+ /** Per-execution compiled-plan diagnostics; direct frames leave these zero. */
776
+ compiledPlanBuilds: number;
777
+ compiledPlanReuses: number;
778
+ compiledTemplateRangeUpdates: number;
779
+ reusedSelections: number;
780
+ reusedBatches: number;
781
+ compiledGpuFullUploads: number;
782
+ compiledGpuRangeUploads: number;
783
+ compiledCachedDrawCalls: number;
784
+ }
785
+ interface CanvasExecutorOptions {
786
+ gl: WebGL2RenderingContext;
787
+ /** The 1x1 white texel untextured quads sample. Defaults to one the executor
788
+ * makes and owns; pass `CanvasTextureCache.white()` to share the cache's. */
789
+ white?: ExecutorTexture;
790
+ /**
791
+ * Who draws `glyphs` commands. Omitted means the executor cannot draw text.
792
+ *
793
+ * INJECTED, exactly like {@link CanvasExecutorOptions.white}, and for the reason
794
+ * {@link GlyphPass} states: the only implementation is backed by a glyph renderer that a scene
795
+ * with no text should not have to load, and the main barrel's export surface is mirrored by hand
796
+ * downstream. Import `@godot-scene-web/canvas/glyphs` and pass one when the scene has text.
797
+ *
798
+ * Leaving it out is not an error — plenty of draw lists have no glyph runs at all — but a list
799
+ * that DOES carry one then counts it in {@link ExecutorStats.glyphRunsDropped} rather than
800
+ * skipping it in silence.
801
+ */
802
+ glyphs?: GlyphPass;
803
+ /**
804
+ * Opt-in only: combine physically adjacent glyph commands when the injected pass exposes
805
+ * `drawRuns`. This never crosses a clip, a command-mask gap, or any non-glyph command.
806
+ */
807
+ batchAdjacentGlyphRuns?: boolean;
808
+ /** Texture units per batch. Clamped to the context's `MAX_TEXTURE_IMAGE_UNITS`
809
+ * and to {@link MAX_TEXTURE_SLOTS}. */
810
+ maxTextureSlots?: number;
811
+ /** Colour-matrix slots per batch, including the identity at slot 0. */
812
+ maxColorMatrices?: number;
813
+ /** Initial instance-arena capacity, in quads. */
814
+ quadCapacity?: number;
815
+ }
816
+ interface ExecuteOptions {
817
+ /** Clear the framebuffer to transparent black before drawing. Default true. */
818
+ clear?: boolean;
819
+ /**
820
+ * Replacement clear colour for a full-frame stage pass. It is intentionally
821
+ * an execute option rather than a global GL mutation: retained FBO replays
822
+ * keep their transparent clear while an opaque presenter can establish its
823
+ * base pixels without a second fullscreen draw.
824
+ */
825
+ clearColor?: readonly [number, number, number, number];
826
+ /**
827
+ * Restrict both clearing and drawing to this top-left framebuffer-pixel
828
+ * rectangle. It is the retained-surface seam, but is also useful to any
829
+ * caller rendering into its own FBO; omitted keeps the direct full-frame
830
+ * executor path byte-for-byte in shape.
831
+ */
832
+ damage?: DamageRect;
833
+ /** Ordered replay selection. Clip closures are supplied by `createReplayMask`.
834
+ * Omitted means every command is executed, as direct frames always have. */
835
+ commandMask?: CommandMask;
836
+ /** Opt-in retained plan. Omitted direct frames do no compilation work. */
837
+ compiled?: CompiledDrawList<ExecutorTexture | null>;
838
+ }
839
+ interface CanvasExecutor {
840
+ readonly gl: WebGL2RenderingContext;
841
+ readonly stats: ExecutorStats;
842
+ /** Texture units a batch can hold on THIS context. */
843
+ readonly maxTextureSlots: number;
844
+ /** Build the program and buffers now, off whatever critical path the caller
845
+ * cares about. `execute` does it lazily otherwise — and reading a shader's
846
+ * compile status BLOCKS on the driver (~100-250 ms on a phone), so paying it
847
+ * during the first frame is a visible hitch. */
848
+ warmUp(): boolean;
849
+ /** Draw one frame. Returns false when the program could not be built. */
850
+ execute(list: DrawList<ExecutorTexture | null>, projection: StageProjection, options?: ExecuteOptions): boolean;
851
+ /** Release executor-owned cached GPU buffers for one compiled plan. */
852
+ releaseCompiled(plan: CompiledDrawList<ExecutorTexture | null>): void;
853
+ /** The context is GONE: drop every GL object without calling into GL. The next
854
+ * `execute` rebuilds them. */
855
+ invalidate(): void;
856
+ /** Delete every GL object this executor owns. */
857
+ dispose(): void;
858
+ }
859
+ declare function createCanvasExecutor(options: CanvasExecutorOptions): CanvasExecutor;
860
+ //#endregion
861
+ //#region src/headless-effects.d.ts
862
+ /** Bind a GPU screen producer at one DrawList painter position. */
863
+ declare function createHeadlessScreenEffectCommand(producer: HeadlessShaderProducer, input: HeadlessShaderRenderInput): ScreenEffectDrawCommand;
864
+ /** Bind a direct GPU particle pass at one DrawList painter position. */
865
+ declare function createHeadlessGodotParticleDirectEffect(pass: HeadlessGodotParticleDirectPass, input: HeadlessGodotParticleDirectRenderInput): ExternalEffectDrawCommand;
866
+ //#endregion
867
+ //#region src/nine-patch.d.ts
868
+ /**
869
+ * Nine-patch band algebra: one `DRAW_NINE_PATCH` command -> up to nine plain
870
+ * quads. Pure (no GL, no state), so the geometry can be unit-tested on its own —
871
+ * it is the part of the executor most likely to be wrong by half a pixel and the
872
+ * part least able to say so on screen.
873
+ *
874
+ * WHERE THE RULES COME FROM. Godot does not expand a nine-patch into quads at
875
+ * all: it draws ONE quad and remaps each fragment's coordinate with
876
+ * `map_ninepatch_axis` (`drivers/gles3/shaders/canvas.glsl`). That function is
877
+ * this module's specification, per axis:
878
+ *
879
+ * - `pixel < margin_begin` -> source coordinate `pixel`, i.e. the leading corner
880
+ * band is copied 1:1 at its native pixel size;
881
+ * - `pixel >= draw_size - margin_end` -> source `tex_size - (draw_size - pixel)`,
882
+ * the trailing corner band, also 1:1;
883
+ * - otherwise the centre band, stretched from `[margin_begin, tex_size -
884
+ * margin_end]` onto `[margin_begin, draw_size - margin_end]`.
885
+ *
886
+ * Expanding that into rects gives identical pixels for the STRETCH axis mode and
887
+ * costs a handful of extra quads that batch with everything else — much cheaper
888
+ * than the branchy per-fragment remap, and it keeps one shader for every command
889
+ * kind. Godot's TILE / TILE_FIT modes are NOT reachable from the draw-list IR
890
+ * (which carries no axis-stretch mode), so STRETCH — Godot's default — is what
891
+ * this implements.
892
+ *
893
+ * DEGENERATE MARGINS FOLLOW THE SAME SPECIFICATION rather than a clamp. Note the
894
+ * order of the branches above: when the two margins together exceed the
895
+ * destination, the LEADING band wins the overlap and the trailing band keeps only
896
+ * what is left. So this module truncates rather than rescaling the corners, which
897
+ * is what the shader does. Bands that come out empty are dropped, so a patch
898
+ * squeezed below its own margins expands to fewer than nine quads (down to one,
899
+ * or to none at all when it has no area).
900
+ *
901
+ * A SOURCE centre that is empty or inverted (the margins meet or cross inside the
902
+ * texture region) is dropped too. Godot's remap would produce a reversed source
903
+ * range there — a mirrored smear — which is nobody's intent.
904
+ */
905
+ /** One expanded band: a destination rect in the command's LOCAL space (before the
906
+ * quad's affine `m`), and the source rect it samples, in page pixels. */
907
+ interface NinePatchBand {
908
+ /** Destination, local space: x in `[0, w]`, y in `[0, h]`. */
909
+ dstX: number;
910
+ dstY: number;
911
+ dstW: number;
912
+ dstH: number;
913
+ /** Source, page pixels — an absolute rect on the page, not relative to the region. */
914
+ srcX: number;
915
+ srcY: number;
916
+ srcW: number;
917
+ srcH: number;
918
+ }
919
+ /** The nine-patch inputs, matching `NinePatchView`'s fields. */
920
+ interface NinePatchGeometry {
921
+ /** Destination size in local units. */
922
+ w: number;
923
+ h: number;
924
+ /** The patch REGION on the page, in page pixels. */
925
+ srcX: number;
926
+ srcY: number;
927
+ srcW: number;
928
+ srcH: number;
929
+ /** Insets into the region, page pixels (Godot `patch_margin_*`). */
930
+ marginLeft: number;
931
+ marginTop: number;
932
+ marginRight: number;
933
+ marginBottom: number;
934
+ }
935
+ declare function createNinePatchBand(): NinePatchBand;
936
+ /** A reusable output buffer: nine bands is the hard maximum, so it never grows. */
937
+ declare function createNinePatchBands(): NinePatchBand[];
938
+ /**
939
+ * Expand a nine-patch into its bands, filling `out` (use
940
+ * {@link createNinePatchBands}, which is always big enough) and returning how
941
+ * many are live. `out` entries past the return value are stale and must not be
942
+ * read.
943
+ *
944
+ * Returns 0 for a patch with no area — a zero-size destination or a zero-size
945
+ * region draws nothing at all, which is not the same as drawing one empty band.
946
+ */
947
+ declare function expandNinePatch(patch: NinePatchGeometry, out: NinePatchBand[]): number;
948
+ //#endregion
949
+ //#region src/polyline.d.ts
950
+ /**
951
+ * Constant-width polyline -> quads, so a `DRAW_POLYLINE` goes through the SAME
952
+ * instance buffer, the same shader and the same batch as everything else instead
953
+ * of forcing a second program and a mid-frame draw break.
954
+ *
955
+ * That is possible because the batcher's instance carries four EXPLICIT corners
956
+ * rather than an affine basis (see `./batcher`): a quad instance is any
957
+ * quadrilateral, and a quadrilateral with its last two corners coincident is a
958
+ * triangle. So a stroke is emitted as
959
+ *
960
+ * - one parallelogram per segment (the segment offset by ±width/2 along its
961
+ * normal), and
962
+ * - one triangle per interior vertex, filling the notch on the OUTSIDE of the
963
+ * turn.
964
+ *
965
+ * JOINS ARE BEVEL, CAPS ARE BUTT — the first pass the wave-1 brief allows, and a
966
+ * deliberate choice rather than an oversight:
967
+ *
968
+ * - a MITER join needs the two segment edges extended to their intersection,
969
+ * which runs away to infinity as the turn approaches a reversal and therefore
970
+ * needs a miter limit that itself falls back to... a bevel. Two code paths for
971
+ * a shape that differs from the bevel only inside a `width/2` disc.
972
+ * - a ROUND join needs an arc, i.e. a fan of triangles whose count depends on the
973
+ * turn angle and the on-screen width — the one thing in this file that would
974
+ * make its output size unpredictable.
975
+ *
976
+ * The bevel differs from both only within half a stroke width of a vertex, and
977
+ * the measured population of polylines in the recorded scenes this executor was
978
+ * sized against is ZERO (the paint-source mix is texture/text/particles/spine/
979
+ * solid). Upgrading to round joins is local to this file: emit a fan instead of
980
+ * the single wedge triangle in the interior-vertex loop.
981
+ *
982
+ * Self-overlap: at a sharp turn the two segment parallelograms overlap near the
983
+ * vertex, so a translucent stroke double-composites there. Godot's own
984
+ * `draw_polyline` has the same artefact; fixing it needs a stencil or a
985
+ * single-pass SDF, neither of which belongs in wave 1.
986
+ */
987
+ /** Floats per emitted quad: four `(x, y)` corners in draw-list local space. */
988
+ declare const POLYLINE_QUAD_FLOATS = 8;
989
+ /**
990
+ * The most quads a `pointCount`-point stroke can produce: one per segment plus
991
+ * one per interior vertex. Sizes a caller's output buffer exactly.
992
+ */
993
+ declare function polylineQuadCapacity(pointCount: number): number;
994
+ /**
995
+ * Tessellate `points` (flattened `x, y, x, y, …`, the draw-list's own layout)
996
+ * into quads, writing `POLYLINE_QUAD_FLOATS` floats per quad into `out` from
997
+ * `outOffset`. Returns the number of quads written.
998
+ *
999
+ * Corners are written in the batcher's unit-square order — `(0,0)`, `(1,0)`,
1000
+ * `(1,1)`, `(0,1)` — so a triangle is spelled by repeating the last corner.
1001
+ *
1002
+ * Zero-length segments are skipped (they have no direction to offset along, and
1003
+ * a duplicated point is a common artefact of a resampled path); a vertex whose
1004
+ * incoming or outgoing segment was skipped gets no join wedge, because there is
1005
+ * no notch to fill.
1006
+ */
1007
+ declare function expandPolyline(points: ArrayLike<number>, pointCount: number, width: number, out: Float32Array, outOffset?: number): number;
1008
+ //#endregion
1009
+ //#region src/retained-surface.d.ts
1010
+ /** An RGBA8 framebuffer that retains scene pixels between frames. */
1011
+ interface RetainedSurface {
1012
+ readonly gl: WebGL2RenderingContext;
1013
+ /** The achieved, integer backing dimensions. Zero means unallocated. */
1014
+ readonly width: number;
1015
+ readonly height: number;
1016
+ /** GPU allocation is live. It says nothing about whether it contains a frame. */
1017
+ readonly allocated: boolean;
1018
+ /** True only after a full replay; `present` refuses unseeded pixels. */
1019
+ readonly contentValid: boolean;
1020
+ /**
1021
+ * Allocate an RGBA8 texture/FBO at the exact snapped requested size. A size
1022
+ * change discards old pixels, so callers must replay a full frame afterwards.
1023
+ */
1024
+ resize(width: number, height: number): boolean;
1025
+ /**
1026
+ * Replay into the retained target. `damage` is top-left framebuffer pixels;
1027
+ * passing it restricts both clear and draw to that region. This operation is
1028
+ * explicit: direct `CanvasExecutor.execute` calls can neither seed this FBO
1029
+ * nor present it.
1030
+ */
1031
+ replay(executor: CanvasExecutor, list: DrawList<ExecutorTexture | null>, projection: StageProjection, options?: RetainedReplayOptions): boolean;
1032
+ /**
1033
+ * Replay a prevalidated exact cover into one retained target binding. Every
1034
+ * region keeps its own damage scissor and clip-complete mask, so painter
1035
+ * order, clips and blend modes are identical to individual partial replays.
1036
+ * The whole set is validated before its first clear; any failure invalidates
1037
+ * retained content and a caller must seed rather than present it.
1038
+ */
1039
+ replayRegions(executor: CanvasExecutor, list: DrawList<ExecutorTexture | null>, projection: StageProjection, regions: readonly RetainedReplayRegion[], options?: RetainedReplayRegionsOptions): boolean;
1040
+ /** Blit the retained texture once, 1:1, to the default framebuffer. */
1041
+ present(): boolean;
1042
+ /** Forget retained pixels while keeping the allocated FBO for the next seed. */
1043
+ invalidateContent(): void;
1044
+ /** Drop dead context handles without calling GL after a context loss. */
1045
+ invalidate(): void;
1046
+ /** Delete owned GL resources while the context is live. */
1047
+ dispose(): void;
1048
+ }
1049
+ interface RetainedReplayOptions {
1050
+ /**
1051
+ * A physical-pixel damage region. Partial retained replay deliberately
1052
+ * requires one; an unbounded replay is a seed and therefore renders the
1053
+ * complete list without a mask.
1054
+ */
1055
+ damage?: DamageRect;
1056
+ /**
1057
+ * A validated, clip-complete selection from `createReplayMaskScratch` or a
1058
+ * compiled draw-list. A generic `CommandMask` is intentionally not accepted:
1059
+ * it could omit an unknown or screen-dependent command and leave stale FBO
1060
+ * pixels that `present()` would otherwise expose.
1061
+ */
1062
+ mask?: ReplayMaskScratch;
1063
+ /** Forwarded only for rare custom executor behaviour; clear defaults to true. */
1064
+ execute?: Omit<ExecuteOptions, "clear" | "damage" | "commandMask">;
1065
+ }
1066
+ /** One independently scissored, clip-complete member of a retained replay set. */
1067
+ interface RetainedReplayRegion {
1068
+ damage: DamageRect;
1069
+ mask: ReplayMaskScratch;
1070
+ }
1071
+ /** Shared executor options for every region in one retained replay set. */
1072
+ interface RetainedReplayRegionsOptions {
1073
+ execute?: Omit<ExecuteOptions, "clear" | "damage" | "commandMask">;
1074
+ }
1075
+ /** Snap a CSS/device calculation once at the allocation boundary. */
1076
+ declare function snapRetainedSize(value: number): number;
1077
+ /**
1078
+ * Create a retained RGBA8 texture/FBO. It deliberately does not create a
1079
+ * canvas or context — a stage owns that DOM-facing concern — and it does not
1080
+ * monkey-patch an executor. Retention is opt-in per replay/present call.
1081
+ */
1082
+ declare function createRetainedSurface(gl: WebGL2RenderingContext): RetainedSurface;
1083
+ //#endregion
1084
+ export { BLEND_ADD, BLEND_MIX, BLEND_MUL, BLEND_SUB, type Batch, type BatchFlushReason, type BatchTexture, type BatcherStats, type BlendMode, type BlendState, COLOR_MATRIX_FLOATS, type CanvasExecutor, type CanvasExecutorOptions, type CanvasStage, type CanvasStageOptions, type CanvasTextureCache, type CanvasTextureHandle, type CanvasTextureOptions, type CanvasTextureSource, type ClipBounds, type ClipRectView, type ClipStack, type CommandBounds, type CommandMask, type CompiledBatchDescriptor, type CompiledDrawList, type CompiledDrawListDiagnostics, type CompiledRefreshResult, DEFAULT_COLOR_MATRIX_SLOTS, DRAW_CLIP_POP, DRAW_CLIP_PUSH, DRAW_COMMAND_NAMES, DRAW_EXTERNAL_EFFECT, DRAW_GLYPHS, DRAW_NINE_PATCH, DRAW_POLYLINE, DRAW_QUAD, DRAW_SCREEN_EFFECT, DRAW_TEXTURED_MESH, type DamageRect, type DamageTile, type DamageTiles, type DamageTransform, type DirtyRect, type DrawCommandKind, type DrawCommandName, type DrawList, type DrawListFragment, type DrawListFragmentPatch, type DrawListOptions, type DrawListPatchView, type ExecuteOptions, type ExecutorStats, type ExecutorTexture, type ExternalEffectDrawCommand, type ExternalEffectDrawContext, FLIP_H, FLIP_V, type GlyphPass, type GlyphsView, IDENTITY_COLOR_MATRIX, INSTANCE_COLOR_OFFSET, INSTANCE_CORNERS_OFFSET, INSTANCE_FLOATS, INSTANCE_SLOTS_OFFSET, INSTANCE_UV_OFFSET, MAX_TEXTURE_SLOTS, type NinePatchBand, type NinePatchGeometry, type NinePatchView, POLYLINE_QUAD_FLOATS, type PixelTransform, type PolylineView, type QuadBatcher, type QuadBatcherOptions, type QuadInstance, type QuadView, RETAINED_DAMAGE_TILE_SIZE, RETAINED_MAX_DAMAGE_COVERAGE, RETAINED_MAX_REPLAY_FRACTION, type ReplayMaskScratch, type ReplaySelectionOptions, type RetainedReplayOptions, type RetainedReplayRegion, type RetainedReplayRegionsOptions, type RetainedSurface, type Rgba, type RoundedClip, STAGE_CONTEXT_ATTRIBUTES, type ScissorBox, type ScreenEffectDrawCommand, type ScreenEffectDrawContext, type StageCanvas, type StageProjection, type TextureCacheStats, type TexturedMeshView, applyColorMatrix01, blendStateFor, clamp01, colorMatricesEqual, commandDamageBounds, compileDrawList, createCanvasExecutor, createCanvasStage, createClipRectView, createClipStack, createCompiledReplayMask, createDamageRect, createDamageTiles, createDrawList, createDrawListFragment, createDrawListPatchView, createGlyphsView, createHeadlessGodotParticleDirectEffect, createHeadlessScreenEffectCommand, createNinePatchBand, createNinePatchBands, createNinePatchView, createPolylineView, createQuadBatcher, createQuadInstance, createQuadView, createReplayMask, createReplayMaskScratch, createRetainedSurface, createRgba, createScissorBox, createTextureCache, createTexturedMeshView, damageIntersects, expandNinePatch, expandPolyline, isAxisAligned, isDamageEmpty, isIdentityColorMatrix, isPartialReplayMask, maxPartialReplayCommands, modulatePremultiplied, outsetDamageRect, polylineQuadCapacity, premultiply, setViewColorMatrix, shadeQuadPixel, snapRetainedSize, transformDamageRect, unionDamageRect, unpremultiply };
1085
+ //# sourceMappingURL=index.d.ts.map