@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.
- package/LICENSE +21 -0
- package/dist/glyph-pass-DfQlp_IH.d.ts +882 -0
- package/dist/glyph-pass-DfQlp_IH.d.ts.map +1 -0
- package/dist/glyph-pass-hbgpu.d.ts +227 -0
- package/dist/glyph-pass-hbgpu.d.ts.map +1 -0
- package/dist/glyph-pass-hbgpu.js +387 -0
- package/dist/glyph-pass-hbgpu.js.map +1 -0
- package/dist/index.d.ts +1085 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4763 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4763 @@
|
|
|
1
|
+
//#region src/color.ts
|
|
2
|
+
/**
|
|
3
|
+
* Premultiplied colour composition — the arithmetic the batcher writes into an
|
|
4
|
+
* instance and the fragment shader repeats on the GPU, in one place so the two
|
|
5
|
+
* can be checked against each other (and against `html`'s CPU tint bake).
|
|
6
|
+
*
|
|
7
|
+
* TWO CONVENTIONS MEET HERE, and mixing them up is the classic renderer bug:
|
|
8
|
+
*
|
|
9
|
+
* - a **premultiplied** colour carries `(r·a, g·a, b·a, a)`. That is what the
|
|
10
|
+
* draw-list's tints are, what every texture in {@link ./textures} is uploaded
|
|
11
|
+
* as, what the fragment emits, and what the canvas is declared as. Composing
|
|
12
|
+
* two premultiplied colours is a plain componentwise multiply.
|
|
13
|
+
* - a **straight** colour carries `(r, g, b, a)` with the channels independent.
|
|
14
|
+
* The only thing that wants straight colour is the 3x3 colour matrix, because
|
|
15
|
+
* the matrix is defined on the texture's own RGB — multiply a premultiplied
|
|
16
|
+
* colour by it and a 50%-alpha pixel is transformed as if it were half as
|
|
17
|
+
* bright.
|
|
18
|
+
*
|
|
19
|
+
* Everything below is sRGB-domain: no linearization anywhere. That is not a
|
|
20
|
+
* shortcut, it is the contract — the matrices come from Godot HSV materials that
|
|
21
|
+
* Godot itself applies to sRGB texture bytes, and `html`'s
|
|
22
|
+
* `applyColorMatrixToPixels` (the CPU bake of the same transform, used by the DOM
|
|
23
|
+
* renderer) applies them to sRGB bytes too. Linearizing here would make the GPU
|
|
24
|
+
* path disagree with the DOM path it is meant to replace.
|
|
25
|
+
*/
|
|
26
|
+
/** Row-major 3x3 identity, the value slot 0 of a batch's matrix table holds. */
|
|
27
|
+
const IDENTITY_COLOR_MATRIX = [
|
|
28
|
+
1,
|
|
29
|
+
0,
|
|
30
|
+
0,
|
|
31
|
+
0,
|
|
32
|
+
1,
|
|
33
|
+
0,
|
|
34
|
+
0,
|
|
35
|
+
0,
|
|
36
|
+
1
|
|
37
|
+
];
|
|
38
|
+
function createRgba() {
|
|
39
|
+
return {
|
|
40
|
+
r: 1,
|
|
41
|
+
g: 1,
|
|
42
|
+
b: 1,
|
|
43
|
+
a: 1
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function clamp01(value) {
|
|
47
|
+
if (!(value > 0)) return 0;
|
|
48
|
+
return value < 1 ? value : 1;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Straight `(r,g,b,a)` -> premultiplied, clamped. The shape a producer has (a
|
|
52
|
+
* Godot `modulate` is straight) turned into the shape {@link QuadView} wants.
|
|
53
|
+
*/
|
|
54
|
+
function premultiply(r, g, b, a, out) {
|
|
55
|
+
const alpha = clamp01(a);
|
|
56
|
+
out.r = clamp01(r) * alpha;
|
|
57
|
+
out.g = clamp01(g) * alpha;
|
|
58
|
+
out.b = clamp01(b) * alpha;
|
|
59
|
+
out.a = alpha;
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Premultiplied -> straight, with the `a === 0` hole filled with black. The
|
|
64
|
+
* inverse of {@link premultiply} up to that hole (a fully transparent
|
|
65
|
+
* premultiplied pixel has forgotten its colour, so nothing can recover it).
|
|
66
|
+
*/
|
|
67
|
+
function unpremultiply(colour, out) {
|
|
68
|
+
const a = colour.a;
|
|
69
|
+
if (a <= 0) {
|
|
70
|
+
out.r = 0;
|
|
71
|
+
out.g = 0;
|
|
72
|
+
out.b = 0;
|
|
73
|
+
out.a = 0;
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
out.r = colour.r / a;
|
|
77
|
+
out.g = colour.g / a;
|
|
78
|
+
out.b = colour.b / a;
|
|
79
|
+
out.a = a;
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Compose two PREMULTIPLIED colours — a texel times its quad's tint. A plain
|
|
84
|
+
* componentwise multiply, which is the whole reason the premultiplied form is
|
|
85
|
+
* worth keeping: with straight colours this would need the alpha handled apart
|
|
86
|
+
* from the channels it has already scaled.
|
|
87
|
+
*/
|
|
88
|
+
function modulatePremultiplied(source, tint, out) {
|
|
89
|
+
out.r = source.r * tint.r;
|
|
90
|
+
out.g = source.g * tint.g;
|
|
91
|
+
out.b = source.b * tint.b;
|
|
92
|
+
out.a = source.a * tint.a;
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Apply a row-major 3x3 to STRAIGHT sRGB channels in 0..1, clamped — the float
|
|
97
|
+
* twin of `html`'s `applyColorMatrixToPixels` (which works in 0..255 bytes and
|
|
98
|
+
* clamps because it writes a `Uint8ClampedArray`) and of the colour-matrix branch
|
|
99
|
+
* in the executor's fragment shader. All three must agree; `colour.test.ts`
|
|
100
|
+
* asserts the first two against each other pixel for pixel.
|
|
101
|
+
*
|
|
102
|
+
* Alpha is untouched, exactly as `feColorMatrix` with only the RGB rows set.
|
|
103
|
+
*/
|
|
104
|
+
function applyColorMatrix01(colour, matrix, offset, out) {
|
|
105
|
+
const r = colour.r;
|
|
106
|
+
const g = colour.g;
|
|
107
|
+
const b = colour.b;
|
|
108
|
+
out.r = clamp01(matrix[offset] * r + matrix[offset + 1] * g + matrix[offset + 2] * b);
|
|
109
|
+
out.g = clamp01(matrix[offset + 3] * r + matrix[offset + 4] * g + matrix[offset + 5] * b);
|
|
110
|
+
out.b = clamp01(matrix[offset + 6] * r + matrix[offset + 7] * g + matrix[offset + 8] * b);
|
|
111
|
+
out.a = colour.a;
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The full per-fragment colour law, on the CPU: a PREMULTIPLIED texel, its
|
|
116
|
+
* optional colour matrix, and the quad's PREMULTIPLIED tint, in the order the
|
|
117
|
+
* shader applies them (un-premultiply, transform, re-premultiply, modulate).
|
|
118
|
+
*
|
|
119
|
+
* This exists so a pixel test can state the number it expects from the same
|
|
120
|
+
* expression the GPU evaluates rather than from a second, hand-derived one.
|
|
121
|
+
*/
|
|
122
|
+
function shadeQuadPixel(texel, matrix, matrixOffset, tint, out) {
|
|
123
|
+
if (matrix) {
|
|
124
|
+
unpremultiply(texel, out);
|
|
125
|
+
applyColorMatrix01(out, matrix, matrixOffset, out);
|
|
126
|
+
out.r *= out.a;
|
|
127
|
+
out.g *= out.a;
|
|
128
|
+
out.b *= out.a;
|
|
129
|
+
} else {
|
|
130
|
+
out.r = texel.r;
|
|
131
|
+
out.g = texel.g;
|
|
132
|
+
out.b = texel.b;
|
|
133
|
+
out.a = texel.a;
|
|
134
|
+
}
|
|
135
|
+
return modulatePremultiplied(out, tint, out);
|
|
136
|
+
}
|
|
137
|
+
/** True when the 9 floats at `offset` are the identity, i.e. a no-op slot. */
|
|
138
|
+
function isIdentityColorMatrix(matrix, offset = 0) {
|
|
139
|
+
for (let i = 0; i < 9; i += 1) if (matrix[offset + i] !== IDENTITY_COLOR_MATRIX[i]) return false;
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* True when the 9 floats at `a`/`b` are equal — the batcher's matrix-table
|
|
144
|
+
* dedupe. EXACT equality, deliberately: the table's job is to notice that many
|
|
145
|
+
* cards carry the same computed tint, and an epsilon would merge two tints a
|
|
146
|
+
* scene meant to differ. The corollary is that a `Float32Array` and a plain
|
|
147
|
+
* `number[]` holding "the same" value do not match (`0.3` and its f32 round-trip
|
|
148
|
+
* are different numbers), so a caller that wants dedupe should keep one storage
|
|
149
|
+
* width — which the draw-list and this package both do.
|
|
150
|
+
*/
|
|
151
|
+
function colorMatricesEqual(a, aOffset, b, bOffset) {
|
|
152
|
+
for (let i = 0; i < 9; i += 1) if (a[aOffset + i] !== b[bOffset + i]) return false;
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/draw-list.ts
|
|
157
|
+
/**
|
|
158
|
+
* The draw-list IR for a Godot 2D scene: a flat, ordered recording of what one
|
|
159
|
+
* frame paints, produced by walking `CanvasItem`s in draw order and consumed by
|
|
160
|
+
* a GPU executor (a later wave). It is deliberately dumb — no scene concepts, no
|
|
161
|
+
* nodes, no styles, just quads, indexed textured meshes, nine-patches,
|
|
162
|
+
* polylines and clip pushes/pops in
|
|
163
|
+
* the order they must hit the framebuffer.
|
|
164
|
+
*
|
|
165
|
+
* Storage is a set of pooled parallel typed arrays, not an array of command
|
|
166
|
+
* objects:
|
|
167
|
+
*
|
|
168
|
+
* - `kinds` / `floatOffsets` / `intOffsets` are one entry per command,
|
|
169
|
+
* - `floats` and `ints` are arenas that every command's numeric payload is
|
|
170
|
+
* appended into (a quad writes 16 floats + 3 ints, a polyline writes a
|
|
171
|
+
* 5-float header plus 2 floats per point, …),
|
|
172
|
+
* - `colorMatrices` is a side arena for the rare 3x3 color transform,
|
|
173
|
+
* - `textures` is the ONLY object side-array — a texture handle cannot live in
|
|
174
|
+
* a typed array.
|
|
175
|
+
*
|
|
176
|
+
* That layout exists so a frame costs zero garbage: `reset()` rewinds the write
|
|
177
|
+
* cursors and the same buffers are refilled next frame, and reading a command
|
|
178
|
+
* back fills a caller-owned view instead of allocating one. Arrays grow on
|
|
179
|
+
* demand (capacity doubling) and never shrink.
|
|
180
|
+
*
|
|
181
|
+
* All geometry is in DESIGN space (the scene's own coordinate system); mapping
|
|
182
|
+
* design space to device pixels is the executor's job.
|
|
183
|
+
*/
|
|
184
|
+
/** A textured/solid rectangle: the workhorse command. */
|
|
185
|
+
const DRAW_QUAD = 0;
|
|
186
|
+
/** A 9-sliced rectangle; the executor expands it to up to 9 quads. */
|
|
187
|
+
const DRAW_NINE_PATCH = 1;
|
|
188
|
+
/** A flattened, constant-width line strip. */
|
|
189
|
+
const DRAW_POLYLINE = 2;
|
|
190
|
+
/** Push a scissor/clip rect; every later command is clipped until the pop. */
|
|
191
|
+
const DRAW_CLIP_PUSH = 3;
|
|
192
|
+
/** Pop the most recent clip rect. */
|
|
193
|
+
const DRAW_CLIP_POP = 4;
|
|
194
|
+
/**
|
|
195
|
+
* A run of glyphs from one face, at one size, in one colour.
|
|
196
|
+
*
|
|
197
|
+
* The ONLY command that is not reducible to a textured quad, which is why it
|
|
198
|
+
* exists as its own kind rather than as sugar over `pushQuad`. Its glyphs are
|
|
199
|
+
* outlines evaluated per fragment (see `@godot-scene-web/hb-gpu`), so they carry
|
|
200
|
+
* an atlas slot id instead of a source rect and stay crisp under rotation and
|
|
201
|
+
* scale — the whole reason for the kind. An executor with no glyph pass
|
|
202
|
+
* installed skips it.
|
|
203
|
+
*/
|
|
204
|
+
const DRAW_GLYPHS = 5;
|
|
205
|
+
/** An arbitrary triangle list with one texture and per-mesh premultiplied tint. */
|
|
206
|
+
const DRAW_TEXTURED_MESH = 6;
|
|
207
|
+
/** A screen-dependent pass executed at this exact painter position. */
|
|
208
|
+
const DRAW_SCREEN_EFFECT = 7;
|
|
209
|
+
/** A caller-owned GPU pass executed directly into the current framebuffer. */
|
|
210
|
+
const DRAW_EXTERNAL_EFFECT = 8;
|
|
211
|
+
/** Indexed by {@link DrawCommandKind}; for debugging and test assertions. */
|
|
212
|
+
const DRAW_COMMAND_NAMES = [
|
|
213
|
+
"quad",
|
|
214
|
+
"ninePatch",
|
|
215
|
+
"polyline",
|
|
216
|
+
"clipPush",
|
|
217
|
+
"clipPop",
|
|
218
|
+
"glyphs",
|
|
219
|
+
"texturedMesh",
|
|
220
|
+
"screenEffect",
|
|
221
|
+
"externalEffect"
|
|
222
|
+
];
|
|
223
|
+
/** Godot `CanvasItemMaterial.BLEND_MODE_MIX`: normal alpha compositing. */
|
|
224
|
+
const BLEND_MIX = 0;
|
|
225
|
+
/** Godot `BLEND_MODE_ADD`. */
|
|
226
|
+
const BLEND_ADD = 1;
|
|
227
|
+
/** Godot `BLEND_MODE_SUB`. */
|
|
228
|
+
const BLEND_SUB = 2;
|
|
229
|
+
/** Godot `BLEND_MODE_MUL`. */
|
|
230
|
+
const BLEND_MUL = 3;
|
|
231
|
+
/** Bit in a command's packed flags int: mirror the source rect horizontally. */
|
|
232
|
+
const FLIP_H = 1;
|
|
233
|
+
/** Bit in a command's packed flags int: mirror the source rect vertically. */
|
|
234
|
+
const FLIP_V = 2;
|
|
235
|
+
function createDrawListPatchView(capacity = 16) {
|
|
236
|
+
const indices = [];
|
|
237
|
+
let marks = new Int32Array(Math.max(1, capacity));
|
|
238
|
+
let generation = 0;
|
|
239
|
+
let revision = 0;
|
|
240
|
+
let overflowed = false;
|
|
241
|
+
const view = {
|
|
242
|
+
get indices() {
|
|
243
|
+
return indices;
|
|
244
|
+
},
|
|
245
|
+
get revision() {
|
|
246
|
+
return revision;
|
|
247
|
+
},
|
|
248
|
+
get overflowed() {
|
|
249
|
+
return overflowed;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
Object.assign(view, {
|
|
253
|
+
begin(nextRevision) {
|
|
254
|
+
indices.length = 0;
|
|
255
|
+
generation += 1;
|
|
256
|
+
if (generation === 2147483647) {
|
|
257
|
+
marks.fill(0);
|
|
258
|
+
generation = 1;
|
|
259
|
+
}
|
|
260
|
+
revision = nextRevision;
|
|
261
|
+
overflowed = false;
|
|
262
|
+
},
|
|
263
|
+
overflow() {
|
|
264
|
+
overflowed = true;
|
|
265
|
+
},
|
|
266
|
+
add(index) {
|
|
267
|
+
if (index >= marks.length) {
|
|
268
|
+
let length = marks.length;
|
|
269
|
+
while (length <= index) length *= 2;
|
|
270
|
+
const next = new Int32Array(length);
|
|
271
|
+
next.set(marks);
|
|
272
|
+
marks = next;
|
|
273
|
+
}
|
|
274
|
+
if (marks[index] === generation) return;
|
|
275
|
+
marks[index] = generation;
|
|
276
|
+
indices.push(index);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
return view;
|
|
280
|
+
}
|
|
281
|
+
const QUAD_FLOATS = 16;
|
|
282
|
+
const QUAD_INTS = 3;
|
|
283
|
+
const NINE_PATCH_FLOATS = 20;
|
|
284
|
+
const NINE_PATCH_INTS = QUAD_INTS;
|
|
285
|
+
const POLYLINE_HEADER_FLOATS = 5;
|
|
286
|
+
const POLYLINE_INTS = 1;
|
|
287
|
+
const TEXTURED_MESH_HEADER_FLOATS = 10;
|
|
288
|
+
const TEXTURED_MESH_HEADER_INTS = 3;
|
|
289
|
+
const GLYPHS_SPREAD_OFFSET = 11;
|
|
290
|
+
const GLYPHS_INK_X_OFFSET = 12;
|
|
291
|
+
const GLYPHS_INK_Y_OFFSET = 13;
|
|
292
|
+
const GLYPHS_INK_WIDTH_OFFSET = 14;
|
|
293
|
+
const GLYPHS_INK_HEIGHT_OFFSET = 15;
|
|
294
|
+
const GLYPHS_INK_OUTSET_OFFSET = 16;
|
|
295
|
+
const GLYPHS_HEADER_FLOATS = 17;
|
|
296
|
+
const GLYPHS_HEADER_INTS = 1;
|
|
297
|
+
const CLIP_FLOATS = 6;
|
|
298
|
+
const COLOR_MATRIX_FLOATS$1 = 9;
|
|
299
|
+
const DEFAULT_COMMAND_CAPACITY = 256;
|
|
300
|
+
const DEFAULT_FLOAT_CAPACITY = 256 * QUAD_FLOATS;
|
|
301
|
+
const DEFAULT_INT_CAPACITY = 256 * QUAD_INTS;
|
|
302
|
+
const DEFAULT_COLOR_MATRIX_CAPACITY = 8;
|
|
303
|
+
const IDENTITY_MATRIX_2D = [
|
|
304
|
+
1,
|
|
305
|
+
0,
|
|
306
|
+
0,
|
|
307
|
+
1,
|
|
308
|
+
0,
|
|
309
|
+
0
|
|
310
|
+
];
|
|
311
|
+
const IDENTITY_COLOR_MATRIX$1 = [
|
|
312
|
+
1,
|
|
313
|
+
0,
|
|
314
|
+
0,
|
|
315
|
+
0,
|
|
316
|
+
1,
|
|
317
|
+
0,
|
|
318
|
+
0,
|
|
319
|
+
0,
|
|
320
|
+
1
|
|
321
|
+
];
|
|
322
|
+
/** A fresh quad view: identity transform, opaque white, mix blend, no matrix. */
|
|
323
|
+
function createQuadView() {
|
|
324
|
+
return {
|
|
325
|
+
m: Float32Array.from(IDENTITY_MATRIX_2D),
|
|
326
|
+
w: 0,
|
|
327
|
+
h: 0,
|
|
328
|
+
srcX: 0,
|
|
329
|
+
srcY: 0,
|
|
330
|
+
srcW: 0,
|
|
331
|
+
srcH: 0,
|
|
332
|
+
r: 1,
|
|
333
|
+
g: 1,
|
|
334
|
+
b: 1,
|
|
335
|
+
a: 1,
|
|
336
|
+
blend: 0,
|
|
337
|
+
flipH: false,
|
|
338
|
+
flipV: false,
|
|
339
|
+
hasColorMatrix: false,
|
|
340
|
+
colorMatrix: Float32Array.from(IDENTITY_COLOR_MATRIX$1)
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/** A fresh nine-patch view: a quad view with zero margins. */
|
|
344
|
+
function createNinePatchView() {
|
|
345
|
+
return {
|
|
346
|
+
...createQuadView(),
|
|
347
|
+
marginLeft: 0,
|
|
348
|
+
marginTop: 0,
|
|
349
|
+
marginRight: 0,
|
|
350
|
+
marginBottom: 0
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/** A fresh polyline view with room for `pointCapacity` points. */
|
|
354
|
+
function createPolylineView(pointCapacity = 8) {
|
|
355
|
+
return {
|
|
356
|
+
points: new Float32Array(Math.max(1, pointCapacity) * 2),
|
|
357
|
+
pointCount: 0,
|
|
358
|
+
width: 1,
|
|
359
|
+
r: 1,
|
|
360
|
+
g: 1,
|
|
361
|
+
b: 1,
|
|
362
|
+
a: 1
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/** A reusable textured mesh view with room for the requested topology. */
|
|
366
|
+
function createTexturedMeshView(vertexCapacity = 4, indexCapacity = 6) {
|
|
367
|
+
const vertices = Math.max(1, Math.floor(vertexCapacity));
|
|
368
|
+
return {
|
|
369
|
+
m: Float32Array.from(IDENTITY_MATRIX_2D),
|
|
370
|
+
positions: new Float32Array(vertices * 2),
|
|
371
|
+
uvs: new Float32Array(vertices * 2),
|
|
372
|
+
vertexCount: 0,
|
|
373
|
+
indices: new Uint32Array(Math.max(1, Math.floor(indexCapacity))),
|
|
374
|
+
indexCount: 0,
|
|
375
|
+
r: 1,
|
|
376
|
+
g: 1,
|
|
377
|
+
b: 1,
|
|
378
|
+
a: 1,
|
|
379
|
+
blend: 0
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
/** A fresh glyph-run view with room for `glyphCapacity` glyphs. */
|
|
383
|
+
function createGlyphsView(glyphCapacity = 32) {
|
|
384
|
+
const capacity = Math.max(1, glyphCapacity);
|
|
385
|
+
return {
|
|
386
|
+
m: Float32Array.from(IDENTITY_MATRIX_2D),
|
|
387
|
+
pixelsPerEm: 16,
|
|
388
|
+
r: 1,
|
|
389
|
+
g: 1,
|
|
390
|
+
b: 1,
|
|
391
|
+
a: 1,
|
|
392
|
+
slots: new Int32Array(capacity),
|
|
393
|
+
positions: new Float32Array(capacity * 2),
|
|
394
|
+
glyphCount: 0,
|
|
395
|
+
spreadPx: 0,
|
|
396
|
+
localInkX: NaN,
|
|
397
|
+
localInkY: NaN,
|
|
398
|
+
localInkWidth: NaN,
|
|
399
|
+
localInkHeight: NaN,
|
|
400
|
+
localInkOutset: NaN
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
/** A fresh clip-rect view: empty rect, square corners, no outset. */
|
|
404
|
+
function createClipRectView() {
|
|
405
|
+
return {
|
|
406
|
+
x: 0,
|
|
407
|
+
y: 0,
|
|
408
|
+
w: 0,
|
|
409
|
+
h: 0,
|
|
410
|
+
cornerRadius: 0,
|
|
411
|
+
outsetX: 0
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Copy an `html` {@link ColorMatrix} into a quad/nine-patch view and arm it.
|
|
416
|
+
* Passing `null` disarms the view's matrix (leaving its contents alone), which
|
|
417
|
+
* is the shape most callers have: a computed transform that is usually absent.
|
|
418
|
+
*/
|
|
419
|
+
function setViewColorMatrix(view, matrix) {
|
|
420
|
+
if (!matrix) {
|
|
421
|
+
view.hasColorMatrix = false;
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const rows = matrix.rows;
|
|
425
|
+
const out = view.colorMatrix;
|
|
426
|
+
out[0] = rows[0][0];
|
|
427
|
+
out[1] = rows[0][1];
|
|
428
|
+
out[2] = rows[0][2];
|
|
429
|
+
out[3] = rows[1][0];
|
|
430
|
+
out[4] = rows[1][1];
|
|
431
|
+
out[5] = rows[1][2];
|
|
432
|
+
out[6] = rows[2][0];
|
|
433
|
+
out[7] = rows[2][1];
|
|
434
|
+
out[8] = rows[2][2];
|
|
435
|
+
view.hasColorMatrix = true;
|
|
436
|
+
}
|
|
437
|
+
function grownFloats(current, needed) {
|
|
438
|
+
let capacity = Math.max(1, current.length);
|
|
439
|
+
while (capacity < needed) capacity *= 2;
|
|
440
|
+
const next = new Float32Array(capacity);
|
|
441
|
+
next.set(current);
|
|
442
|
+
return next;
|
|
443
|
+
}
|
|
444
|
+
/** Preserve an omitted optional producer field as an unknown IR value. */
|
|
445
|
+
function finiteOrNaN(value) {
|
|
446
|
+
return typeof value === "number" && Number.isFinite(value) ? value : NaN;
|
|
447
|
+
}
|
|
448
|
+
function grownInts(current, needed) {
|
|
449
|
+
let capacity = Math.max(1, current.length);
|
|
450
|
+
while (capacity < needed) capacity *= 2;
|
|
451
|
+
const next = new Int32Array(capacity);
|
|
452
|
+
next.set(current);
|
|
453
|
+
return next;
|
|
454
|
+
}
|
|
455
|
+
function grownObjects(current, needed) {
|
|
456
|
+
let capacity = Math.max(1, current.length);
|
|
457
|
+
while (capacity < needed) capacity *= 2;
|
|
458
|
+
const next = new Array(capacity).fill(null);
|
|
459
|
+
for (let index = 0; index < current.length; index += 1) next[index] = current[index];
|
|
460
|
+
return next;
|
|
461
|
+
}
|
|
462
|
+
const fragmentStorage = /* @__PURE__ */ new WeakMap();
|
|
463
|
+
function requireFragmentStorage(fragment) {
|
|
464
|
+
const storage = fragmentStorage.get(fragment);
|
|
465
|
+
if (!storage) throw new TypeError("draw-list appendFragment() needs a fragment created by createDrawListFragment()");
|
|
466
|
+
return storage;
|
|
467
|
+
}
|
|
468
|
+
function fillFragmentPayloadLengths(list, index, out) {
|
|
469
|
+
const kind = list.kindAt(index);
|
|
470
|
+
const intAt = list.intOffsetAt(index);
|
|
471
|
+
const requireInt = (offset, label) => {
|
|
472
|
+
const value = list.ints[intAt + offset];
|
|
473
|
+
if (!Number.isInteger(value) || value < 0) throw new RangeError(`draw-list ${label} at command ${index} is malformed`);
|
|
474
|
+
return value;
|
|
475
|
+
};
|
|
476
|
+
switch (kind) {
|
|
477
|
+
case 0:
|
|
478
|
+
out.floats = QUAD_FLOATS;
|
|
479
|
+
out.ints = QUAD_INTS;
|
|
480
|
+
return;
|
|
481
|
+
case 1:
|
|
482
|
+
out.floats = NINE_PATCH_FLOATS;
|
|
483
|
+
out.ints = NINE_PATCH_INTS;
|
|
484
|
+
return;
|
|
485
|
+
case 2:
|
|
486
|
+
out.floats = POLYLINE_HEADER_FLOATS + requireInt(0, "polyline point count") * 2;
|
|
487
|
+
out.ints = POLYLINE_INTS;
|
|
488
|
+
return;
|
|
489
|
+
case 3:
|
|
490
|
+
out.floats = CLIP_FLOATS;
|
|
491
|
+
out.ints = 0;
|
|
492
|
+
return;
|
|
493
|
+
case 4:
|
|
494
|
+
case 7:
|
|
495
|
+
case 8:
|
|
496
|
+
out.floats = 0;
|
|
497
|
+
out.ints = 0;
|
|
498
|
+
return;
|
|
499
|
+
case 5: {
|
|
500
|
+
const glyphs = requireInt(0, "glyph count");
|
|
501
|
+
out.floats = GLYPHS_HEADER_FLOATS + glyphs * 2;
|
|
502
|
+
out.ints = GLYPHS_HEADER_INTS + glyphs;
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
case 6: {
|
|
506
|
+
const vertices = requireInt(0, "textured mesh vertex count");
|
|
507
|
+
const indexes = requireInt(1, "textured mesh index count");
|
|
508
|
+
out.floats = TEXTURED_MESH_HEADER_FLOATS + vertices * 4;
|
|
509
|
+
out.ints = TEXTURED_MESH_HEADER_INTS + indexes;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
default: throw new RangeError(`draw-list command ${index} has an unknown kind ${kind}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function fillFragmentStoragePayloadLengths(storage, index, out) {
|
|
516
|
+
if (!Number.isInteger(index) || index < 0 || index >= storage.count) throw new RangeError(`draw-list fragment index ${index} out of range (count ${storage.count})`);
|
|
517
|
+
const kind = storage.kinds[index];
|
|
518
|
+
const intAt = storage.intOffsets[index];
|
|
519
|
+
switch (kind) {
|
|
520
|
+
case 0:
|
|
521
|
+
out.floats = QUAD_FLOATS;
|
|
522
|
+
out.ints = QUAD_INTS;
|
|
523
|
+
return;
|
|
524
|
+
case 1:
|
|
525
|
+
out.floats = NINE_PATCH_FLOATS;
|
|
526
|
+
out.ints = NINE_PATCH_INTS;
|
|
527
|
+
return;
|
|
528
|
+
case 2: {
|
|
529
|
+
const points = storage.ints[intAt];
|
|
530
|
+
if (!Number.isInteger(points) || points < 0) throw new RangeError(`draw-list fragment polyline point count at command ${index} is malformed`);
|
|
531
|
+
out.floats = POLYLINE_HEADER_FLOATS + points * 2;
|
|
532
|
+
out.ints = POLYLINE_INTS;
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
case 3:
|
|
536
|
+
out.floats = CLIP_FLOATS;
|
|
537
|
+
out.ints = 0;
|
|
538
|
+
return;
|
|
539
|
+
case 4:
|
|
540
|
+
case 7:
|
|
541
|
+
case 8:
|
|
542
|
+
out.floats = 0;
|
|
543
|
+
out.ints = 0;
|
|
544
|
+
return;
|
|
545
|
+
case 5: {
|
|
546
|
+
const glyphs = storage.ints[intAt];
|
|
547
|
+
if (!Number.isInteger(glyphs) || glyphs < 0) throw new RangeError(`draw-list fragment glyph count at command ${index} is malformed`);
|
|
548
|
+
out.floats = GLYPHS_HEADER_FLOATS + glyphs * 2;
|
|
549
|
+
out.ints = GLYPHS_HEADER_INTS + glyphs;
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
case 6: {
|
|
553
|
+
const vertices = storage.ints[intAt];
|
|
554
|
+
const indexes = storage.ints[intAt + 1];
|
|
555
|
+
if (!Number.isInteger(vertices) || vertices < 0) throw new RangeError(`draw-list fragment textured mesh vertex count at command ${index} is malformed`);
|
|
556
|
+
if (!Number.isInteger(indexes) || indexes < 0) throw new RangeError(`draw-list fragment textured mesh index count at command ${index} is malformed`);
|
|
557
|
+
out.floats = TEXTURED_MESH_HEADER_FLOATS + vertices * 4;
|
|
558
|
+
out.ints = TEXTURED_MESH_HEADER_INTS + indexes;
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
default: throw new RangeError(`draw-list fragment command ${index} has an unknown kind ${kind}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Create reusable storage for a retained command fragment. The fragment holds
|
|
566
|
+
* only draw-list data and opaque caller references; it never creates or owns a
|
|
567
|
+
* DOM node, a WebGL object, or an executor.
|
|
568
|
+
*/
|
|
569
|
+
function createDrawListFragment(options = {}) {
|
|
570
|
+
const commandCapacity = Math.max(1, options.commandCapacity ?? DEFAULT_COMMAND_CAPACITY);
|
|
571
|
+
const storage = {
|
|
572
|
+
kinds: new Int32Array(commandCapacity),
|
|
573
|
+
floatOffsets: new Int32Array(commandCapacity),
|
|
574
|
+
intOffsets: new Int32Array(commandCapacity),
|
|
575
|
+
textures: new Array(commandCapacity).fill(null),
|
|
576
|
+
screenEffects: new Array(commandCapacity).fill(null),
|
|
577
|
+
externalEffects: new Array(commandCapacity).fill(null),
|
|
578
|
+
floats: new Float32Array(Math.max(1, options.floatCapacity ?? DEFAULT_FLOAT_CAPACITY)),
|
|
579
|
+
ints: new Int32Array(Math.max(1, options.intCapacity ?? DEFAULT_INT_CAPACITY)),
|
|
580
|
+
colorMatrices: new Float32Array(Math.max(1, options.colorMatrixCapacity ?? DEFAULT_COLOR_MATRIX_CAPACITY) * COLOR_MATRIX_FLOATS$1),
|
|
581
|
+
sourceMatrixIndexes: new Int32Array(Math.max(1, options.colorMatrixCapacity ?? DEFAULT_COLOR_MATRIX_CAPACITY)),
|
|
582
|
+
count: 0,
|
|
583
|
+
floatLength: 0,
|
|
584
|
+
intLength: 0,
|
|
585
|
+
colorMatrixCount: 0,
|
|
586
|
+
maxClipDepth: 0
|
|
587
|
+
};
|
|
588
|
+
function reset() {
|
|
589
|
+
for (let index = 0; index < storage.count; index += 1) {
|
|
590
|
+
storage.textures[index] = null;
|
|
591
|
+
storage.screenEffects[index] = null;
|
|
592
|
+
storage.externalEffects[index] = null;
|
|
593
|
+
}
|
|
594
|
+
storage.count = 0;
|
|
595
|
+
storage.floatLength = 0;
|
|
596
|
+
storage.intLength = 0;
|
|
597
|
+
storage.colorMatrixCount = 0;
|
|
598
|
+
storage.maxClipDepth = 0;
|
|
599
|
+
}
|
|
600
|
+
function ensureCommand(needed) {
|
|
601
|
+
if (needed <= storage.kinds.length) return;
|
|
602
|
+
const capacity = storage.kinds.length * 2;
|
|
603
|
+
const nextKinds = new Int32Array(capacity);
|
|
604
|
+
nextKinds.set(storage.kinds);
|
|
605
|
+
storage.kinds = nextKinds;
|
|
606
|
+
const nextFloatOffsets = new Int32Array(capacity);
|
|
607
|
+
nextFloatOffsets.set(storage.floatOffsets);
|
|
608
|
+
storage.floatOffsets = nextFloatOffsets;
|
|
609
|
+
const nextIntOffsets = new Int32Array(capacity);
|
|
610
|
+
nextIntOffsets.set(storage.intOffsets);
|
|
611
|
+
storage.intOffsets = nextIntOffsets;
|
|
612
|
+
storage.textures = grownObjects(storage.textures, needed);
|
|
613
|
+
storage.screenEffects = grownObjects(storage.screenEffects, needed);
|
|
614
|
+
storage.externalEffects = grownObjects(storage.externalEffects, needed);
|
|
615
|
+
}
|
|
616
|
+
function matrixIndexFor(source, sourceIndex) {
|
|
617
|
+
for (let index = 0; index < storage.colorMatrixCount; index += 1) if (storage.sourceMatrixIndexes[index] === sourceIndex) return index;
|
|
618
|
+
const sourceAt = sourceIndex * COLOR_MATRIX_FLOATS$1;
|
|
619
|
+
if (sourceIndex < 0 || sourceAt + COLOR_MATRIX_FLOATS$1 > source.colorMatrices.length) throw new RangeError(`draw-list color matrix ${sourceIndex} is malformed`);
|
|
620
|
+
const nextCount = storage.colorMatrixCount + 1;
|
|
621
|
+
if (nextCount * COLOR_MATRIX_FLOATS$1 > storage.colorMatrices.length) storage.colorMatrices = grownFloats(storage.colorMatrices, nextCount * COLOR_MATRIX_FLOATS$1);
|
|
622
|
+
if (nextCount > storage.sourceMatrixIndexes.length) storage.sourceMatrixIndexes = grownInts(storage.sourceMatrixIndexes, nextCount);
|
|
623
|
+
const target = storage.colorMatrixCount;
|
|
624
|
+
const targetAt = target * COLOR_MATRIX_FLOATS$1;
|
|
625
|
+
for (let offset = 0; offset < COLOR_MATRIX_FLOATS$1; offset += 1) storage.colorMatrices[targetAt + offset] = source.colorMatrices[sourceAt + offset];
|
|
626
|
+
storage.sourceMatrixIndexes[target] = sourceIndex;
|
|
627
|
+
storage.colorMatrixCount = nextCount;
|
|
628
|
+
return target;
|
|
629
|
+
}
|
|
630
|
+
function capture(source, start, end = source.count) {
|
|
631
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > source.count) throw new RangeError(`draw-list fragment range [${start}, ${end}) is outside count ${source.count}`);
|
|
632
|
+
const lengths = {
|
|
633
|
+
floats: 0,
|
|
634
|
+
ints: 0
|
|
635
|
+
};
|
|
636
|
+
let depth = 0;
|
|
637
|
+
let maxDepth = 0;
|
|
638
|
+
for (let index = start; index < end; index += 1) {
|
|
639
|
+
fillFragmentPayloadLengths(source, index, lengths);
|
|
640
|
+
const floatAt = source.floatOffsetAt(index);
|
|
641
|
+
const intAt = source.intOffsetAt(index);
|
|
642
|
+
if (floatAt < 0 || intAt < 0 || floatAt + lengths.floats > source.floats.length || intAt + lengths.ints > source.ints.length) throw new RangeError(`draw-list command ${index} has an invalid arena range`);
|
|
643
|
+
const kind = source.kindAt(index);
|
|
644
|
+
if (kind === 0 || kind === 1) {
|
|
645
|
+
const matrix = source.ints[intAt + 2];
|
|
646
|
+
if (!Number.isInteger(matrix) || matrix < -1 || matrix >= 0 && (matrix + 1) * COLOR_MATRIX_FLOATS$1 > source.colorMatrices.length) throw new RangeError(`draw-list color matrix ${matrix} at command ${index} is malformed`);
|
|
647
|
+
}
|
|
648
|
+
if (kind === 3) {
|
|
649
|
+
depth += 1;
|
|
650
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
651
|
+
} else if (kind === 4) {
|
|
652
|
+
depth -= 1;
|
|
653
|
+
if (depth < 0) throw new RangeError(`draw-list fragment range [${start}, ${end}) pops a clip it did not push`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (depth !== 0) throw new RangeError(`draw-list fragment range [${start}, ${end}) leaves ${depth} clip(s) open`);
|
|
657
|
+
reset();
|
|
658
|
+
for (let sourceIndex = start; sourceIndex < end; sourceIndex += 1) {
|
|
659
|
+
fillFragmentPayloadLengths(source, sourceIndex, lengths);
|
|
660
|
+
const index = storage.count;
|
|
661
|
+
ensureCommand(index + 1);
|
|
662
|
+
if (storage.floatLength + lengths.floats > storage.floats.length) storage.floats = grownFloats(storage.floats, storage.floatLength + lengths.floats);
|
|
663
|
+
if (storage.intLength + lengths.ints > storage.ints.length) storage.ints = grownInts(storage.ints, storage.intLength + lengths.ints);
|
|
664
|
+
storage.kinds[index] = source.kindAt(sourceIndex);
|
|
665
|
+
storage.floatOffsets[index] = storage.floatLength;
|
|
666
|
+
storage.intOffsets[index] = storage.intLength;
|
|
667
|
+
storage.textures[index] = source.textureAt(sourceIndex);
|
|
668
|
+
storage.screenEffects[index] = source.screenEffectAt(sourceIndex);
|
|
669
|
+
storage.externalEffects[index] = source.externalEffectAt(sourceIndex);
|
|
670
|
+
const sourceFloatAt = source.floatOffsetAt(sourceIndex);
|
|
671
|
+
const sourceIntAt = source.intOffsetAt(sourceIndex);
|
|
672
|
+
for (let offset = 0; offset < lengths.floats; offset += 1) storage.floats[storage.floatLength + offset] = source.floats[sourceFloatAt + offset];
|
|
673
|
+
for (let offset = 0; offset < lengths.ints; offset += 1) storage.ints[storage.intLength + offset] = source.ints[sourceIntAt + offset];
|
|
674
|
+
const kind = storage.kinds[index];
|
|
675
|
+
if (kind === 0 || kind === 1) {
|
|
676
|
+
const matrix = storage.ints[storage.intLength + 2];
|
|
677
|
+
if (matrix >= 0) storage.ints[storage.intLength + 2] = matrixIndexFor(source, matrix);
|
|
678
|
+
}
|
|
679
|
+
storage.count += 1;
|
|
680
|
+
storage.floatLength += lengths.floats;
|
|
681
|
+
storage.intLength += lengths.ints;
|
|
682
|
+
}
|
|
683
|
+
storage.maxClipDepth = maxDepth;
|
|
684
|
+
}
|
|
685
|
+
const fragment = {
|
|
686
|
+
get count() {
|
|
687
|
+
return storage.count;
|
|
688
|
+
},
|
|
689
|
+
get clipDepth() {
|
|
690
|
+
return 0;
|
|
691
|
+
},
|
|
692
|
+
get maxClipDepth() {
|
|
693
|
+
return storage.maxClipDepth;
|
|
694
|
+
},
|
|
695
|
+
reset,
|
|
696
|
+
capture
|
|
697
|
+
};
|
|
698
|
+
fragmentStorage.set(fragment, storage);
|
|
699
|
+
return Object.freeze(fragment);
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Create an empty draw list. Capacities are only a starting point; every arena
|
|
703
|
+
* grows on demand, so a list settles at the high-water mark of the frames it
|
|
704
|
+
* has recorded and then allocates nothing.
|
|
705
|
+
*/
|
|
706
|
+
function createDrawList(options = {}) {
|
|
707
|
+
let kinds = new Int32Array(Math.max(1, options.commandCapacity ?? DEFAULT_COMMAND_CAPACITY));
|
|
708
|
+
let floatOffsets = new Int32Array(kinds.length);
|
|
709
|
+
let intOffsets = new Int32Array(kinds.length);
|
|
710
|
+
let textures = new Array(kinds.length).fill(null);
|
|
711
|
+
let screenEffects = new Array(kinds.length).fill(null);
|
|
712
|
+
let externalEffects = new Array(kinds.length).fill(null);
|
|
713
|
+
let floats = new Float32Array(Math.max(1, options.floatCapacity ?? DEFAULT_FLOAT_CAPACITY));
|
|
714
|
+
let ints = new Int32Array(Math.max(1, options.intCapacity ?? DEFAULT_INT_CAPACITY));
|
|
715
|
+
let colorMatrices = new Float32Array(Math.max(1, options.colorMatrixCapacity ?? DEFAULT_COLOR_MATRIX_CAPACITY) * COLOR_MATRIX_FLOATS$1);
|
|
716
|
+
let count = 0;
|
|
717
|
+
let floatLength = 0;
|
|
718
|
+
let intLength = 0;
|
|
719
|
+
let colorMatrixCount = 0;
|
|
720
|
+
let clipDepth = 0;
|
|
721
|
+
let maxClipDepth = 0;
|
|
722
|
+
let structuralRevision = 0;
|
|
723
|
+
let contentRevision = 0;
|
|
724
|
+
let commandRevisions = new Int32Array(kinds.length);
|
|
725
|
+
const patchJournalCapacity = Math.max(1, options.patchJournalCapacity ?? 256);
|
|
726
|
+
const patchJournalRevisions = new Int32Array(patchJournalCapacity);
|
|
727
|
+
const patchJournalIndexes = new Int32Array(patchJournalCapacity);
|
|
728
|
+
let patchJournalStart = 0;
|
|
729
|
+
let patchJournalCount = 0;
|
|
730
|
+
const fragmentAppendLengths = {
|
|
731
|
+
floats: 0,
|
|
732
|
+
ints: 0
|
|
733
|
+
};
|
|
734
|
+
const fragmentPatchDestinationView = {
|
|
735
|
+
kindAt(index) {
|
|
736
|
+
return kinds[index];
|
|
737
|
+
},
|
|
738
|
+
intOffsetAt(index) {
|
|
739
|
+
return intOffsets[index];
|
|
740
|
+
},
|
|
741
|
+
get ints() {
|
|
742
|
+
return ints;
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
function ensureCommandSlot() {
|
|
746
|
+
if (count < kinds.length) return;
|
|
747
|
+
const capacity = kinds.length * 2;
|
|
748
|
+
const nextKinds = new Int32Array(capacity);
|
|
749
|
+
nextKinds.set(kinds);
|
|
750
|
+
kinds = nextKinds;
|
|
751
|
+
const nextFloatOffsets = new Int32Array(capacity);
|
|
752
|
+
nextFloatOffsets.set(floatOffsets);
|
|
753
|
+
floatOffsets = nextFloatOffsets;
|
|
754
|
+
const nextIntOffsets = new Int32Array(capacity);
|
|
755
|
+
nextIntOffsets.set(intOffsets);
|
|
756
|
+
intOffsets = nextIntOffsets;
|
|
757
|
+
const nextRevisions = new Int32Array(capacity);
|
|
758
|
+
nextRevisions.set(commandRevisions);
|
|
759
|
+
commandRevisions = nextRevisions;
|
|
760
|
+
const nextTextures = new Array(capacity).fill(null);
|
|
761
|
+
for (let i = 0; i < textures.length; i += 1) nextTextures[i] = textures[i];
|
|
762
|
+
textures = nextTextures;
|
|
763
|
+
const nextEffects = new Array(capacity).fill(null);
|
|
764
|
+
for (let i = 0; i < screenEffects.length; i += 1) nextEffects[i] = screenEffects[i];
|
|
765
|
+
screenEffects = nextEffects;
|
|
766
|
+
const nextExternalEffects = new Array(capacity).fill(null);
|
|
767
|
+
for (let i = 0; i < externalEffects.length; i += 1) nextExternalEffects[i] = externalEffects[i];
|
|
768
|
+
externalEffects = nextExternalEffects;
|
|
769
|
+
}
|
|
770
|
+
function beginCommand(kind, floatCount, intCount, texture) {
|
|
771
|
+
ensureCommandSlot();
|
|
772
|
+
if (floatLength + floatCount > floats.length) floats = grownFloats(floats, floatLength + floatCount);
|
|
773
|
+
if (intLength + intCount > ints.length) ints = grownInts(ints, intLength + intCount);
|
|
774
|
+
const index = count;
|
|
775
|
+
kinds[index] = kind;
|
|
776
|
+
floatOffsets[index] = floatLength;
|
|
777
|
+
intOffsets[index] = intLength;
|
|
778
|
+
textures[index] = texture;
|
|
779
|
+
count += 1;
|
|
780
|
+
contentRevision += 1;
|
|
781
|
+
structuralRevision += 1;
|
|
782
|
+
commandRevisions[index] = contentRevision;
|
|
783
|
+
floatLength += floatCount;
|
|
784
|
+
intLength += intCount;
|
|
785
|
+
return index;
|
|
786
|
+
}
|
|
787
|
+
function storeColorMatrix(view) {
|
|
788
|
+
if (!view.hasColorMatrix) return -1;
|
|
789
|
+
const needed = (colorMatrixCount + 1) * COLOR_MATRIX_FLOATS$1;
|
|
790
|
+
if (needed > colorMatrices.length) colorMatrices = grownFloats(colorMatrices, needed);
|
|
791
|
+
const at = colorMatrixCount * COLOR_MATRIX_FLOATS$1;
|
|
792
|
+
colorMatrices.set(view.colorMatrix.subarray(0, COLOR_MATRIX_FLOATS$1), at);
|
|
793
|
+
colorMatrixCount += 1;
|
|
794
|
+
return colorMatrixCount - 1;
|
|
795
|
+
}
|
|
796
|
+
function storeFragmentColorMatrix(matrices, matrixIndex) {
|
|
797
|
+
const from = matrixIndex * COLOR_MATRIX_FLOATS$1;
|
|
798
|
+
if (matrixIndex < 0 || from + COLOR_MATRIX_FLOATS$1 > matrices.length) throw new RangeError(`draw-list fragment color matrix ${matrixIndex} is malformed`);
|
|
799
|
+
const needed = (colorMatrixCount + 1) * COLOR_MATRIX_FLOATS$1;
|
|
800
|
+
if (needed > colorMatrices.length) colorMatrices = grownFloats(colorMatrices, needed);
|
|
801
|
+
const target = colorMatrixCount * COLOR_MATRIX_FLOATS$1;
|
|
802
|
+
for (let offset = 0; offset < COLOR_MATRIX_FLOATS$1; offset += 1) colorMatrices[target + offset] = matrices[from + offset];
|
|
803
|
+
colorMatrixCount += 1;
|
|
804
|
+
return colorMatrixCount - 1;
|
|
805
|
+
}
|
|
806
|
+
function requireIndex(index) {
|
|
807
|
+
if (!Number.isInteger(index) || index < 0 || index >= count) throw new RangeError(`draw-list index ${index} out of range (count ${count})`);
|
|
808
|
+
}
|
|
809
|
+
function requireKind(index, kind) {
|
|
810
|
+
requireIndex(index);
|
|
811
|
+
if (kinds[index] !== kind) throw new TypeError(`draw-list command ${index} is "${DRAW_COMMAND_NAMES[kinds[index]]}", not "${DRAW_COMMAND_NAMES[kind]}"`);
|
|
812
|
+
return index;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* The guard for the patch methods: both quad kinds are accepted because a
|
|
816
|
+
* nine-patch stores a quad payload first and its margins after, so the
|
|
817
|
+
* transform and colour slots are at the same offsets in both. Refusing
|
|
818
|
+
* nine-patches would drop the feature exactly where a consumer needs it most
|
|
819
|
+
* (a dialog fading out is nine-patch frames plus quads).
|
|
820
|
+
*/
|
|
821
|
+
function requireQuadLike(index) {
|
|
822
|
+
requireIndex(index);
|
|
823
|
+
const kind = kinds[index];
|
|
824
|
+
if (kind !== 0 && kind !== 1) throw new TypeError(`draw-list command ${index} is "${DRAW_COMMAND_NAMES[kind]}", not a quad-like command ("quad" or "ninePatch")`);
|
|
825
|
+
return index;
|
|
826
|
+
}
|
|
827
|
+
function requireTexturedMesh(index) {
|
|
828
|
+
return requireKind(index, 6);
|
|
829
|
+
}
|
|
830
|
+
function markPatched(index) {
|
|
831
|
+
contentRevision += 1;
|
|
832
|
+
commandRevisions[index] = contentRevision;
|
|
833
|
+
const slot = (patchJournalStart + patchJournalCount) % patchJournalCapacity;
|
|
834
|
+
patchJournalRevisions[slot] = contentRevision;
|
|
835
|
+
patchJournalIndexes[slot] = index;
|
|
836
|
+
if (patchJournalCount < patchJournalCapacity) patchJournalCount += 1;
|
|
837
|
+
else patchJournalStart = (patchJournalStart + 1) % patchJournalCapacity;
|
|
838
|
+
}
|
|
839
|
+
function hasExpectedObjectPayload(kind, texture, screenEffect, externalEffect) {
|
|
840
|
+
switch (kind) {
|
|
841
|
+
case 0:
|
|
842
|
+
case 1:
|
|
843
|
+
case 6: return screenEffect === null && externalEffect === null;
|
|
844
|
+
case 7: return texture === null && externalEffect === null && screenEffect !== null && screenEffect.screenDependent === true && typeof screenEffect.execute === "function";
|
|
845
|
+
case 8: return texture === null && screenEffect === null && externalEffect !== null && typeof externalEffect.execute === "function";
|
|
846
|
+
default: return texture === null && screenEffect === null && externalEffect === null;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Validate whether a captured range can overwrite the existing slots without
|
|
851
|
+
* changing any command layout. This runs to completion before a byte or
|
|
852
|
+
* reference is touched, which is the atomicity boundary for patchFragment.
|
|
853
|
+
*/
|
|
854
|
+
function canPatchFragment(start, captured) {
|
|
855
|
+
if (!Number.isInteger(start) || start < 0 || start > count || captured.count > count - start) return false;
|
|
856
|
+
if (captured.count === 0) return true;
|
|
857
|
+
const destinationMatrices = /* @__PURE__ */ new Map();
|
|
858
|
+
let sourceClipDepth = 0;
|
|
859
|
+
let destinationClipDepth = 0;
|
|
860
|
+
try {
|
|
861
|
+
for (let sourceIndex = 0; sourceIndex < captured.count; sourceIndex += 1) {
|
|
862
|
+
const destinationIndex = start + sourceIndex;
|
|
863
|
+
const sourceKind = captured.kinds[sourceIndex];
|
|
864
|
+
if (kinds[destinationIndex] !== sourceKind) return false;
|
|
865
|
+
const sourceFloatAt = captured.floatOffsets[sourceIndex];
|
|
866
|
+
const sourceIntAt = captured.intOffsets[sourceIndex];
|
|
867
|
+
const destinationFloatAt = floatOffsets[destinationIndex];
|
|
868
|
+
const destinationIntAt = intOffsets[destinationIndex];
|
|
869
|
+
fillFragmentStoragePayloadLengths(captured, sourceIndex, fragmentAppendLengths);
|
|
870
|
+
const sourceFloats = fragmentAppendLengths.floats;
|
|
871
|
+
const sourceInts = fragmentAppendLengths.ints;
|
|
872
|
+
if (sourceFloatAt < 0 || sourceIntAt < 0 || sourceFloatAt + sourceFloats > captured.floatLength || sourceIntAt + sourceInts > captured.intLength || destinationFloatAt < 0 || destinationIntAt < 0 || destinationFloatAt + sourceFloats > floatLength || destinationIntAt + sourceInts > intLength) return false;
|
|
873
|
+
fillFragmentPayloadLengths(fragmentPatchDestinationView, destinationIndex, fragmentAppendLengths);
|
|
874
|
+
if (fragmentAppendLengths.floats !== sourceFloats || fragmentAppendLengths.ints !== sourceInts) return false;
|
|
875
|
+
if (!hasExpectedObjectPayload(sourceKind, captured.textures[sourceIndex], captured.screenEffects[sourceIndex], captured.externalEffects[sourceIndex]) || !hasExpectedObjectPayload(sourceKind, textures[destinationIndex], screenEffects[destinationIndex], externalEffects[destinationIndex])) return false;
|
|
876
|
+
if (sourceKind === 0 || sourceKind === 1) {
|
|
877
|
+
const sourceMatrix = captured.ints[sourceIntAt + 2];
|
|
878
|
+
const destinationMatrix = ints[destinationIntAt + 2];
|
|
879
|
+
const sourceHasMatrix = sourceMatrix >= 0;
|
|
880
|
+
if (sourceHasMatrix !== destinationMatrix >= 0) return false;
|
|
881
|
+
if (sourceHasMatrix) {
|
|
882
|
+
if (!Number.isInteger(sourceMatrix) || !Number.isInteger(destinationMatrix) || sourceMatrix >= captured.colorMatrixCount || destinationMatrix >= colorMatrixCount || (sourceMatrix + 1) * COLOR_MATRIX_FLOATS$1 > captured.colorMatrices.length || (destinationMatrix + 1) * COLOR_MATRIX_FLOATS$1 > colorMatrices.length) return false;
|
|
883
|
+
const previousSource = destinationMatrices.get(destinationMatrix);
|
|
884
|
+
if (previousSource !== void 0 && previousSource !== sourceMatrix) return false;
|
|
885
|
+
destinationMatrices.set(destinationMatrix, sourceMatrix);
|
|
886
|
+
} else if (sourceMatrix !== -1 || destinationMatrix !== -1) return false;
|
|
887
|
+
}
|
|
888
|
+
if (sourceKind === 3) {
|
|
889
|
+
sourceClipDepth += 1;
|
|
890
|
+
destinationClipDepth += 1;
|
|
891
|
+
} else if (sourceKind === 4) {
|
|
892
|
+
sourceClipDepth -= 1;
|
|
893
|
+
destinationClipDepth -= 1;
|
|
894
|
+
if (sourceClipDepth < 0 || destinationClipDepth < 0) return false;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
} catch {
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
if (sourceClipDepth !== 0 || destinationClipDepth !== 0) return false;
|
|
901
|
+
if (destinationMatrices.size > 0) for (let index = 0; index < count; index += 1) {
|
|
902
|
+
if (index >= start && index < start + captured.count) continue;
|
|
903
|
+
const kind = kinds[index];
|
|
904
|
+
if (kind !== 0 && kind !== 1) continue;
|
|
905
|
+
if (destinationMatrices.has(ints[intOffsets[index] + 2])) return false;
|
|
906
|
+
}
|
|
907
|
+
return true;
|
|
908
|
+
}
|
|
909
|
+
function applyFragmentPatch(start, captured) {
|
|
910
|
+
for (let sourceIndex = 0; sourceIndex < captured.count; sourceIndex += 1) {
|
|
911
|
+
const destinationIndex = start + sourceIndex;
|
|
912
|
+
const kind = captured.kinds[sourceIndex];
|
|
913
|
+
const sourceFloatAt = captured.floatOffsets[sourceIndex];
|
|
914
|
+
const sourceIntAt = captured.intOffsets[sourceIndex];
|
|
915
|
+
const destinationFloatAt = floatOffsets[destinationIndex];
|
|
916
|
+
const destinationIntAt = intOffsets[destinationIndex];
|
|
917
|
+
const destinationMatrix = kind === 0 || kind === 1 ? ints[destinationIntAt + 2] : -1;
|
|
918
|
+
fillFragmentStoragePayloadLengths(captured, sourceIndex, fragmentAppendLengths);
|
|
919
|
+
floats.set(captured.floats.subarray(sourceFloatAt, sourceFloatAt + fragmentAppendLengths.floats), destinationFloatAt);
|
|
920
|
+
ints.set(captured.ints.subarray(sourceIntAt, sourceIntAt + fragmentAppendLengths.ints), destinationIntAt);
|
|
921
|
+
textures[destinationIndex] = captured.textures[sourceIndex];
|
|
922
|
+
screenEffects[destinationIndex] = captured.screenEffects[sourceIndex];
|
|
923
|
+
externalEffects[destinationIndex] = captured.externalEffects[sourceIndex];
|
|
924
|
+
if (kind === 0 || kind === 1) {
|
|
925
|
+
const sourceMatrix = captured.ints[sourceIntAt + 2];
|
|
926
|
+
if (sourceMatrix >= 0) {
|
|
927
|
+
const sourceMatrixAt = sourceMatrix * COLOR_MATRIX_FLOATS$1;
|
|
928
|
+
const destinationMatrixAt = destinationMatrix * COLOR_MATRIX_FLOATS$1;
|
|
929
|
+
colorMatrices.set(captured.colorMatrices.subarray(sourceMatrixAt, sourceMatrixAt + COLOR_MATRIX_FLOATS$1), destinationMatrixAt);
|
|
930
|
+
ints[destinationIntAt + 2] = destinationMatrix;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
for (let index = start; index < start + captured.count; index += 1) markPatched(index);
|
|
935
|
+
}
|
|
936
|
+
function applyFragmentPatches(patches) {
|
|
937
|
+
if (!Array.isArray(patches)) return false;
|
|
938
|
+
const validated = [];
|
|
939
|
+
let previousStart = -1;
|
|
940
|
+
let previousEnd = 0;
|
|
941
|
+
for (const patch of patches) {
|
|
942
|
+
if (!patch || !Number.isInteger(patch.start)) return false;
|
|
943
|
+
const start = patch.start;
|
|
944
|
+
if (start < previousStart) return false;
|
|
945
|
+
let captured;
|
|
946
|
+
try {
|
|
947
|
+
captured = requireFragmentStorage(patch.fragment);
|
|
948
|
+
} catch {
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
if (start < previousEnd || !canPatchFragment(start, captured)) return false;
|
|
952
|
+
validated.push({
|
|
953
|
+
start,
|
|
954
|
+
captured
|
|
955
|
+
});
|
|
956
|
+
previousStart = start;
|
|
957
|
+
previousEnd = Math.max(previousEnd, start + captured.count);
|
|
958
|
+
}
|
|
959
|
+
for (const patch of validated) applyFragmentPatch(patch.start, patch.captured);
|
|
960
|
+
return true;
|
|
961
|
+
}
|
|
962
|
+
function writeQuadPayload(view, at) {
|
|
963
|
+
floats[at] = view.m[0];
|
|
964
|
+
floats[at + 1] = view.m[1];
|
|
965
|
+
floats[at + 2] = view.m[2];
|
|
966
|
+
floats[at + 3] = view.m[3];
|
|
967
|
+
floats[at + 4] = view.m[4];
|
|
968
|
+
floats[at + 5] = view.m[5];
|
|
969
|
+
floats[at + 6] = view.w;
|
|
970
|
+
floats[at + 7] = view.h;
|
|
971
|
+
floats[at + 8] = view.srcX;
|
|
972
|
+
floats[at + 9] = view.srcY;
|
|
973
|
+
floats[at + 10] = view.srcW;
|
|
974
|
+
floats[at + 11] = view.srcH;
|
|
975
|
+
floats[at + 12] = view.r;
|
|
976
|
+
floats[at + 13] = view.g;
|
|
977
|
+
floats[at + 14] = view.b;
|
|
978
|
+
floats[at + 15] = view.a;
|
|
979
|
+
}
|
|
980
|
+
function readQuadPayload(view, at) {
|
|
981
|
+
view.m[0] = floats[at];
|
|
982
|
+
view.m[1] = floats[at + 1];
|
|
983
|
+
view.m[2] = floats[at + 2];
|
|
984
|
+
view.m[3] = floats[at + 3];
|
|
985
|
+
view.m[4] = floats[at + 4];
|
|
986
|
+
view.m[5] = floats[at + 5];
|
|
987
|
+
view.w = floats[at + 6];
|
|
988
|
+
view.h = floats[at + 7];
|
|
989
|
+
view.srcX = floats[at + 8];
|
|
990
|
+
view.srcY = floats[at + 9];
|
|
991
|
+
view.srcW = floats[at + 10];
|
|
992
|
+
view.srcH = floats[at + 11];
|
|
993
|
+
view.r = floats[at + 12];
|
|
994
|
+
view.g = floats[at + 13];
|
|
995
|
+
view.b = floats[at + 14];
|
|
996
|
+
view.a = floats[at + 15];
|
|
997
|
+
}
|
|
998
|
+
function writeQuadInts(view, at, matrixIndex) {
|
|
999
|
+
ints[at] = view.blend;
|
|
1000
|
+
ints[at + 1] = (view.flipH ? 1 : 0) | (view.flipV ? 2 : 0);
|
|
1001
|
+
ints[at + 2] = matrixIndex;
|
|
1002
|
+
}
|
|
1003
|
+
function readQuadInts(view, at) {
|
|
1004
|
+
view.blend = ints[at];
|
|
1005
|
+
const flags = ints[at + 1];
|
|
1006
|
+
view.flipH = (flags & 1) !== 0;
|
|
1007
|
+
view.flipV = (flags & 2) !== 0;
|
|
1008
|
+
const matrixIndex = ints[at + 2];
|
|
1009
|
+
view.hasColorMatrix = matrixIndex >= 0;
|
|
1010
|
+
if (matrixIndex >= 0) {
|
|
1011
|
+
const from = matrixIndex * COLOR_MATRIX_FLOATS$1;
|
|
1012
|
+
for (let i = 0; i < COLOR_MATRIX_FLOATS$1; i += 1) view.colorMatrix[i] = colorMatrices[from + i];
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return {
|
|
1016
|
+
get count() {
|
|
1017
|
+
return count;
|
|
1018
|
+
},
|
|
1019
|
+
get clipDepth() {
|
|
1020
|
+
return clipDepth;
|
|
1021
|
+
},
|
|
1022
|
+
get maxClipDepth() {
|
|
1023
|
+
return maxClipDepth;
|
|
1024
|
+
},
|
|
1025
|
+
get structuralRevision() {
|
|
1026
|
+
return structuralRevision;
|
|
1027
|
+
},
|
|
1028
|
+
get contentRevision() {
|
|
1029
|
+
return contentRevision;
|
|
1030
|
+
},
|
|
1031
|
+
get floats() {
|
|
1032
|
+
return floats;
|
|
1033
|
+
},
|
|
1034
|
+
get ints() {
|
|
1035
|
+
return ints;
|
|
1036
|
+
},
|
|
1037
|
+
get colorMatrices() {
|
|
1038
|
+
return colorMatrices;
|
|
1039
|
+
},
|
|
1040
|
+
reset() {
|
|
1041
|
+
for (let i = 0; i < count; i += 1) {
|
|
1042
|
+
textures[i] = null;
|
|
1043
|
+
screenEffects[i] = null;
|
|
1044
|
+
externalEffects[i] = null;
|
|
1045
|
+
}
|
|
1046
|
+
count = 0;
|
|
1047
|
+
floatLength = 0;
|
|
1048
|
+
intLength = 0;
|
|
1049
|
+
colorMatrixCount = 0;
|
|
1050
|
+
clipDepth = 0;
|
|
1051
|
+
maxClipDepth = 0;
|
|
1052
|
+
contentRevision += 1;
|
|
1053
|
+
structuralRevision += 1;
|
|
1054
|
+
},
|
|
1055
|
+
appendFragment(fragment) {
|
|
1056
|
+
const captured = requireFragmentStorage(fragment);
|
|
1057
|
+
const start = count;
|
|
1058
|
+
for (let sourceIndex = 0; sourceIndex < captured.count; sourceIndex += 1) {
|
|
1059
|
+
const kind = captured.kinds[sourceIndex];
|
|
1060
|
+
const floatAt = captured.floatOffsets[sourceIndex];
|
|
1061
|
+
const intAt = captured.intOffsets[sourceIndex];
|
|
1062
|
+
fillFragmentStoragePayloadLengths(captured, sourceIndex, fragmentAppendLengths);
|
|
1063
|
+
if (floatAt < 0 || intAt < 0 || floatAt + fragmentAppendLengths.floats > captured.floatLength || intAt + fragmentAppendLengths.ints > captured.intLength) throw new RangeError(`draw-list fragment command ${sourceIndex} has an invalid arena range`);
|
|
1064
|
+
const index = beginCommand(kind, fragmentAppendLengths.floats, fragmentAppendLengths.ints, captured.textures[sourceIndex]);
|
|
1065
|
+
const targetFloatAt = floatOffsets[index];
|
|
1066
|
+
const targetIntAt = intOffsets[index];
|
|
1067
|
+
for (let offset = 0; offset < fragmentAppendLengths.floats; offset += 1) floats[targetFloatAt + offset] = captured.floats[floatAt + offset];
|
|
1068
|
+
for (let offset = 0; offset < fragmentAppendLengths.ints; offset += 1) ints[targetIntAt + offset] = captured.ints[intAt + offset];
|
|
1069
|
+
screenEffects[index] = captured.screenEffects[sourceIndex];
|
|
1070
|
+
externalEffects[index] = captured.externalEffects[sourceIndex];
|
|
1071
|
+
if (kind === 0 || kind === 1) {
|
|
1072
|
+
const matrix = captured.ints[intAt + 2];
|
|
1073
|
+
if (matrix >= 0) ints[targetIntAt + 2] = storeFragmentColorMatrix(captured.colorMatrices, matrix);
|
|
1074
|
+
}
|
|
1075
|
+
if (kind === 3) {
|
|
1076
|
+
clipDepth += 1;
|
|
1077
|
+
maxClipDepth = Math.max(maxClipDepth, clipDepth);
|
|
1078
|
+
} else if (kind === 4) {
|
|
1079
|
+
if (clipDepth <= 0) throw new RangeError("draw-list fragment popClip() with no clip rect pushed");
|
|
1080
|
+
clipDepth -= 1;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return start;
|
|
1084
|
+
},
|
|
1085
|
+
patchFragment(start, fragment) {
|
|
1086
|
+
return applyFragmentPatches([{
|
|
1087
|
+
start,
|
|
1088
|
+
fragment
|
|
1089
|
+
}]);
|
|
1090
|
+
},
|
|
1091
|
+
patchFragments(patches) {
|
|
1092
|
+
return applyFragmentPatches(patches);
|
|
1093
|
+
},
|
|
1094
|
+
kindAt(index) {
|
|
1095
|
+
requireIndex(index);
|
|
1096
|
+
return kinds[index];
|
|
1097
|
+
},
|
|
1098
|
+
commandRevisionAt(index) {
|
|
1099
|
+
requireIndex(index);
|
|
1100
|
+
return commandRevisions[index];
|
|
1101
|
+
},
|
|
1102
|
+
readPatchesSince(revision, out) {
|
|
1103
|
+
const mutable = out;
|
|
1104
|
+
mutable.begin(contentRevision);
|
|
1105
|
+
if (revision === contentRevision) return out;
|
|
1106
|
+
if (patchJournalCount === 0) {
|
|
1107
|
+
mutable.overflow();
|
|
1108
|
+
return out;
|
|
1109
|
+
}
|
|
1110
|
+
if (revision < patchJournalRevisions[patchJournalStart] - 1) {
|
|
1111
|
+
mutable.overflow();
|
|
1112
|
+
return out;
|
|
1113
|
+
}
|
|
1114
|
+
for (let offset = 0; offset < patchJournalCount; offset += 1) {
|
|
1115
|
+
const slot = (patchJournalStart + offset) % patchJournalCapacity;
|
|
1116
|
+
if (patchJournalRevisions[slot] > revision) mutable.add(patchJournalIndexes[slot]);
|
|
1117
|
+
}
|
|
1118
|
+
return out;
|
|
1119
|
+
},
|
|
1120
|
+
kindNameAt(index) {
|
|
1121
|
+
requireIndex(index);
|
|
1122
|
+
return DRAW_COMMAND_NAMES[kinds[index]];
|
|
1123
|
+
},
|
|
1124
|
+
textureAt(index) {
|
|
1125
|
+
requireIndex(index);
|
|
1126
|
+
return textures[index];
|
|
1127
|
+
},
|
|
1128
|
+
screenEffectAt(index) {
|
|
1129
|
+
requireIndex(index);
|
|
1130
|
+
return screenEffects[index];
|
|
1131
|
+
},
|
|
1132
|
+
externalEffectAt(index) {
|
|
1133
|
+
requireIndex(index);
|
|
1134
|
+
return externalEffects[index];
|
|
1135
|
+
},
|
|
1136
|
+
floatOffsetAt(index) {
|
|
1137
|
+
requireIndex(index);
|
|
1138
|
+
return floatOffsets[index];
|
|
1139
|
+
},
|
|
1140
|
+
intOffsetAt(index) {
|
|
1141
|
+
requireIndex(index);
|
|
1142
|
+
return intOffsets[index];
|
|
1143
|
+
},
|
|
1144
|
+
colorMatrixIndexAt(index) {
|
|
1145
|
+
requireIndex(index);
|
|
1146
|
+
const kind = kinds[index];
|
|
1147
|
+
if (kind !== 0 && kind !== 1) return -1;
|
|
1148
|
+
return ints[intOffsets[index] + 2];
|
|
1149
|
+
},
|
|
1150
|
+
pushQuad(quad, texture = null) {
|
|
1151
|
+
const matrixIndex = storeColorMatrix(quad);
|
|
1152
|
+
const index = beginCommand(0, QUAD_FLOATS, QUAD_INTS, texture);
|
|
1153
|
+
writeQuadPayload(quad, floatOffsets[index]);
|
|
1154
|
+
writeQuadInts(quad, intOffsets[index], matrixIndex);
|
|
1155
|
+
return index;
|
|
1156
|
+
},
|
|
1157
|
+
pushNinePatch(patch, texture = null) {
|
|
1158
|
+
const matrixIndex = storeColorMatrix(patch);
|
|
1159
|
+
const index = beginCommand(1, NINE_PATCH_FLOATS, NINE_PATCH_INTS, texture);
|
|
1160
|
+
const at = floatOffsets[index];
|
|
1161
|
+
writeQuadPayload(patch, at);
|
|
1162
|
+
floats[at + QUAD_FLOATS] = patch.marginLeft;
|
|
1163
|
+
floats[at + QUAD_FLOATS + 1] = patch.marginTop;
|
|
1164
|
+
floats[at + QUAD_FLOATS + 2] = patch.marginRight;
|
|
1165
|
+
floats[at + QUAD_FLOATS + 3] = patch.marginBottom;
|
|
1166
|
+
writeQuadInts(patch, intOffsets[index], matrixIndex);
|
|
1167
|
+
return index;
|
|
1168
|
+
},
|
|
1169
|
+
pushPolyline(line) {
|
|
1170
|
+
const pointCount = Math.max(0, Math.floor(line.pointCount));
|
|
1171
|
+
if (pointCount * 2 > line.points.length) throw new RangeError(`polyline claims ${pointCount} points but its buffer holds ${Math.floor(line.points.length / 2)}`);
|
|
1172
|
+
const index = beginCommand(2, POLYLINE_HEADER_FLOATS + pointCount * 2, POLYLINE_INTS, null);
|
|
1173
|
+
const at = floatOffsets[index];
|
|
1174
|
+
floats[at] = line.width;
|
|
1175
|
+
floats[at + 1] = line.r;
|
|
1176
|
+
floats[at + 2] = line.g;
|
|
1177
|
+
floats[at + 3] = line.b;
|
|
1178
|
+
floats[at + 4] = line.a;
|
|
1179
|
+
floats.set(line.points.subarray(0, pointCount * 2), at + POLYLINE_HEADER_FLOATS);
|
|
1180
|
+
ints[intOffsets[index]] = pointCount;
|
|
1181
|
+
return index;
|
|
1182
|
+
},
|
|
1183
|
+
pushTexturedMesh(mesh, texture = null) {
|
|
1184
|
+
const vertexCount = Math.max(0, Math.floor(mesh.vertexCount));
|
|
1185
|
+
const indexCount = Math.max(0, Math.floor(mesh.indexCount));
|
|
1186
|
+
if (mesh.m.length < 6) throw new RangeError(`textured mesh transform needs 6 entries, got ${mesh.m.length}`);
|
|
1187
|
+
if (vertexCount * 2 > mesh.positions.length) throw new RangeError(`textured mesh claims ${vertexCount} vertices but its position buffer holds ${Math.floor(mesh.positions.length / 2)}`);
|
|
1188
|
+
if (vertexCount * 2 > mesh.uvs.length) throw new RangeError(`textured mesh claims ${vertexCount} vertices but its UV buffer holds ${Math.floor(mesh.uvs.length / 2)}`);
|
|
1189
|
+
if (indexCount > mesh.indices.length) throw new RangeError(`textured mesh claims ${indexCount} indices but its index buffer holds ${mesh.indices.length}`);
|
|
1190
|
+
if (indexCount % 3 !== 0) throw new RangeError(`textured mesh index count ${indexCount} is not a triangle list`);
|
|
1191
|
+
for (let i = 0; i < indexCount; i += 1) if (mesh.indices[i] >= vertexCount) throw new RangeError(`textured mesh index ${mesh.indices[i]} at ${i} is outside ${vertexCount} vertices`);
|
|
1192
|
+
const index = beginCommand(6, TEXTURED_MESH_HEADER_FLOATS + vertexCount * 4, TEXTURED_MESH_HEADER_INTS + indexCount, texture);
|
|
1193
|
+
const at = floatOffsets[index];
|
|
1194
|
+
for (let i = 0; i < 6; i += 1) floats[at + i] = mesh.m[i];
|
|
1195
|
+
floats[at + 6] = mesh.r;
|
|
1196
|
+
floats[at + 7] = mesh.g;
|
|
1197
|
+
floats[at + 8] = mesh.b;
|
|
1198
|
+
floats[at + 9] = mesh.a;
|
|
1199
|
+
const positionsAt = at + TEXTURED_MESH_HEADER_FLOATS;
|
|
1200
|
+
for (let i = 0; i < vertexCount * 2; i += 1) {
|
|
1201
|
+
floats[positionsAt + i] = mesh.positions[i];
|
|
1202
|
+
floats[positionsAt + vertexCount * 2 + i] = mesh.uvs[i];
|
|
1203
|
+
}
|
|
1204
|
+
const intAt = intOffsets[index];
|
|
1205
|
+
ints[intAt] = vertexCount;
|
|
1206
|
+
ints[intAt + 1] = indexCount;
|
|
1207
|
+
ints[intAt + 2] = mesh.blend;
|
|
1208
|
+
for (let i = 0; i < indexCount; i += 1) ints[intAt + TEXTURED_MESH_HEADER_INTS + i] = mesh.indices[i];
|
|
1209
|
+
return index;
|
|
1210
|
+
},
|
|
1211
|
+
pushGlyphs(run) {
|
|
1212
|
+
const glyphCount = Math.max(0, Math.floor(run.glyphCount));
|
|
1213
|
+
if (glyphCount * 2 > run.positions.length) throw new RangeError(`glyph run claims ${glyphCount} glyphs but its position buffer holds ${Math.floor(run.positions.length / 2)}`);
|
|
1214
|
+
if (glyphCount > run.slots.length) throw new RangeError(`glyph run claims ${glyphCount} glyphs but its slot buffer holds ${run.slots.length}`);
|
|
1215
|
+
const index = beginCommand(5, GLYPHS_HEADER_FLOATS + glyphCount * 2, GLYPHS_HEADER_INTS + glyphCount, null);
|
|
1216
|
+
const at = floatOffsets[index];
|
|
1217
|
+
floats[at] = run.m[0];
|
|
1218
|
+
floats[at + 1] = run.m[1];
|
|
1219
|
+
floats[at + 2] = run.m[2];
|
|
1220
|
+
floats[at + 3] = run.m[3];
|
|
1221
|
+
floats[at + 4] = run.m[4];
|
|
1222
|
+
floats[at + 5] = run.m[5];
|
|
1223
|
+
floats[at + 6] = run.pixelsPerEm;
|
|
1224
|
+
floats[at + 7] = run.r;
|
|
1225
|
+
floats[at + 8] = run.g;
|
|
1226
|
+
floats[at + 9] = run.b;
|
|
1227
|
+
floats[at + 10] = run.a;
|
|
1228
|
+
floats[at + GLYPHS_SPREAD_OFFSET] = Number.isFinite(run.spreadPx) ? run.spreadPx : 0;
|
|
1229
|
+
floats[at + GLYPHS_INK_X_OFFSET] = finiteOrNaN(run.localInkX);
|
|
1230
|
+
floats[at + GLYPHS_INK_Y_OFFSET] = finiteOrNaN(run.localInkY);
|
|
1231
|
+
floats[at + GLYPHS_INK_WIDTH_OFFSET] = finiteOrNaN(run.localInkWidth);
|
|
1232
|
+
floats[at + GLYPHS_INK_HEIGHT_OFFSET] = finiteOrNaN(run.localInkHeight);
|
|
1233
|
+
floats[at + GLYPHS_INK_OUTSET_OFFSET] = finiteOrNaN(run.localInkOutset);
|
|
1234
|
+
floats.set(run.positions.subarray(0, glyphCount * 2), at + GLYPHS_HEADER_FLOATS);
|
|
1235
|
+
const intAt = intOffsets[index];
|
|
1236
|
+
ints[intAt] = glyphCount;
|
|
1237
|
+
ints.set(run.slots.subarray(0, glyphCount), intAt + GLYPHS_HEADER_INTS);
|
|
1238
|
+
return index;
|
|
1239
|
+
},
|
|
1240
|
+
pushScreenEffect(command) {
|
|
1241
|
+
const index = beginCommand(7, 0, 0, null);
|
|
1242
|
+
screenEffects[index] = command;
|
|
1243
|
+
return index;
|
|
1244
|
+
},
|
|
1245
|
+
pushExternalEffect(command) {
|
|
1246
|
+
const index = beginCommand(8, 0, 0, null);
|
|
1247
|
+
externalEffects[index] = command;
|
|
1248
|
+
return index;
|
|
1249
|
+
},
|
|
1250
|
+
pushClipRect(clip) {
|
|
1251
|
+
const index = beginCommand(3, CLIP_FLOATS, 0, null);
|
|
1252
|
+
const at = floatOffsets[index];
|
|
1253
|
+
floats[at] = clip.x;
|
|
1254
|
+
floats[at + 1] = clip.y;
|
|
1255
|
+
floats[at + 2] = clip.w;
|
|
1256
|
+
floats[at + 3] = clip.h;
|
|
1257
|
+
floats[at + 4] = clip.cornerRadius;
|
|
1258
|
+
floats[at + 5] = clip.outsetX;
|
|
1259
|
+
clipDepth += 1;
|
|
1260
|
+
if (clipDepth > maxClipDepth) maxClipDepth = clipDepth;
|
|
1261
|
+
return index;
|
|
1262
|
+
},
|
|
1263
|
+
popClip() {
|
|
1264
|
+
if (clipDepth === 0) throw new RangeError("draw-list popClip() with no clip rect pushed");
|
|
1265
|
+
const index = beginCommand(4, 0, 0, null);
|
|
1266
|
+
clipDepth -= 1;
|
|
1267
|
+
return index;
|
|
1268
|
+
},
|
|
1269
|
+
readQuad(index, out) {
|
|
1270
|
+
requireKind(index, 0);
|
|
1271
|
+
readQuadPayload(out, floatOffsets[index]);
|
|
1272
|
+
readQuadInts(out, intOffsets[index]);
|
|
1273
|
+
return out;
|
|
1274
|
+
},
|
|
1275
|
+
readNinePatch(index, out) {
|
|
1276
|
+
requireKind(index, 1);
|
|
1277
|
+
const at = floatOffsets[index];
|
|
1278
|
+
readQuadPayload(out, at);
|
|
1279
|
+
out.marginLeft = floats[at + QUAD_FLOATS];
|
|
1280
|
+
out.marginTop = floats[at + QUAD_FLOATS + 1];
|
|
1281
|
+
out.marginRight = floats[at + QUAD_FLOATS + 2];
|
|
1282
|
+
out.marginBottom = floats[at + QUAD_FLOATS + 3];
|
|
1283
|
+
readQuadInts(out, intOffsets[index]);
|
|
1284
|
+
return out;
|
|
1285
|
+
},
|
|
1286
|
+
readPolyline(index, out) {
|
|
1287
|
+
requireKind(index, 2);
|
|
1288
|
+
const at = floatOffsets[index];
|
|
1289
|
+
out.width = floats[at];
|
|
1290
|
+
out.r = floats[at + 1];
|
|
1291
|
+
out.g = floats[at + 2];
|
|
1292
|
+
out.b = floats[at + 3];
|
|
1293
|
+
out.a = floats[at + 4];
|
|
1294
|
+
const pointCount = ints[intOffsets[index]];
|
|
1295
|
+
out.pointCount = pointCount;
|
|
1296
|
+
if (out.points.length < pointCount * 2) out.points = new Float32Array(pointCount * 2);
|
|
1297
|
+
const from = at + POLYLINE_HEADER_FLOATS;
|
|
1298
|
+
out.points.set(floats.subarray(from, from + pointCount * 2));
|
|
1299
|
+
return out;
|
|
1300
|
+
},
|
|
1301
|
+
readTexturedMesh(index, out) {
|
|
1302
|
+
requireTexturedMesh(index);
|
|
1303
|
+
const at = floatOffsets[index];
|
|
1304
|
+
const intAt = intOffsets[index];
|
|
1305
|
+
const vertexCount = ints[intAt];
|
|
1306
|
+
const indexCount = ints[intAt + 1];
|
|
1307
|
+
for (let i = 0; i < 6; i += 1) out.m[i] = floats[at + i];
|
|
1308
|
+
out.r = floats[at + 6];
|
|
1309
|
+
out.g = floats[at + 7];
|
|
1310
|
+
out.b = floats[at + 8];
|
|
1311
|
+
out.a = floats[at + 9];
|
|
1312
|
+
out.vertexCount = vertexCount;
|
|
1313
|
+
out.indexCount = indexCount;
|
|
1314
|
+
out.blend = ints[intAt + 2];
|
|
1315
|
+
if (out.positions.length < vertexCount * 2) out.positions = new Float32Array(vertexCount * 2);
|
|
1316
|
+
if (out.uvs.length < vertexCount * 2) out.uvs = new Float32Array(vertexCount * 2);
|
|
1317
|
+
if (out.indices.length < indexCount) out.indices = new Uint32Array(indexCount);
|
|
1318
|
+
const positionsAt = at + TEXTURED_MESH_HEADER_FLOATS;
|
|
1319
|
+
for (let i = 0; i < vertexCount * 2; i += 1) {
|
|
1320
|
+
out.positions[i] = floats[positionsAt + i];
|
|
1321
|
+
out.uvs[i] = floats[positionsAt + vertexCount * 2 + i];
|
|
1322
|
+
}
|
|
1323
|
+
for (let i = 0; i < indexCount; i += 1) out.indices[i] = ints[intAt + TEXTURED_MESH_HEADER_INTS + i];
|
|
1324
|
+
return out;
|
|
1325
|
+
},
|
|
1326
|
+
readGlyphs(index, out) {
|
|
1327
|
+
requireKind(index, 5);
|
|
1328
|
+
const at = floatOffsets[index];
|
|
1329
|
+
out.m[0] = floats[at];
|
|
1330
|
+
out.m[1] = floats[at + 1];
|
|
1331
|
+
out.m[2] = floats[at + 2];
|
|
1332
|
+
out.m[3] = floats[at + 3];
|
|
1333
|
+
out.m[4] = floats[at + 4];
|
|
1334
|
+
out.m[5] = floats[at + 5];
|
|
1335
|
+
out.pixelsPerEm = floats[at + 6];
|
|
1336
|
+
out.r = floats[at + 7];
|
|
1337
|
+
out.g = floats[at + 8];
|
|
1338
|
+
out.b = floats[at + 9];
|
|
1339
|
+
out.a = floats[at + 10];
|
|
1340
|
+
out.spreadPx = floats[at + GLYPHS_SPREAD_OFFSET];
|
|
1341
|
+
out.localInkX = floats[at + GLYPHS_INK_X_OFFSET];
|
|
1342
|
+
out.localInkY = floats[at + GLYPHS_INK_Y_OFFSET];
|
|
1343
|
+
out.localInkWidth = floats[at + GLYPHS_INK_WIDTH_OFFSET];
|
|
1344
|
+
out.localInkHeight = floats[at + GLYPHS_INK_HEIGHT_OFFSET];
|
|
1345
|
+
out.localInkOutset = floats[at + GLYPHS_INK_OUTSET_OFFSET];
|
|
1346
|
+
const intAt = intOffsets[index];
|
|
1347
|
+
const glyphCount = ints[intAt];
|
|
1348
|
+
out.glyphCount = glyphCount;
|
|
1349
|
+
if (out.slots.length < glyphCount) out.slots = new Int32Array(glyphCount);
|
|
1350
|
+
if (out.positions.length < glyphCount * 2) out.positions = new Float32Array(glyphCount * 2);
|
|
1351
|
+
out.slots.set(ints.subarray(intAt + GLYPHS_HEADER_INTS, intAt + 1 + glyphCount));
|
|
1352
|
+
const from = at + GLYPHS_HEADER_FLOATS;
|
|
1353
|
+
out.positions.set(floats.subarray(from, from + glyphCount * 2));
|
|
1354
|
+
return out;
|
|
1355
|
+
},
|
|
1356
|
+
readClipRect(index, out) {
|
|
1357
|
+
requireKind(index, 3);
|
|
1358
|
+
const at = floatOffsets[index];
|
|
1359
|
+
out.x = floats[at];
|
|
1360
|
+
out.y = floats[at + 1];
|
|
1361
|
+
out.w = floats[at + 2];
|
|
1362
|
+
out.h = floats[at + 3];
|
|
1363
|
+
out.cornerRadius = floats[at + 4];
|
|
1364
|
+
out.outsetX = floats[at + 5];
|
|
1365
|
+
return out;
|
|
1366
|
+
},
|
|
1367
|
+
patchQuadTransform(index, m) {
|
|
1368
|
+
requireQuadLike(index);
|
|
1369
|
+
if (m.length < 6) throw new RangeError(`draw-list patchQuadTransform needs 6 transform entries, got ${m.length}`);
|
|
1370
|
+
const at = floatOffsets[index];
|
|
1371
|
+
floats[at] = m[0];
|
|
1372
|
+
floats[at + 1] = m[1];
|
|
1373
|
+
floats[at + 2] = m[2];
|
|
1374
|
+
floats[at + 3] = m[3];
|
|
1375
|
+
floats[at + 4] = m[4];
|
|
1376
|
+
floats[at + 5] = m[5];
|
|
1377
|
+
markPatched(index);
|
|
1378
|
+
},
|
|
1379
|
+
patchQuadColor(index, r, g, b, a) {
|
|
1380
|
+
requireQuadLike(index);
|
|
1381
|
+
const at = floatOffsets[index];
|
|
1382
|
+
floats[at + 12] = r;
|
|
1383
|
+
floats[at + 13] = g;
|
|
1384
|
+
floats[at + 14] = b;
|
|
1385
|
+
floats[at + 15] = a;
|
|
1386
|
+
markPatched(index);
|
|
1387
|
+
},
|
|
1388
|
+
patchQuadSource(index, texture, srcX, srcY, srcW, srcH) {
|
|
1389
|
+
requireQuadLike(index);
|
|
1390
|
+
const at = floatOffsets[index];
|
|
1391
|
+
textures[index] = texture;
|
|
1392
|
+
floats[at + 8] = srcX;
|
|
1393
|
+
floats[at + 9] = srcY;
|
|
1394
|
+
floats[at + 10] = srcW;
|
|
1395
|
+
floats[at + 11] = srcH;
|
|
1396
|
+
markPatched(index);
|
|
1397
|
+
},
|
|
1398
|
+
patchTexturedMeshPositions(index, positions) {
|
|
1399
|
+
requireTexturedMesh(index);
|
|
1400
|
+
const intAt = intOffsets[index];
|
|
1401
|
+
const vertexCount = ints[intAt];
|
|
1402
|
+
if (positions.length < vertexCount * 2) throw new RangeError(`textured mesh position patch needs ${vertexCount * 2} entries, got ${positions.length}`);
|
|
1403
|
+
const at = floatOffsets[index] + TEXTURED_MESH_HEADER_FLOATS;
|
|
1404
|
+
for (let i = 0; i < vertexCount * 2; i += 1) floats[at + i] = positions[i];
|
|
1405
|
+
markPatched(index);
|
|
1406
|
+
},
|
|
1407
|
+
patchTexturedMeshUvs(index, uvs) {
|
|
1408
|
+
requireTexturedMesh(index);
|
|
1409
|
+
const intAt = intOffsets[index];
|
|
1410
|
+
const vertexCount = ints[intAt];
|
|
1411
|
+
if (uvs.length < vertexCount * 2) throw new RangeError(`textured mesh UV patch needs ${vertexCount * 2} entries, got ${uvs.length}`);
|
|
1412
|
+
const at = floatOffsets[index] + TEXTURED_MESH_HEADER_FLOATS + vertexCount * 2;
|
|
1413
|
+
for (let i = 0; i < vertexCount * 2; i += 1) floats[at + i] = uvs[i];
|
|
1414
|
+
markPatched(index);
|
|
1415
|
+
},
|
|
1416
|
+
patchTexturedMeshSource(index, texture) {
|
|
1417
|
+
requireTexturedMesh(index);
|
|
1418
|
+
textures[index] = texture;
|
|
1419
|
+
markPatched(index);
|
|
1420
|
+
},
|
|
1421
|
+
patchTexturedMeshTransform(index, m) {
|
|
1422
|
+
requireTexturedMesh(index);
|
|
1423
|
+
if (m.length < 6) throw new RangeError(`draw-list patchTexturedMeshTransform needs 6 transform entries, got ${m.length}`);
|
|
1424
|
+
const at = floatOffsets[index];
|
|
1425
|
+
for (let i = 0; i < 6; i += 1) floats[at + i] = m[i];
|
|
1426
|
+
markPatched(index);
|
|
1427
|
+
},
|
|
1428
|
+
patchTexturedMeshColor(index, r, g, b, a) {
|
|
1429
|
+
requireTexturedMesh(index);
|
|
1430
|
+
const at = floatOffsets[index];
|
|
1431
|
+
floats[at + 6] = r;
|
|
1432
|
+
floats[at + 7] = g;
|
|
1433
|
+
floats[at + 8] = b;
|
|
1434
|
+
floats[at + 9] = a;
|
|
1435
|
+
markPatched(index);
|
|
1436
|
+
},
|
|
1437
|
+
patchGlyphsTransform(index, m) {
|
|
1438
|
+
requireKind(index, 5);
|
|
1439
|
+
if (m.length < 6) throw new RangeError(`draw-list patchGlyphsTransform needs 6 transform entries, got ${m.length}`);
|
|
1440
|
+
const at = floatOffsets[index];
|
|
1441
|
+
floats[at] = m[0];
|
|
1442
|
+
floats[at + 1] = m[1];
|
|
1443
|
+
floats[at + 2] = m[2];
|
|
1444
|
+
floats[at + 3] = m[3];
|
|
1445
|
+
floats[at + 4] = m[4];
|
|
1446
|
+
floats[at + 5] = m[5];
|
|
1447
|
+
markPatched(index);
|
|
1448
|
+
},
|
|
1449
|
+
patchGlyphsColor(index, r, g, b, a) {
|
|
1450
|
+
requireKind(index, 5);
|
|
1451
|
+
const at = floatOffsets[index];
|
|
1452
|
+
floats[at + 7] = r;
|
|
1453
|
+
floats[at + 8] = g;
|
|
1454
|
+
floats[at + 9] = b;
|
|
1455
|
+
floats[at + 10] = a;
|
|
1456
|
+
markPatched(index);
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
//#endregion
|
|
1461
|
+
//#region src/batcher.ts
|
|
1462
|
+
/**
|
|
1463
|
+
* The quad batcher: everything the executor knows about MERGING draws, with no
|
|
1464
|
+
* GL in it, so the flush decisions — the thing that decides whether a frame is 20
|
|
1465
|
+
* draw calls or 200 — are unit-testable rather than inferred from a profiler.
|
|
1466
|
+
*
|
|
1467
|
+
* WHY MULTI-TEXTURE BATCHING IS THE WHOLE POINT. Probing recorded scenes for
|
|
1468
|
+
* batch runs (a run = a maximal span of consecutive painting nodes sharing
|
|
1469
|
+
* texture + blend + clip + colour-matrix state) found combat at 152 runs over 183
|
|
1470
|
+
* painting nodes — 1.2 nodes per batch, i.e. essentially no batching — and 151 of
|
|
1471
|
+
* those 152 breaks were the TEXTURE alone. The map is the same story: 174 runs
|
|
1472
|
+
* over 825 nodes, 173 texture breaks. But the number of DISTINCT textures per
|
|
1473
|
+
* screen is only 24-65. So a batch that can hold many textures at once collapses
|
|
1474
|
+
* the run count towards the number of times the state that a batch CANNOT hold
|
|
1475
|
+
* changes — blend (0-16 per screen) and clip (0-26) — plus one flush per
|
|
1476
|
+
* texture-table refill. That is the difference between "tens of draws" and "one
|
|
1477
|
+
* draw per node", and it is why the slot table below is not an optimization to
|
|
1478
|
+
* add later.
|
|
1479
|
+
*
|
|
1480
|
+
* HOW A TEXTURE STOPS BREAKING A BATCH. Each batch binds up to
|
|
1481
|
+
* `maxTextureSlots` textures to consecutive texture units and each quad carries
|
|
1482
|
+
* the INDEX of the one it samples. The fragment shader turns that index back into
|
|
1483
|
+
* a sampler with a compiled `if` ladder (GLSL ES 3.00 forbids indexing a sampler
|
|
1484
|
+
* array with anything but a constant). A quad whose texture is not in the table
|
|
1485
|
+
* takes the next free slot; when the table is full the batch flushes and starts a
|
|
1486
|
+
* new table.
|
|
1487
|
+
*
|
|
1488
|
+
* COLOUR MATRICES GET THE SAME TREATMENT, for the same reason at a smaller scale:
|
|
1489
|
+
* the card screens carry 30-48 HSV-transformed quads, and one draw call each
|
|
1490
|
+
* would undo the texture win. A batch holds up to `maxColorMatrices` matrices in
|
|
1491
|
+
* a uniform array with the IDENTITY pinned at slot 0, so "no matrix" costs
|
|
1492
|
+
* nothing and needs no separate program. Identical matrices share a slot (the
|
|
1493
|
+
* table is deduped by value), which matters because those 30-48 quads are usually
|
|
1494
|
+
* a handful of distinct tints applied to many cards.
|
|
1495
|
+
*
|
|
1496
|
+
* ORDER IS NEVER REORDERED. Instances are drawn in the order they were pushed —
|
|
1497
|
+
* `drawArraysInstanced` rasterizes instance N before instance N+1, which is the
|
|
1498
|
+
* property a painter's-algorithm 2D renderer with no depth buffer depends on. So
|
|
1499
|
+
* this batcher only ever MERGES CONSECUTIVE commands; it never sorts, and it can
|
|
1500
|
+
* therefore be dropped in front of any draw list without changing what the frame
|
|
1501
|
+
* looks like. Bucketing non-adjacent commands by texture is a separate,
|
|
1502
|
+
* order-unsafe transformation that belongs to whoever BUILDS the list and knows
|
|
1503
|
+
* which spans are safe to permute.
|
|
1504
|
+
*
|
|
1505
|
+
* FOUR EXPLICIT CORNERS, NOT AN AFFINE BASIS. An instance carries `p0..p3`
|
|
1506
|
+
* outright (8 floats) rather than a 2x3 transform (6). The two extra floats buy
|
|
1507
|
+
* arbitrary quadrilaterals, which is what lets `./polyline`'s stroke segments AND
|
|
1508
|
+
* its join wedges (a triangle spelled as a quad with two coincident corners) ride
|
|
1509
|
+
* in the same buffer, the same shader and the same batch as every sprite. The
|
|
1510
|
+
* alternative — a second program and a mid-frame break for every polyline — costs
|
|
1511
|
+
* far more than 8 bytes per quad. The consequence to know about: UV interpolates
|
|
1512
|
+
* affinely per triangle, so a NON-parallelogram textured quad would show a seam
|
|
1513
|
+
* along the split diagonal. Nothing produces one (sprites and nine-patch bands
|
|
1514
|
+
* are affine images of a rect; the non-affine quads are untextured stroke
|
|
1515
|
+
* geometry), and if something ever does, the fix is to split it rather than to
|
|
1516
|
+
* make every quad pay for projective interpolation.
|
|
1517
|
+
*/
|
|
1518
|
+
/** Floats per instance. See the field offsets below for the layout. */
|
|
1519
|
+
const INSTANCE_FLOATS = 18;
|
|
1520
|
+
/** Offset of `p0.x` — four `(x, y)` corners, in the unit-square order
|
|
1521
|
+
* `(0,0)`, `(1,0)`, `(1,1)`, `(0,1)`. Design space. */
|
|
1522
|
+
const INSTANCE_CORNERS_OFFSET = 0;
|
|
1523
|
+
/** Offset of the normalized source rect `(u0, v0, uSpan, vSpan)`. A span is
|
|
1524
|
+
* NEGATIVE for a flipped axis, which is how `FLIP_H`/`FLIP_V` are carried. */
|
|
1525
|
+
const INSTANCE_UV_OFFSET = 8;
|
|
1526
|
+
/** Offset of the PREMULTIPLIED tint, `(r, g, b, a)`. */
|
|
1527
|
+
const INSTANCE_COLOR_OFFSET = 12;
|
|
1528
|
+
/** Offset of `(textureSlot, colorMatrixSlot)`. Matrix slot 0 is the identity. */
|
|
1529
|
+
const INSTANCE_SLOTS_OFFSET = 16;
|
|
1530
|
+
/** Floats per colour-matrix slot. */
|
|
1531
|
+
const COLOR_MATRIX_FLOATS = 9;
|
|
1532
|
+
/** The most texture units this batcher will ever ask for, whatever the GPU
|
|
1533
|
+
* reports. Sixteen is the WebGL2 (GLES 3.0) guaranteed minimum for
|
|
1534
|
+
* `MAX_TEXTURE_IMAGE_UNITS`, so asking for more buys a shader that some
|
|
1535
|
+
* conformant device cannot link, in exchange for a batch boundary the measured
|
|
1536
|
+
* key counts (24-65 distinct textures per screen) would still hit. */
|
|
1537
|
+
const MAX_TEXTURE_SLOTS = 16;
|
|
1538
|
+
/** Default colour-matrix table size, including the identity at slot 0. */
|
|
1539
|
+
const DEFAULT_COLOR_MATRIX_SLOTS = 16;
|
|
1540
|
+
function createQuadInstance() {
|
|
1541
|
+
return {
|
|
1542
|
+
x0: 0,
|
|
1543
|
+
y0: 0,
|
|
1544
|
+
x1: 0,
|
|
1545
|
+
y1: 0,
|
|
1546
|
+
x2: 0,
|
|
1547
|
+
y2: 0,
|
|
1548
|
+
x3: 0,
|
|
1549
|
+
y3: 0,
|
|
1550
|
+
u0: 0,
|
|
1551
|
+
v0: 0,
|
|
1552
|
+
uSpan: 1,
|
|
1553
|
+
vSpan: 1,
|
|
1554
|
+
r: 1,
|
|
1555
|
+
g: 1,
|
|
1556
|
+
b: 1,
|
|
1557
|
+
a: 1
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
function emptyFlushCounts() {
|
|
1561
|
+
return {
|
|
1562
|
+
textureSlots: 0,
|
|
1563
|
+
colorMatrices: 0,
|
|
1564
|
+
blend: 0,
|
|
1565
|
+
clip: 0,
|
|
1566
|
+
glyphs: 0,
|
|
1567
|
+
effects: 0,
|
|
1568
|
+
meshes: 0,
|
|
1569
|
+
compiled: 0,
|
|
1570
|
+
end: 0
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
function zeroFlushCounts(counts) {
|
|
1574
|
+
counts.textureSlots = 0;
|
|
1575
|
+
counts.colorMatrices = 0;
|
|
1576
|
+
counts.blend = 0;
|
|
1577
|
+
counts.clip = 0;
|
|
1578
|
+
counts.glyphs = 0;
|
|
1579
|
+
counts.effects = 0;
|
|
1580
|
+
counts.meshes = 0;
|
|
1581
|
+
counts.compiled = 0;
|
|
1582
|
+
counts.end = 0;
|
|
1583
|
+
}
|
|
1584
|
+
function createQuadBatcher(options) {
|
|
1585
|
+
const maxTextureSlots = Math.max(1, Math.min(16, Math.floor(options.maxTextureSlots ?? 16)));
|
|
1586
|
+
const maxColorMatrices = Math.max(1, Math.floor(options.maxColorMatrices ?? 16));
|
|
1587
|
+
const draw = options.draw;
|
|
1588
|
+
let instances = new Float32Array(Math.max(1, Math.floor(options.quadCapacity ?? 512)) * 18);
|
|
1589
|
+
let quadCount = 0;
|
|
1590
|
+
const textures = new Array(maxTextureSlots).fill(null);
|
|
1591
|
+
let textureCount = 0;
|
|
1592
|
+
const colorMatrices = new Float32Array(maxColorMatrices * 9);
|
|
1593
|
+
colorMatrices.set(IDENTITY_COLOR_MATRIX, 0);
|
|
1594
|
+
let colorMatrixCount = 1;
|
|
1595
|
+
let blend = 0;
|
|
1596
|
+
let clipEpoch = 0;
|
|
1597
|
+
const quad = createQuadInstance();
|
|
1598
|
+
const stats = {
|
|
1599
|
+
batches: 0,
|
|
1600
|
+
quads: 0,
|
|
1601
|
+
textureBinds: 0,
|
|
1602
|
+
maxBatchQuads: 0,
|
|
1603
|
+
arenaGrowths: 0,
|
|
1604
|
+
flushes: emptyFlushCounts()
|
|
1605
|
+
};
|
|
1606
|
+
const batch = {
|
|
1607
|
+
instances,
|
|
1608
|
+
quadCount: 0,
|
|
1609
|
+
textures,
|
|
1610
|
+
textureCount: 0,
|
|
1611
|
+
colorMatrices,
|
|
1612
|
+
colorMatrixCount: 1,
|
|
1613
|
+
blend,
|
|
1614
|
+
clipEpoch: 0,
|
|
1615
|
+
reason: "end"
|
|
1616
|
+
};
|
|
1617
|
+
function flush(reason = "end") {
|
|
1618
|
+
if (quadCount === 0) return;
|
|
1619
|
+
batch.instances = instances;
|
|
1620
|
+
batch.quadCount = quadCount;
|
|
1621
|
+
batch.textureCount = textureCount;
|
|
1622
|
+
batch.colorMatrixCount = colorMatrixCount;
|
|
1623
|
+
batch.blend = blend;
|
|
1624
|
+
batch.clipEpoch = clipEpoch;
|
|
1625
|
+
batch.reason = reason;
|
|
1626
|
+
stats.batches += 1;
|
|
1627
|
+
stats.textureBinds += textureCount;
|
|
1628
|
+
if (quadCount > stats.maxBatchQuads) stats.maxBatchQuads = quadCount;
|
|
1629
|
+
stats.flushes[reason] += 1;
|
|
1630
|
+
draw(batch);
|
|
1631
|
+
quadCount = 0;
|
|
1632
|
+
for (let i = 0; i < textureCount; i += 1) textures[i] = null;
|
|
1633
|
+
textureCount = 0;
|
|
1634
|
+
colorMatrixCount = 1;
|
|
1635
|
+
}
|
|
1636
|
+
/** The texture's slot in the open batch, or -1 when the table is full. */
|
|
1637
|
+
function slotFor(texture) {
|
|
1638
|
+
for (let i = 0; i < textureCount; i += 1) if (textures[i] === texture) return i;
|
|
1639
|
+
if (textureCount >= maxTextureSlots) return -1;
|
|
1640
|
+
textures[textureCount] = texture;
|
|
1641
|
+
textureCount += 1;
|
|
1642
|
+
return textureCount - 1;
|
|
1643
|
+
}
|
|
1644
|
+
/** The matrix's slot, deduped by value, or -1 when the table is full. */
|
|
1645
|
+
function matrixSlotFor(matrix, offset) {
|
|
1646
|
+
for (let i = 0; i < colorMatrixCount; i += 1) if (colorMatricesEqual(colorMatrices, i * 9, matrix, offset)) return i;
|
|
1647
|
+
if (colorMatrixCount >= maxColorMatrices) return -1;
|
|
1648
|
+
const at = colorMatrixCount * 9;
|
|
1649
|
+
for (let i = 0; i < 9; i += 1) colorMatrices[at + i] = matrix[offset + i];
|
|
1650
|
+
colorMatrixCount += 1;
|
|
1651
|
+
return colorMatrixCount - 1;
|
|
1652
|
+
}
|
|
1653
|
+
function ensureCapacity() {
|
|
1654
|
+
const needed = (quadCount + 1) * 18;
|
|
1655
|
+
if (needed <= instances.length) return;
|
|
1656
|
+
let capacity = Math.max(18, instances.length);
|
|
1657
|
+
while (capacity < needed) capacity *= 2;
|
|
1658
|
+
const grown = new Float32Array(capacity);
|
|
1659
|
+
grown.set(instances);
|
|
1660
|
+
instances = grown;
|
|
1661
|
+
stats.arenaGrowths += 1;
|
|
1662
|
+
}
|
|
1663
|
+
return {
|
|
1664
|
+
maxTextureSlots,
|
|
1665
|
+
maxColorMatrices,
|
|
1666
|
+
get quadCount() {
|
|
1667
|
+
return quadCount;
|
|
1668
|
+
},
|
|
1669
|
+
get textureCount() {
|
|
1670
|
+
return textureCount;
|
|
1671
|
+
},
|
|
1672
|
+
get colorMatrixCount() {
|
|
1673
|
+
return colorMatrixCount;
|
|
1674
|
+
},
|
|
1675
|
+
get blend() {
|
|
1676
|
+
return blend;
|
|
1677
|
+
},
|
|
1678
|
+
stats,
|
|
1679
|
+
quad,
|
|
1680
|
+
reset() {
|
|
1681
|
+
quadCount = 0;
|
|
1682
|
+
for (let i = 0; i < textureCount; i += 1) textures[i] = null;
|
|
1683
|
+
textureCount = 0;
|
|
1684
|
+
colorMatrixCount = 1;
|
|
1685
|
+
blend = 0;
|
|
1686
|
+
clipEpoch = 0;
|
|
1687
|
+
stats.batches = 0;
|
|
1688
|
+
stats.quads = 0;
|
|
1689
|
+
stats.textureBinds = 0;
|
|
1690
|
+
stats.maxBatchQuads = 0;
|
|
1691
|
+
stats.arenaGrowths = 0;
|
|
1692
|
+
zeroFlushCounts(stats.flushes);
|
|
1693
|
+
},
|
|
1694
|
+
setBlend(next) {
|
|
1695
|
+
if (next === blend) return;
|
|
1696
|
+
flush("blend");
|
|
1697
|
+
blend = next;
|
|
1698
|
+
},
|
|
1699
|
+
setClipEpoch(epoch) {
|
|
1700
|
+
if (epoch === clipEpoch) return;
|
|
1701
|
+
flush("clip");
|
|
1702
|
+
clipEpoch = epoch;
|
|
1703
|
+
},
|
|
1704
|
+
push(texture, colorMatrix = null, colorMatrixOffset = 0) {
|
|
1705
|
+
let slot = slotFor(texture);
|
|
1706
|
+
if (slot < 0) {
|
|
1707
|
+
flush("textureSlots");
|
|
1708
|
+
slot = slotFor(texture);
|
|
1709
|
+
}
|
|
1710
|
+
let matrixSlot = 0;
|
|
1711
|
+
if (colorMatrix) {
|
|
1712
|
+
matrixSlot = matrixSlotFor(colorMatrix, colorMatrixOffset);
|
|
1713
|
+
if (matrixSlot < 0) {
|
|
1714
|
+
flush("colorMatrices");
|
|
1715
|
+
slot = slotFor(texture);
|
|
1716
|
+
matrixSlot = matrixSlotFor(colorMatrix, colorMatrixOffset);
|
|
1717
|
+
if (matrixSlot < 0) matrixSlot = 0;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
ensureCapacity();
|
|
1721
|
+
const at = quadCount * 18;
|
|
1722
|
+
instances[at] = quad.x0;
|
|
1723
|
+
instances[at + 1] = quad.y0;
|
|
1724
|
+
instances[at + 2] = quad.x1;
|
|
1725
|
+
instances[at + 3] = quad.y1;
|
|
1726
|
+
instances[at + 4] = quad.x2;
|
|
1727
|
+
instances[at + 5] = quad.y2;
|
|
1728
|
+
instances[at + 6] = quad.x3;
|
|
1729
|
+
instances[at + 7] = quad.y3;
|
|
1730
|
+
instances[at + 8] = quad.u0;
|
|
1731
|
+
instances[at + 9] = quad.v0;
|
|
1732
|
+
instances[at + 10] = quad.uSpan;
|
|
1733
|
+
instances[at + 11] = quad.vSpan;
|
|
1734
|
+
instances[at + 12] = quad.r;
|
|
1735
|
+
instances[at + 13] = quad.g;
|
|
1736
|
+
instances[at + 14] = quad.b;
|
|
1737
|
+
instances[at + 15] = quad.a;
|
|
1738
|
+
instances[at + 16] = slot;
|
|
1739
|
+
instances[at + 17] = matrixSlot;
|
|
1740
|
+
quadCount += 1;
|
|
1741
|
+
stats.quads += 1;
|
|
1742
|
+
},
|
|
1743
|
+
flush
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
//#endregion
|
|
1747
|
+
//#region src/clip-stack.ts
|
|
1748
|
+
function createEntry() {
|
|
1749
|
+
return {
|
|
1750
|
+
minX: 0,
|
|
1751
|
+
minY: 0,
|
|
1752
|
+
maxX: 0,
|
|
1753
|
+
maxY: 0,
|
|
1754
|
+
roundedCenterX: 0,
|
|
1755
|
+
roundedCenterY: 0,
|
|
1756
|
+
roundedHalfWidth: 0,
|
|
1757
|
+
roundedHalfHeight: 0,
|
|
1758
|
+
roundedRadius: 0
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
function createScissorBox() {
|
|
1762
|
+
return {
|
|
1763
|
+
x: 0,
|
|
1764
|
+
y: 0,
|
|
1765
|
+
width: 0,
|
|
1766
|
+
height: 0
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
/** True when `transform` is a pure scale + translate, i.e. a scissor can be exact. */
|
|
1770
|
+
function isAxisAligned(transform) {
|
|
1771
|
+
return transform[1] === 0 && transform[2] === 0;
|
|
1772
|
+
}
|
|
1773
|
+
function createClipStack() {
|
|
1774
|
+
const entries = [];
|
|
1775
|
+
let depth = 0;
|
|
1776
|
+
let epoch = 0;
|
|
1777
|
+
let rotatedFallbacks = 0;
|
|
1778
|
+
const boundsOut = {
|
|
1779
|
+
minX: 0,
|
|
1780
|
+
minY: 0,
|
|
1781
|
+
maxX: 0,
|
|
1782
|
+
maxY: 0
|
|
1783
|
+
};
|
|
1784
|
+
const roundedOut = {
|
|
1785
|
+
centerX: 0,
|
|
1786
|
+
centerY: 0,
|
|
1787
|
+
halfWidth: 0,
|
|
1788
|
+
halfHeight: 0,
|
|
1789
|
+
radius: 0
|
|
1790
|
+
};
|
|
1791
|
+
return {
|
|
1792
|
+
get depth() {
|
|
1793
|
+
return depth;
|
|
1794
|
+
},
|
|
1795
|
+
get epoch() {
|
|
1796
|
+
return epoch;
|
|
1797
|
+
},
|
|
1798
|
+
get rotatedFallbacks() {
|
|
1799
|
+
return rotatedFallbacks;
|
|
1800
|
+
},
|
|
1801
|
+
bounds() {
|
|
1802
|
+
if (depth === 0) return null;
|
|
1803
|
+
const top = entries[depth - 1];
|
|
1804
|
+
boundsOut.minX = top.minX;
|
|
1805
|
+
boundsOut.minY = top.minY;
|
|
1806
|
+
boundsOut.maxX = top.maxX;
|
|
1807
|
+
boundsOut.maxY = top.maxY;
|
|
1808
|
+
return boundsOut;
|
|
1809
|
+
},
|
|
1810
|
+
rounded() {
|
|
1811
|
+
if (depth === 0) return null;
|
|
1812
|
+
const top = entries[depth - 1];
|
|
1813
|
+
if (!(top.roundedRadius > 0)) return null;
|
|
1814
|
+
roundedOut.centerX = top.roundedCenterX;
|
|
1815
|
+
roundedOut.centerY = top.roundedCenterY;
|
|
1816
|
+
roundedOut.halfWidth = top.roundedHalfWidth;
|
|
1817
|
+
roundedOut.halfHeight = top.roundedHalfHeight;
|
|
1818
|
+
roundedOut.radius = top.roundedRadius;
|
|
1819
|
+
return roundedOut;
|
|
1820
|
+
},
|
|
1821
|
+
push(clip) {
|
|
1822
|
+
while (entries.length <= depth) entries.push(createEntry());
|
|
1823
|
+
const entry = entries[depth];
|
|
1824
|
+
const outset = Math.max(0, clip.outsetX);
|
|
1825
|
+
let minX = clip.x - outset;
|
|
1826
|
+
let minY = clip.y;
|
|
1827
|
+
let maxX = clip.x + clip.w + outset;
|
|
1828
|
+
let maxY = clip.y + clip.h;
|
|
1829
|
+
const parent = depth > 0 ? entries[depth - 1] : null;
|
|
1830
|
+
if (parent) {
|
|
1831
|
+
if (parent.minX > minX) minX = parent.minX;
|
|
1832
|
+
if (parent.minY > minY) minY = parent.minY;
|
|
1833
|
+
if (parent.maxX < maxX) maxX = parent.maxX;
|
|
1834
|
+
if (parent.maxY < maxY) maxY = parent.maxY;
|
|
1835
|
+
}
|
|
1836
|
+
entry.minX = minX;
|
|
1837
|
+
entry.minY = minY;
|
|
1838
|
+
entry.maxX = Math.max(minX, maxX);
|
|
1839
|
+
entry.maxY = Math.max(minY, maxY);
|
|
1840
|
+
if (clip.cornerRadius > 0) {
|
|
1841
|
+
const halfWidth = (clip.w + outset * 2) / 2;
|
|
1842
|
+
const halfHeight = clip.h / 2;
|
|
1843
|
+
entry.roundedCenterX = clip.x - outset + halfWidth;
|
|
1844
|
+
entry.roundedCenterY = clip.y + halfHeight;
|
|
1845
|
+
entry.roundedHalfWidth = Math.max(0, halfWidth);
|
|
1846
|
+
entry.roundedHalfHeight = Math.max(0, halfHeight);
|
|
1847
|
+
entry.roundedRadius = Math.min(clip.cornerRadius, entry.roundedHalfWidth, entry.roundedHalfHeight);
|
|
1848
|
+
} else if (parent) {
|
|
1849
|
+
entry.roundedCenterX = parent.roundedCenterX;
|
|
1850
|
+
entry.roundedCenterY = parent.roundedCenterY;
|
|
1851
|
+
entry.roundedHalfWidth = parent.roundedHalfWidth;
|
|
1852
|
+
entry.roundedHalfHeight = parent.roundedHalfHeight;
|
|
1853
|
+
entry.roundedRadius = parent.roundedRadius;
|
|
1854
|
+
} else entry.roundedRadius = 0;
|
|
1855
|
+
depth += 1;
|
|
1856
|
+
epoch += 1;
|
|
1857
|
+
},
|
|
1858
|
+
pop() {
|
|
1859
|
+
if (depth === 0) throw new RangeError("clip stack pop with no clip pushed");
|
|
1860
|
+
depth -= 1;
|
|
1861
|
+
epoch += 1;
|
|
1862
|
+
},
|
|
1863
|
+
reset() {
|
|
1864
|
+
if (depth !== 0) epoch += 1;
|
|
1865
|
+
depth = 0;
|
|
1866
|
+
rotatedFallbacks = 0;
|
|
1867
|
+
},
|
|
1868
|
+
scissor(transform, width, height, out) {
|
|
1869
|
+
if (depth === 0) {
|
|
1870
|
+
out.x = 0;
|
|
1871
|
+
out.y = 0;
|
|
1872
|
+
out.width = Math.max(0, width);
|
|
1873
|
+
out.height = Math.max(0, height);
|
|
1874
|
+
return out;
|
|
1875
|
+
}
|
|
1876
|
+
const top = entries[depth - 1];
|
|
1877
|
+
const xx = transform[0];
|
|
1878
|
+
const xy = transform[1];
|
|
1879
|
+
const yx = transform[2];
|
|
1880
|
+
const yy = transform[3];
|
|
1881
|
+
const ox = transform[4];
|
|
1882
|
+
const oy = transform[5];
|
|
1883
|
+
if (xy !== 0 || yx !== 0) rotatedFallbacks += 1;
|
|
1884
|
+
let minPx = Number.POSITIVE_INFINITY;
|
|
1885
|
+
let minPy = Number.POSITIVE_INFINITY;
|
|
1886
|
+
let maxPx = Number.NEGATIVE_INFINITY;
|
|
1887
|
+
let maxPy = Number.NEGATIVE_INFINITY;
|
|
1888
|
+
for (let corner = 0; corner < 4; corner += 1) {
|
|
1889
|
+
const x = corner === 0 || corner === 3 ? top.minX : top.maxX;
|
|
1890
|
+
const y = corner < 2 ? top.minY : top.maxY;
|
|
1891
|
+
const px = xx * x + yx * y + ox;
|
|
1892
|
+
const py = xy * x + yy * y + oy;
|
|
1893
|
+
if (px < minPx) minPx = px;
|
|
1894
|
+
if (px > maxPx) maxPx = px;
|
|
1895
|
+
if (py < minPy) minPy = py;
|
|
1896
|
+
if (py > maxPy) maxPy = py;
|
|
1897
|
+
}
|
|
1898
|
+
const left = clampInt(Math.floor(minPx), 0, width);
|
|
1899
|
+
const right = clampInt(Math.ceil(maxPx), 0, width);
|
|
1900
|
+
const top_ = clampInt(Math.floor(minPy), 0, height);
|
|
1901
|
+
const bottom = clampInt(Math.ceil(maxPy), 0, height);
|
|
1902
|
+
out.x = left;
|
|
1903
|
+
out.width = Math.max(0, right - left);
|
|
1904
|
+
out.y = Math.max(0, height - bottom);
|
|
1905
|
+
out.height = Math.max(0, bottom - top_);
|
|
1906
|
+
return out;
|
|
1907
|
+
}
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
function clampInt(value, low, high) {
|
|
1911
|
+
if (!Number.isFinite(value)) return value < 0 ? low : high;
|
|
1912
|
+
return value < low ? low : value > high ? high : value;
|
|
1913
|
+
}
|
|
1914
|
+
//#endregion
|
|
1915
|
+
//#region src/damage.ts
|
|
1916
|
+
/** Retained replay uses fixed physical-pixel tiles, never CSS pixels. */
|
|
1917
|
+
const RETAINED_DAMAGE_TILE_SIZE = 64;
|
|
1918
|
+
/** The largest tile coverage that is worth planning for retained replay. */
|
|
1919
|
+
const RETAINED_MAX_DAMAGE_COVERAGE = .2;
|
|
1920
|
+
function createDamageRect() {
|
|
1921
|
+
return {
|
|
1922
|
+
x: 0,
|
|
1923
|
+
y: 0,
|
|
1924
|
+
width: 0,
|
|
1925
|
+
height: 0
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
function isDamageEmpty(rect) {
|
|
1929
|
+
return !(rect.width > 0 && rect.height > 0);
|
|
1930
|
+
}
|
|
1931
|
+
/** Expand in the rectangle's own coordinate space without mutating inputs. */
|
|
1932
|
+
function outsetDamageRect(rect, outset, out = createDamageRect()) {
|
|
1933
|
+
if (!isFiniteDamageRect(rect) || !Number.isFinite(outset) || outset < 0) return null;
|
|
1934
|
+
out.x = rect.x - outset;
|
|
1935
|
+
out.y = rect.y - outset;
|
|
1936
|
+
out.width = rect.width + outset * 2;
|
|
1937
|
+
out.height = rect.height + outset * 2;
|
|
1938
|
+
return isFiniteDamageRect(out) ? out : null;
|
|
1939
|
+
}
|
|
1940
|
+
/** True when two half-open rectangles overlap. Touching edges do not repaint. */
|
|
1941
|
+
function damageIntersects(a, b) {
|
|
1942
|
+
return a.width > 0 && a.height > 0 && b.width > 0 && b.height > 0 && a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
1943
|
+
}
|
|
1944
|
+
/** Expand `out` to cover both input rectangles. */
|
|
1945
|
+
function unionDamageRect(a, b, out = createDamageRect()) {
|
|
1946
|
+
if (isDamageEmpty(a)) {
|
|
1947
|
+
out.x = b.x;
|
|
1948
|
+
out.y = b.y;
|
|
1949
|
+
out.width = Math.max(0, b.width);
|
|
1950
|
+
out.height = Math.max(0, b.height);
|
|
1951
|
+
return out;
|
|
1952
|
+
}
|
|
1953
|
+
if (isDamageEmpty(b)) {
|
|
1954
|
+
out.x = a.x;
|
|
1955
|
+
out.y = a.y;
|
|
1956
|
+
out.width = Math.max(0, a.width);
|
|
1957
|
+
out.height = Math.max(0, a.height);
|
|
1958
|
+
return out;
|
|
1959
|
+
}
|
|
1960
|
+
const minX = Math.min(a.x, b.x);
|
|
1961
|
+
const minY = Math.min(a.y, b.y);
|
|
1962
|
+
const maxX = Math.max(a.x + a.width, b.x + b.width);
|
|
1963
|
+
const maxY = Math.max(a.y + a.height, b.y + b.height);
|
|
1964
|
+
out.x = minX;
|
|
1965
|
+
out.y = minY;
|
|
1966
|
+
out.width = Math.max(0, maxX - minX);
|
|
1967
|
+
out.height = Math.max(0, maxY - minY);
|
|
1968
|
+
return out;
|
|
1969
|
+
}
|
|
1970
|
+
/** Transform all four corners, returning the enclosing axis-aligned rectangle. */
|
|
1971
|
+
function transformDamageRect(rect, transform, out = createDamageRect()) {
|
|
1972
|
+
return transformDamageValues(rect.x, rect.y, rect.width, rect.height, transform, out);
|
|
1973
|
+
}
|
|
1974
|
+
/** Allocation-free coordinate form used by command bounds in retained loops. */
|
|
1975
|
+
function transformDamageValues(x0, y0, width, height, transform, out, transformOffset = 0) {
|
|
1976
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
1977
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
1978
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
1979
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
1980
|
+
for (let corner = 0; corner < 4; corner += 1) {
|
|
1981
|
+
const x = corner === 0 || corner === 3 ? x0 : x0 + width;
|
|
1982
|
+
const y = corner < 2 ? y0 : y0 + height;
|
|
1983
|
+
const px = transform[transformOffset] * x + transform[transformOffset + 2] * y + transform[transformOffset + 4];
|
|
1984
|
+
const py = transform[transformOffset + 1] * x + transform[transformOffset + 3] * y + transform[transformOffset + 5];
|
|
1985
|
+
minX = Math.min(minX, px);
|
|
1986
|
+
minY = Math.min(minY, py);
|
|
1987
|
+
maxX = Math.max(maxX, px);
|
|
1988
|
+
maxY = Math.max(maxY, py);
|
|
1989
|
+
}
|
|
1990
|
+
out.x = minX;
|
|
1991
|
+
out.y = minY;
|
|
1992
|
+
out.width = Math.max(0, maxX - minX);
|
|
1993
|
+
out.height = Math.max(0, maxY - minY);
|
|
1994
|
+
return out;
|
|
1995
|
+
}
|
|
1996
|
+
function hasFiniteTransform(transform, transformOffset) {
|
|
1997
|
+
for (let index = 0; index < 6; index += 1) if (!Number.isFinite(transform[transformOffset + index])) return false;
|
|
1998
|
+
return true;
|
|
1999
|
+
}
|
|
2000
|
+
function isFiniteDamageRect(rect) {
|
|
2001
|
+
return Number.isFinite(rect.x) && Number.isFinite(rect.y) && Number.isFinite(rect.width) && Number.isFinite(rect.height) && rect.width >= 0 && rect.height >= 0;
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Compute a conservative visual bound for one command. `null` means unknown,
|
|
2005
|
+
* which callers must treat as intersecting every damage region. Glyphs are
|
|
2006
|
+
* known only when their producer supplied explicit local ink bounds; guessing
|
|
2007
|
+
* from pen positions or em size could leave stale ink on a retained surface.
|
|
2008
|
+
*/
|
|
2009
|
+
function commandDamageBounds(list, index, out = createDamageRect()) {
|
|
2010
|
+
const kind = list.kindAt(index);
|
|
2011
|
+
if (kind === 7 || kind === 8) return null;
|
|
2012
|
+
if (kind === 0 || kind === 1) {
|
|
2013
|
+
const at = list.floatOffsetAt(index);
|
|
2014
|
+
const floats = list.floats;
|
|
2015
|
+
const w = floats[at + 6];
|
|
2016
|
+
const h = floats[at + 7];
|
|
2017
|
+
if (!hasFiniteTransform(floats, at) || !Number.isFinite(w) || !Number.isFinite(h) || w < 0 || h < 0) return null;
|
|
2018
|
+
transformDamageValues(0, 0, w, h, floats, out, at);
|
|
2019
|
+
return isFiniteDamageRect(out) ? out : null;
|
|
2020
|
+
}
|
|
2021
|
+
if (kind === 2) {
|
|
2022
|
+
const at = list.floatOffsetAt(index);
|
|
2023
|
+
const ints = list.ints;
|
|
2024
|
+
const floats = list.floats;
|
|
2025
|
+
const count = ints[list.intOffsetAt(index)];
|
|
2026
|
+
if (count <= 0) {
|
|
2027
|
+
out.x = 0;
|
|
2028
|
+
out.y = 0;
|
|
2029
|
+
out.width = 0;
|
|
2030
|
+
out.height = 0;
|
|
2031
|
+
return out;
|
|
2032
|
+
}
|
|
2033
|
+
const width = floats[at];
|
|
2034
|
+
if (!Number.isFinite(width)) return null;
|
|
2035
|
+
const half = Math.abs(width) / 2;
|
|
2036
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
2037
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
2038
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
2039
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
2040
|
+
for (let point = 0; point < count; point += 1) {
|
|
2041
|
+
const x = floats[at + 5 + point * 2];
|
|
2042
|
+
const y = floats[at + 6 + point * 2];
|
|
2043
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
2044
|
+
minX = Math.min(minX, x - half);
|
|
2045
|
+
minY = Math.min(minY, y - half);
|
|
2046
|
+
maxX = Math.max(maxX, x + half);
|
|
2047
|
+
maxY = Math.max(maxY, y + half);
|
|
2048
|
+
}
|
|
2049
|
+
out.x = minX;
|
|
2050
|
+
out.y = minY;
|
|
2051
|
+
out.width = maxX - minX;
|
|
2052
|
+
out.height = maxY - minY;
|
|
2053
|
+
return isFiniteDamageRect(out) ? out : null;
|
|
2054
|
+
}
|
|
2055
|
+
if (kind === 6) {
|
|
2056
|
+
const at = list.floatOffsetAt(index);
|
|
2057
|
+
const intAt = list.intOffsetAt(index);
|
|
2058
|
+
const floats = list.floats;
|
|
2059
|
+
const vertexCount = list.ints[intAt];
|
|
2060
|
+
if (vertexCount <= 0) {
|
|
2061
|
+
out.x = 0;
|
|
2062
|
+
out.y = 0;
|
|
2063
|
+
out.width = 0;
|
|
2064
|
+
out.height = 0;
|
|
2065
|
+
return out;
|
|
2066
|
+
}
|
|
2067
|
+
const xx = floats[at];
|
|
2068
|
+
const xy = floats[at + 1];
|
|
2069
|
+
const yx = floats[at + 2];
|
|
2070
|
+
const yy = floats[at + 3];
|
|
2071
|
+
const ox = floats[at + 4];
|
|
2072
|
+
const oy = floats[at + 5];
|
|
2073
|
+
const positionsAt = at + 10;
|
|
2074
|
+
if (!hasFiniteTransform(floats, at)) return null;
|
|
2075
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
2076
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
2077
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
2078
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
2079
|
+
for (let vertex = 0; vertex < vertexCount; vertex += 1) {
|
|
2080
|
+
const x = floats[positionsAt + vertex * 2];
|
|
2081
|
+
const y = floats[positionsAt + vertex * 2 + 1];
|
|
2082
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
2083
|
+
const transformedX = xx * x + yx * y + ox;
|
|
2084
|
+
const transformedY = xy * x + yy * y + oy;
|
|
2085
|
+
minX = Math.min(minX, transformedX);
|
|
2086
|
+
minY = Math.min(minY, transformedY);
|
|
2087
|
+
maxX = Math.max(maxX, transformedX);
|
|
2088
|
+
maxY = Math.max(maxY, transformedY);
|
|
2089
|
+
}
|
|
2090
|
+
out.x = minX;
|
|
2091
|
+
out.y = minY;
|
|
2092
|
+
out.width = Math.max(0, maxX - minX);
|
|
2093
|
+
out.height = Math.max(0, maxY - minY);
|
|
2094
|
+
return isFiniteDamageRect(out) ? out : null;
|
|
2095
|
+
}
|
|
2096
|
+
if (kind === 5) {
|
|
2097
|
+
const at = list.floatOffsetAt(index);
|
|
2098
|
+
const floats = list.floats;
|
|
2099
|
+
const x = floats[at + 12];
|
|
2100
|
+
const y = floats[at + 13];
|
|
2101
|
+
const width = floats[at + 14];
|
|
2102
|
+
const height = floats[at + 15];
|
|
2103
|
+
const effectOutset = floats[at + 16];
|
|
2104
|
+
if (!hasFiniteTransform(floats, at) || !Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(effectOutset) || width < 0 || height < 0 || effectOutset < 0) return null;
|
|
2105
|
+
const spread = Math.abs(floats[at + 11]);
|
|
2106
|
+
const outset = effectOutset + (Number.isFinite(spread) ? spread : 0);
|
|
2107
|
+
transformDamageValues(x - outset, y - outset, width + outset * 2, height + outset * 2, floats, out, at);
|
|
2108
|
+
return isFiniteDamageRect(out) ? out : null;
|
|
2109
|
+
}
|
|
2110
|
+
if (kind === 3 || kind === 4) {
|
|
2111
|
+
out.x = 0;
|
|
2112
|
+
out.y = 0;
|
|
2113
|
+
out.width = 0;
|
|
2114
|
+
out.height = 0;
|
|
2115
|
+
return out;
|
|
2116
|
+
}
|
|
2117
|
+
return null;
|
|
2118
|
+
}
|
|
2119
|
+
function createDamageTiles(width, height, tileWidth = 64, tileHeight = tileWidth) {
|
|
2120
|
+
const tileW = Number.isFinite(tileWidth) ? Math.max(1, Math.floor(tileWidth)) : 64;
|
|
2121
|
+
const tileH = Number.isFinite(tileHeight) ? Math.max(1, Math.floor(tileHeight)) : tileW;
|
|
2122
|
+
let surfaceWidth = 0;
|
|
2123
|
+
let surfaceHeight = 0;
|
|
2124
|
+
let columns = 0;
|
|
2125
|
+
let rows = 0;
|
|
2126
|
+
let marked = new Uint8Array(0);
|
|
2127
|
+
let dirty = false;
|
|
2128
|
+
let dirtyCount = 0;
|
|
2129
|
+
let regionsStale = true;
|
|
2130
|
+
let regionCount = 0;
|
|
2131
|
+
const regionPool = [];
|
|
2132
|
+
let previousEnds = new Int32Array(0);
|
|
2133
|
+
let previousRegions = new Int32Array(0);
|
|
2134
|
+
let currentEnds = new Int32Array(0);
|
|
2135
|
+
let currentRegions = new Int32Array(0);
|
|
2136
|
+
const iterationTile = {
|
|
2137
|
+
column: 0,
|
|
2138
|
+
row: 0,
|
|
2139
|
+
x: 0,
|
|
2140
|
+
y: 0,
|
|
2141
|
+
width: 0,
|
|
2142
|
+
height: 0
|
|
2143
|
+
};
|
|
2144
|
+
function resize(nextWidth, nextHeight) {
|
|
2145
|
+
surfaceWidth = Number.isFinite(nextWidth) ? Math.max(0, Math.round(nextWidth)) : 0;
|
|
2146
|
+
surfaceHeight = Number.isFinite(nextHeight) ? Math.max(0, Math.round(nextHeight)) : 0;
|
|
2147
|
+
columns = Math.ceil(surfaceWidth / tileW);
|
|
2148
|
+
rows = Math.ceil(surfaceHeight / tileH);
|
|
2149
|
+
marked = new Uint8Array(columns * rows);
|
|
2150
|
+
dirty = false;
|
|
2151
|
+
dirtyCount = 0;
|
|
2152
|
+
regionsStale = true;
|
|
2153
|
+
previousEnds = new Int32Array(columns);
|
|
2154
|
+
previousRegions = new Int32Array(columns);
|
|
2155
|
+
currentEnds = new Int32Array(columns);
|
|
2156
|
+
currentRegions = new Int32Array(columns);
|
|
2157
|
+
}
|
|
2158
|
+
function mark(rect) {
|
|
2159
|
+
if (surfaceWidth === 0 || surfaceHeight === 0) return;
|
|
2160
|
+
if (!rect || !isFiniteDamageRect(rect)) {
|
|
2161
|
+
marked.fill(1);
|
|
2162
|
+
dirty = marked.length > 0;
|
|
2163
|
+
dirtyCount = marked.length;
|
|
2164
|
+
regionsStale = true;
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
if (isDamageEmpty(rect)) return;
|
|
2168
|
+
const left = Math.max(0, Math.floor(rect.x / tileW));
|
|
2169
|
+
const top = Math.max(0, Math.floor(rect.y / tileH));
|
|
2170
|
+
const right = Math.min(columns, Math.ceil((rect.x + rect.width) / tileW));
|
|
2171
|
+
const bottom = Math.min(rows, Math.ceil((rect.y + rect.height) / tileH));
|
|
2172
|
+
for (let row = top; row < bottom; row += 1) for (let column = left; column < right; column += 1) {
|
|
2173
|
+
const index = row * columns + column;
|
|
2174
|
+
if (marked[index] === 0) {
|
|
2175
|
+
marked[index] = 1;
|
|
2176
|
+
dirtyCount += 1;
|
|
2177
|
+
dirty = true;
|
|
2178
|
+
regionsStale = true;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
function tiles() {
|
|
2183
|
+
const result = [];
|
|
2184
|
+
for (let row = 0; row < rows; row += 1) for (let column = 0; column < columns; column += 1) {
|
|
2185
|
+
if (marked[row * columns + column] === 0) continue;
|
|
2186
|
+
const x = column * tileW;
|
|
2187
|
+
const y = row * tileH;
|
|
2188
|
+
result.push({
|
|
2189
|
+
column,
|
|
2190
|
+
row,
|
|
2191
|
+
x,
|
|
2192
|
+
y,
|
|
2193
|
+
width: Math.min(tileW, surfaceWidth - x),
|
|
2194
|
+
height: Math.min(tileH, surfaceHeight - y)
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
return result;
|
|
2198
|
+
}
|
|
2199
|
+
function forEach(callback) {
|
|
2200
|
+
for (let row = 0; row < rows; row += 1) for (let column = 0; column < columns; column += 1) {
|
|
2201
|
+
if (marked[row * columns + column] === 0) continue;
|
|
2202
|
+
const x = column * tileW;
|
|
2203
|
+
const y = row * tileH;
|
|
2204
|
+
iterationTile.column = column;
|
|
2205
|
+
iterationTile.row = row;
|
|
2206
|
+
iterationTile.x = x;
|
|
2207
|
+
iterationTile.y = y;
|
|
2208
|
+
iterationTile.width = Math.min(tileW, surfaceWidth - x);
|
|
2209
|
+
iterationTile.height = Math.min(tileH, surfaceHeight - y);
|
|
2210
|
+
callback(iterationTile);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Build a maximal exact rectangular cover from the tile bitset. A row is
|
|
2215
|
+
* first reduced to horizontal runs; only a run with the same column span in
|
|
2216
|
+
* the immediately preceding row extends its existing rectangle. That rule
|
|
2217
|
+
* is what prevents a T/L-shaped set of tiles from filling its clean corner.
|
|
2218
|
+
*/
|
|
2219
|
+
function rebuildRegions() {
|
|
2220
|
+
if (!regionsStale) return;
|
|
2221
|
+
regionsStale = false;
|
|
2222
|
+
regionCount = 0;
|
|
2223
|
+
previousEnds.fill(-1);
|
|
2224
|
+
previousRegions.fill(-1);
|
|
2225
|
+
for (let row = 0; row < rows; row += 1) {
|
|
2226
|
+
currentEnds.fill(-1);
|
|
2227
|
+
currentRegions.fill(-1);
|
|
2228
|
+
const y = row * tileH;
|
|
2229
|
+
const height = Math.min(tileH, surfaceHeight - y);
|
|
2230
|
+
for (let column = 0; column < columns;) {
|
|
2231
|
+
if (marked[row * columns + column] === 0) {
|
|
2232
|
+
column += 1;
|
|
2233
|
+
continue;
|
|
2234
|
+
}
|
|
2235
|
+
const start = column;
|
|
2236
|
+
column += 1;
|
|
2237
|
+
while (column < columns && marked[row * columns + column] !== 0) column += 1;
|
|
2238
|
+
const end = column;
|
|
2239
|
+
let regionIndex = -1;
|
|
2240
|
+
if (previousEnds[start] === end) regionIndex = previousRegions[start];
|
|
2241
|
+
if (regionIndex >= 0) regionPool[regionIndex].height += height;
|
|
2242
|
+
else {
|
|
2243
|
+
regionIndex = regionCount;
|
|
2244
|
+
regionCount += 1;
|
|
2245
|
+
const region = regionPool[regionIndex] ?? createDamageRect();
|
|
2246
|
+
region.x = start * tileW;
|
|
2247
|
+
region.y = y;
|
|
2248
|
+
region.width = Math.min(surfaceWidth, end * tileW) - region.x;
|
|
2249
|
+
region.height = height;
|
|
2250
|
+
regionPool[regionIndex] = region;
|
|
2251
|
+
}
|
|
2252
|
+
currentEnds[start] = end;
|
|
2253
|
+
currentRegions[start] = regionIndex;
|
|
2254
|
+
}
|
|
2255
|
+
[previousEnds, currentEnds] = [currentEnds, previousEnds];
|
|
2256
|
+
[previousRegions, currentRegions] = [currentRegions, previousRegions];
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
function forEachRegion(callback) {
|
|
2260
|
+
rebuildRegions();
|
|
2261
|
+
for (let index = 0; index < regionCount; index += 1) callback(regionPool[index]);
|
|
2262
|
+
}
|
|
2263
|
+
resize(width, height);
|
|
2264
|
+
return {
|
|
2265
|
+
get width() {
|
|
2266
|
+
return surfaceWidth;
|
|
2267
|
+
},
|
|
2268
|
+
get height() {
|
|
2269
|
+
return surfaceHeight;
|
|
2270
|
+
},
|
|
2271
|
+
get tileWidth() {
|
|
2272
|
+
return tileW;
|
|
2273
|
+
},
|
|
2274
|
+
get tileHeight() {
|
|
2275
|
+
return tileH;
|
|
2276
|
+
},
|
|
2277
|
+
get columns() {
|
|
2278
|
+
return columns;
|
|
2279
|
+
},
|
|
2280
|
+
get rows() {
|
|
2281
|
+
return rows;
|
|
2282
|
+
},
|
|
2283
|
+
get tileCount() {
|
|
2284
|
+
return marked.length;
|
|
2285
|
+
},
|
|
2286
|
+
get dirtyCount() {
|
|
2287
|
+
return dirtyCount;
|
|
2288
|
+
},
|
|
2289
|
+
get coverage() {
|
|
2290
|
+
return marked.length === 0 ? 0 : dirtyCount / marked.length;
|
|
2291
|
+
},
|
|
2292
|
+
get dirty() {
|
|
2293
|
+
return dirty;
|
|
2294
|
+
},
|
|
2295
|
+
resize,
|
|
2296
|
+
mark,
|
|
2297
|
+
clear() {
|
|
2298
|
+
marked.fill(0);
|
|
2299
|
+
dirty = false;
|
|
2300
|
+
dirtyCount = 0;
|
|
2301
|
+
regionsStale = true;
|
|
2302
|
+
},
|
|
2303
|
+
tiles,
|
|
2304
|
+
forEach,
|
|
2305
|
+
forEachRegion,
|
|
2306
|
+
consume() {
|
|
2307
|
+
const result = tiles();
|
|
2308
|
+
marked.fill(0);
|
|
2309
|
+
dirty = false;
|
|
2310
|
+
dirtyCount = 0;
|
|
2311
|
+
regionsStale = true;
|
|
2312
|
+
return result;
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
//#endregion
|
|
2317
|
+
//#region src/replay.ts
|
|
2318
|
+
/** Retained replay must stay below this fraction of the complete painter list. */
|
|
2319
|
+
const RETAINED_MAX_REPLAY_FRACTION = .4;
|
|
2320
|
+
/**
|
|
2321
|
+
* Largest command count that is strictly below the retained replay threshold.
|
|
2322
|
+
* Clip pushes/pops are commands too: omitting them would not restore the
|
|
2323
|
+
* original painter state, so they count against the same budget.
|
|
2324
|
+
*/
|
|
2325
|
+
function maxPartialReplayCommands(commandCount) {
|
|
2326
|
+
if (!Number.isFinite(commandCount) || commandCount <= 0) return 0;
|
|
2327
|
+
return Math.max(0, Math.ceil(commandCount * RETAINED_MAX_REPLAY_FRACTION) - 1);
|
|
2328
|
+
}
|
|
2329
|
+
const REPLAY_MASK_SCRATCH = Symbol("ReplayMaskScratch");
|
|
2330
|
+
const replayMaskFrames = /* @__PURE__ */ new WeakMap();
|
|
2331
|
+
/** True only for masks created by this module and safe for a partial FBO replay. */
|
|
2332
|
+
function isPartialReplayMask(mask, list) {
|
|
2333
|
+
const frame = replayMaskFrames.get(mask);
|
|
2334
|
+
return mask[REPLAY_MASK_SCRATCH] === true && mask.selected && !mask.requiresFullReplay && (list === void 0 || frame?.list === list && frame.structuralRevision === list.structuralRevision && frame.contentRevision === list.contentRevision);
|
|
2335
|
+
}
|
|
2336
|
+
function isFiniteBounds(bounds) {
|
|
2337
|
+
return Number.isFinite(bounds.x) && Number.isFinite(bounds.y) && Number.isFinite(bounds.width) && Number.isFinite(bounds.height) && bounds.width >= 0 && bounds.height >= 0;
|
|
2338
|
+
}
|
|
2339
|
+
/** Intersect `bounds` after applying final-coordinate reach without mutating a
|
|
2340
|
+
* provider-owned cached rectangle. */
|
|
2341
|
+
function boundsIntersectsDamage(bounds, damage, outset) {
|
|
2342
|
+
const x = bounds.x - outset;
|
|
2343
|
+
const y = bounds.y - outset;
|
|
2344
|
+
const width = bounds.width + outset * 2;
|
|
2345
|
+
const height = bounds.height + outset * 2;
|
|
2346
|
+
return width > 0 && height > 0 && damage.width > 0 && damage.height > 0 && x < damage.x + damage.width && x + width > damage.x && y < damage.y + damage.height && y + height > damage.y;
|
|
2347
|
+
}
|
|
2348
|
+
function createReplayMaskScratch(capacity = 0) {
|
|
2349
|
+
let selected = new Uint8Array(Math.max(1, capacity));
|
|
2350
|
+
let open = new Int32Array(Math.max(1, capacity));
|
|
2351
|
+
let openCount = 0;
|
|
2352
|
+
const output = [];
|
|
2353
|
+
const bounds = {
|
|
2354
|
+
x: 0,
|
|
2355
|
+
y: 0,
|
|
2356
|
+
width: 0,
|
|
2357
|
+
height: 0
|
|
2358
|
+
};
|
|
2359
|
+
const transformed = {
|
|
2360
|
+
x: 0,
|
|
2361
|
+
y: 0,
|
|
2362
|
+
width: 0,
|
|
2363
|
+
height: 0
|
|
2364
|
+
};
|
|
2365
|
+
let requiresFullReplay = false;
|
|
2366
|
+
let thresholdExceeded = false;
|
|
2367
|
+
let selectedForFrame = false;
|
|
2368
|
+
let selectionLimit;
|
|
2369
|
+
function ensure(count) {
|
|
2370
|
+
if (selected.length >= count) return;
|
|
2371
|
+
let size = selected.length;
|
|
2372
|
+
while (size < count) size *= 2;
|
|
2373
|
+
selected = new Uint8Array(size);
|
|
2374
|
+
open = new Int32Array(size);
|
|
2375
|
+
}
|
|
2376
|
+
function selectIndex(index, count) {
|
|
2377
|
+
if (selected[index] !== 0) return true;
|
|
2378
|
+
selected[index] = 1;
|
|
2379
|
+
output.push(index);
|
|
2380
|
+
if (selectionLimit !== void 0 && output.length > selectionLimit) {
|
|
2381
|
+
thresholdExceeded = true;
|
|
2382
|
+
requiresFullReplay = true;
|
|
2383
|
+
output.length = 0;
|
|
2384
|
+
selected.fill(0, 0, count);
|
|
2385
|
+
return false;
|
|
2386
|
+
}
|
|
2387
|
+
return true;
|
|
2388
|
+
}
|
|
2389
|
+
function finish(list) {
|
|
2390
|
+
selectedForFrame = true;
|
|
2391
|
+
replayMaskFrames.set(scratch, {
|
|
2392
|
+
list,
|
|
2393
|
+
structuralRevision: list.structuralRevision,
|
|
2394
|
+
contentRevision: list.contentRevision
|
|
2395
|
+
});
|
|
2396
|
+
return scratch;
|
|
2397
|
+
}
|
|
2398
|
+
const scratch = {
|
|
2399
|
+
get count() {
|
|
2400
|
+
return output.length;
|
|
2401
|
+
},
|
|
2402
|
+
get requiresFullReplay() {
|
|
2403
|
+
return requiresFullReplay;
|
|
2404
|
+
},
|
|
2405
|
+
get thresholdExceeded() {
|
|
2406
|
+
return thresholdExceeded;
|
|
2407
|
+
},
|
|
2408
|
+
get selected() {
|
|
2409
|
+
return selectedForFrame;
|
|
2410
|
+
},
|
|
2411
|
+
[REPLAY_MASK_SCRATCH]: true,
|
|
2412
|
+
includes(index) {
|
|
2413
|
+
return index >= 0 && index < selected.length && selected[index] !== 0;
|
|
2414
|
+
},
|
|
2415
|
+
indices() {
|
|
2416
|
+
return output;
|
|
2417
|
+
},
|
|
2418
|
+
select(list, damage, options) {
|
|
2419
|
+
ensure(list.count);
|
|
2420
|
+
selected.fill(0, 0, list.count);
|
|
2421
|
+
output.length = 0;
|
|
2422
|
+
openCount = 0;
|
|
2423
|
+
requiresFullReplay = false;
|
|
2424
|
+
thresholdExceeded = false;
|
|
2425
|
+
selectedForFrame = false;
|
|
2426
|
+
selectionLimit = options?.maxCommands;
|
|
2427
|
+
if (selectionLimit !== void 0 && (!Number.isFinite(selectionLimit) || selectionLimit < 0)) {
|
|
2428
|
+
requiresFullReplay = true;
|
|
2429
|
+
return finish(list);
|
|
2430
|
+
}
|
|
2431
|
+
if (selectionLimit !== void 0) selectionLimit = Math.floor(selectionLimit);
|
|
2432
|
+
const rasterOutset = options?.rasterOutset ?? 1;
|
|
2433
|
+
if (!Number.isFinite(rasterOutset) || rasterOutset < 0) {
|
|
2434
|
+
requiresFullReplay = true;
|
|
2435
|
+
return finish(list);
|
|
2436
|
+
}
|
|
2437
|
+
selection: for (let index = 0; index < list.count; index += 1) {
|
|
2438
|
+
const kind = list.kindAt(index);
|
|
2439
|
+
if (kind === 7 || kind === 8) {
|
|
2440
|
+
requiresFullReplay = true;
|
|
2441
|
+
output.length = 0;
|
|
2442
|
+
selected.fill(0, 0, list.count);
|
|
2443
|
+
break;
|
|
2444
|
+
}
|
|
2445
|
+
if (kind === 3) {
|
|
2446
|
+
open[openCount] = index;
|
|
2447
|
+
openCount += 1;
|
|
2448
|
+
continue;
|
|
2449
|
+
}
|
|
2450
|
+
if (kind === 4) {
|
|
2451
|
+
if (openCount === 0) {
|
|
2452
|
+
requiresFullReplay = true;
|
|
2453
|
+
output.length = 0;
|
|
2454
|
+
selected.fill(0, 0, list.count);
|
|
2455
|
+
break;
|
|
2456
|
+
}
|
|
2457
|
+
openCount -= 1;
|
|
2458
|
+
const push = open[openCount];
|
|
2459
|
+
if (selected[push] !== 0 && !selectIndex(index, list.count)) break;
|
|
2460
|
+
continue;
|
|
2461
|
+
}
|
|
2462
|
+
const commandBounds = options?.boundsAt ? options.boundsAt(index, bounds) : commandDamageBounds(list, index, bounds);
|
|
2463
|
+
const mapped = commandBounds && options?.transform ? transformDamageRect(commandBounds, options.transform, transformed) : commandBounds;
|
|
2464
|
+
if (mapped === null && options?.unknownBounds !== "select") {
|
|
2465
|
+
requiresFullReplay = true;
|
|
2466
|
+
output.length = 0;
|
|
2467
|
+
selected.fill(0, 0, list.count);
|
|
2468
|
+
break;
|
|
2469
|
+
}
|
|
2470
|
+
if (mapped !== null && !isFiniteBounds(mapped)) {
|
|
2471
|
+
requiresFullReplay = true;
|
|
2472
|
+
output.length = 0;
|
|
2473
|
+
selected.fill(0, 0, list.count);
|
|
2474
|
+
break;
|
|
2475
|
+
}
|
|
2476
|
+
if (mapped !== null && !boundsIntersectsDamage(mapped, damage, rasterOutset)) continue;
|
|
2477
|
+
for (let depth = 0; depth < openCount; depth += 1) if (!selectIndex(open[depth], list.count)) break selection;
|
|
2478
|
+
if (!selectIndex(index, list.count)) break;
|
|
2479
|
+
}
|
|
2480
|
+
if (openCount !== 0 && !requiresFullReplay) {
|
|
2481
|
+
requiresFullReplay = true;
|
|
2482
|
+
output.length = 0;
|
|
2483
|
+
selected.fill(0, 0, list.count);
|
|
2484
|
+
}
|
|
2485
|
+
return finish(list);
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2488
|
+
return scratch;
|
|
2489
|
+
}
|
|
2490
|
+
/**
|
|
2491
|
+
* Select just the commands that can change `damage`, preserving all clip pushes
|
|
2492
|
+
* and pops needed to replay them from an empty clip stack. Unknown bounds are
|
|
2493
|
+
* selected conservatively. This is deliberately a mask rather than a copied
|
|
2494
|
+
* command list: all reads stay in the original retained arenas.
|
|
2495
|
+
*/
|
|
2496
|
+
function createReplayMask(list, damage, options) {
|
|
2497
|
+
return createReplayMaskScratch(list.count).select(list, damage, options);
|
|
2498
|
+
}
|
|
2499
|
+
//#endregion
|
|
2500
|
+
//#region src/compiled-draw-list.ts
|
|
2501
|
+
const TEMPLATE_FLOATS = 16;
|
|
2502
|
+
const NO_TEMPLATE = -1;
|
|
2503
|
+
function compileDrawList(list) {
|
|
2504
|
+
let seenStructural = -1;
|
|
2505
|
+
let seenContent = -1;
|
|
2506
|
+
let templateOffsets = new Int32Array(0);
|
|
2507
|
+
let templates = new Float32Array(0);
|
|
2508
|
+
let bounds = new Float32Array(0);
|
|
2509
|
+
let knownBounds = new Uint8Array(0);
|
|
2510
|
+
let batchDescriptors = [];
|
|
2511
|
+
let invalidated = false;
|
|
2512
|
+
let planGeneration = 0;
|
|
2513
|
+
let deltaBaseRevision = 0;
|
|
2514
|
+
const patchView = createDrawListPatchView();
|
|
2515
|
+
const diagnostics = {
|
|
2516
|
+
planBuilds: 0,
|
|
2517
|
+
structuralInvalidations: 0,
|
|
2518
|
+
planReuses: 0,
|
|
2519
|
+
templateRangeUpdates: 0,
|
|
2520
|
+
reusedSelections: 0,
|
|
2521
|
+
reusedBatches: 0
|
|
2522
|
+
};
|
|
2523
|
+
const selectionOptions = { boundsAt(index, out) {
|
|
2524
|
+
return plan.commandBounds(index, out);
|
|
2525
|
+
} };
|
|
2526
|
+
const boundsScratch = createDamageRect();
|
|
2527
|
+
const changedCommands = [];
|
|
2528
|
+
const refreshResult = {
|
|
2529
|
+
rebuilt: false,
|
|
2530
|
+
rangeUpdates: 0,
|
|
2531
|
+
changedCommands,
|
|
2532
|
+
planGeneration: 0,
|
|
2533
|
+
contentRevision: 0,
|
|
2534
|
+
deltaBaseRevision: 0
|
|
2535
|
+
};
|
|
2536
|
+
function updateBounds(index) {
|
|
2537
|
+
const value = commandDamageBounds(list, index, boundsScratch);
|
|
2538
|
+
const at = index * 4;
|
|
2539
|
+
if (!value) {
|
|
2540
|
+
knownBounds[index] = 0;
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
knownBounds[index] = 1;
|
|
2544
|
+
bounds[at] = value.x;
|
|
2545
|
+
bounds[at + 1] = value.y;
|
|
2546
|
+
bounds[at + 2] = value.width;
|
|
2547
|
+
bounds[at + 3] = value.height;
|
|
2548
|
+
}
|
|
2549
|
+
function updateQuadTemplate(index) {
|
|
2550
|
+
const target = templateOffsets[index];
|
|
2551
|
+
if (target === NO_TEMPLATE) return;
|
|
2552
|
+
const source = list.floatOffsetAt(index);
|
|
2553
|
+
const floats = list.floats;
|
|
2554
|
+
const m0 = floats[source];
|
|
2555
|
+
const m1 = floats[source + 1];
|
|
2556
|
+
const m2 = floats[source + 2];
|
|
2557
|
+
const m3 = floats[source + 3];
|
|
2558
|
+
const m4 = floats[source + 4];
|
|
2559
|
+
const m5 = floats[source + 5];
|
|
2560
|
+
const w = floats[source + 6];
|
|
2561
|
+
const h = floats[source + 7];
|
|
2562
|
+
templates[target] = m4;
|
|
2563
|
+
templates[target + 1] = m5;
|
|
2564
|
+
templates[target + 2] = m0 * w + m4;
|
|
2565
|
+
templates[target + 3] = m1 * w + m5;
|
|
2566
|
+
templates[target + 4] = m0 * w + m2 * h + m4;
|
|
2567
|
+
templates[target + 5] = m1 * w + m3 * h + m5;
|
|
2568
|
+
templates[target + 6] = m2 * h + m4;
|
|
2569
|
+
templates[target + 7] = m3 * h + m5;
|
|
2570
|
+
templates[target + 8] = floats[source + 8];
|
|
2571
|
+
templates[target + 9] = floats[source + 9];
|
|
2572
|
+
templates[target + 10] = floats[source + 10];
|
|
2573
|
+
templates[target + 11] = floats[source + 11];
|
|
2574
|
+
templates[target + 12] = floats[source + 12];
|
|
2575
|
+
templates[target + 13] = floats[source + 13];
|
|
2576
|
+
templates[target + 14] = floats[source + 14];
|
|
2577
|
+
templates[target + 15] = floats[source + 15];
|
|
2578
|
+
}
|
|
2579
|
+
function rebuild() {
|
|
2580
|
+
const count = list.count;
|
|
2581
|
+
templateOffsets = new Int32Array(count);
|
|
2582
|
+
templateOffsets.fill(NO_TEMPLATE);
|
|
2583
|
+
templates = new Float32Array(count * TEMPLATE_FLOATS);
|
|
2584
|
+
bounds = new Float32Array(count * 4);
|
|
2585
|
+
knownBounds = new Uint8Array(count);
|
|
2586
|
+
batchDescriptors = [];
|
|
2587
|
+
let templateCount = 0;
|
|
2588
|
+
let clipDepth = 0;
|
|
2589
|
+
let runStart = -1;
|
|
2590
|
+
let runBlend = 0;
|
|
2591
|
+
let runClipDepth = 0;
|
|
2592
|
+
const closeRun = (end) => {
|
|
2593
|
+
if (runStart < 0) return;
|
|
2594
|
+
batchDescriptors.push({
|
|
2595
|
+
start: runStart,
|
|
2596
|
+
end,
|
|
2597
|
+
blend: runBlend,
|
|
2598
|
+
clipDepth: runClipDepth
|
|
2599
|
+
});
|
|
2600
|
+
runStart = -1;
|
|
2601
|
+
};
|
|
2602
|
+
for (let index = 0; index < count; index += 1) {
|
|
2603
|
+
const kind = list.kindAt(index);
|
|
2604
|
+
updateBounds(index);
|
|
2605
|
+
if (kind === 0) {
|
|
2606
|
+
const blend = list.ints[list.intOffsetAt(index)];
|
|
2607
|
+
if (runStart < 0 || runBlend !== blend || runClipDepth !== clipDepth) {
|
|
2608
|
+
closeRun(index);
|
|
2609
|
+
runStart = index;
|
|
2610
|
+
runBlend = blend;
|
|
2611
|
+
runClipDepth = clipDepth;
|
|
2612
|
+
}
|
|
2613
|
+
templateOffsets[index] = templateCount * TEMPLATE_FLOATS;
|
|
2614
|
+
templateCount += 1;
|
|
2615
|
+
updateQuadTemplate(index);
|
|
2616
|
+
} else {
|
|
2617
|
+
closeRun(index);
|
|
2618
|
+
if (kind === 3) clipDepth += 1;
|
|
2619
|
+
else if (kind === 4) clipDepth = Math.max(0, clipDepth - 1);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
closeRun(count);
|
|
2623
|
+
templates = templates.subarray(0, templateCount * TEMPLATE_FLOATS);
|
|
2624
|
+
seenStructural = list.structuralRevision;
|
|
2625
|
+
seenContent = list.contentRevision;
|
|
2626
|
+
deltaBaseRevision = seenContent;
|
|
2627
|
+
planGeneration += 1;
|
|
2628
|
+
invalidated = false;
|
|
2629
|
+
diagnostics.planBuilds += 1;
|
|
2630
|
+
}
|
|
2631
|
+
const plan = {
|
|
2632
|
+
list,
|
|
2633
|
+
get batches() {
|
|
2634
|
+
return batchDescriptors;
|
|
2635
|
+
},
|
|
2636
|
+
diagnostics,
|
|
2637
|
+
refresh() {
|
|
2638
|
+
if (invalidated || seenStructural !== list.structuralRevision) {
|
|
2639
|
+
rebuild();
|
|
2640
|
+
changedCommands.length = 0;
|
|
2641
|
+
refreshResult.rebuilt = true;
|
|
2642
|
+
refreshResult.rangeUpdates = 0;
|
|
2643
|
+
refreshResult.planGeneration = planGeneration;
|
|
2644
|
+
refreshResult.contentRevision = seenContent;
|
|
2645
|
+
refreshResult.deltaBaseRevision = deltaBaseRevision;
|
|
2646
|
+
return refreshResult;
|
|
2647
|
+
}
|
|
2648
|
+
if (seenContent === list.contentRevision) {
|
|
2649
|
+
diagnostics.planReuses += 1;
|
|
2650
|
+
diagnostics.reusedBatches += batchDescriptors.length;
|
|
2651
|
+
refreshResult.rebuilt = false;
|
|
2652
|
+
refreshResult.rangeUpdates = 0;
|
|
2653
|
+
refreshResult.planGeneration = planGeneration;
|
|
2654
|
+
refreshResult.contentRevision = seenContent;
|
|
2655
|
+
refreshResult.deltaBaseRevision = deltaBaseRevision;
|
|
2656
|
+
return refreshResult;
|
|
2657
|
+
}
|
|
2658
|
+
const patches = list.readPatchesSince(seenContent, patchView);
|
|
2659
|
+
if (patches.overflowed) {
|
|
2660
|
+
rebuild();
|
|
2661
|
+
changedCommands.length = 0;
|
|
2662
|
+
refreshResult.rebuilt = true;
|
|
2663
|
+
refreshResult.rangeUpdates = 0;
|
|
2664
|
+
refreshResult.planGeneration = planGeneration;
|
|
2665
|
+
refreshResult.contentRevision = seenContent;
|
|
2666
|
+
refreshResult.deltaBaseRevision = deltaBaseRevision;
|
|
2667
|
+
return refreshResult;
|
|
2668
|
+
}
|
|
2669
|
+
let updates = 0;
|
|
2670
|
+
changedCommands.length = 0;
|
|
2671
|
+
for (let patch = 0; patch < patches.indices.length; patch += 1) {
|
|
2672
|
+
const index = patches.indices[patch];
|
|
2673
|
+
updateBounds(index);
|
|
2674
|
+
updateQuadTemplate(index);
|
|
2675
|
+
updates += 1;
|
|
2676
|
+
changedCommands.push(index);
|
|
2677
|
+
}
|
|
2678
|
+
deltaBaseRevision = seenContent;
|
|
2679
|
+
seenContent = list.contentRevision;
|
|
2680
|
+
diagnostics.planReuses += 1;
|
|
2681
|
+
diagnostics.reusedBatches += batchDescriptors.length;
|
|
2682
|
+
diagnostics.templateRangeUpdates += updates;
|
|
2683
|
+
refreshResult.rebuilt = false;
|
|
2684
|
+
refreshResult.rangeUpdates = updates;
|
|
2685
|
+
refreshResult.planGeneration = planGeneration;
|
|
2686
|
+
refreshResult.contentRevision = seenContent;
|
|
2687
|
+
refreshResult.deltaBaseRevision = deltaBaseRevision;
|
|
2688
|
+
return refreshResult;
|
|
2689
|
+
},
|
|
2690
|
+
invalidate() {
|
|
2691
|
+
invalidated = true;
|
|
2692
|
+
diagnostics.structuralInvalidations += 1;
|
|
2693
|
+
},
|
|
2694
|
+
fillQuad(index, textureWidth, textureHeight, out) {
|
|
2695
|
+
const at = templateOffsets[index];
|
|
2696
|
+
if (at === NO_TEMPLATE) return false;
|
|
2697
|
+
out.x0 = templates[at];
|
|
2698
|
+
out.y0 = templates[at + 1];
|
|
2699
|
+
out.x1 = templates[at + 2];
|
|
2700
|
+
out.y1 = templates[at + 3];
|
|
2701
|
+
out.x2 = templates[at + 4];
|
|
2702
|
+
out.y2 = templates[at + 5];
|
|
2703
|
+
out.x3 = templates[at + 6];
|
|
2704
|
+
out.y3 = templates[at + 7];
|
|
2705
|
+
const invWidth = 1 / Math.max(1, textureWidth);
|
|
2706
|
+
const invHeight = 1 / Math.max(1, textureHeight);
|
|
2707
|
+
out.u0 = templates[at + 8] * invWidth;
|
|
2708
|
+
out.v0 = templates[at + 9] * invHeight;
|
|
2709
|
+
out.uSpan = templates[at + 10] * invWidth;
|
|
2710
|
+
out.vSpan = templates[at + 11] * invHeight;
|
|
2711
|
+
out.r = templates[at + 12];
|
|
2712
|
+
out.g = templates[at + 13];
|
|
2713
|
+
out.b = templates[at + 14];
|
|
2714
|
+
out.a = templates[at + 15];
|
|
2715
|
+
const flags = list.ints[list.intOffsetAt(index) + 1];
|
|
2716
|
+
if ((flags & 1) !== 0) {
|
|
2717
|
+
out.u0 += out.uSpan;
|
|
2718
|
+
out.uSpan = -out.uSpan;
|
|
2719
|
+
}
|
|
2720
|
+
if ((flags & 2) !== 0) {
|
|
2721
|
+
out.v0 += out.vSpan;
|
|
2722
|
+
out.vSpan = -out.vSpan;
|
|
2723
|
+
}
|
|
2724
|
+
return true;
|
|
2725
|
+
},
|
|
2726
|
+
commandBounds(index, out) {
|
|
2727
|
+
if (index < 0 || index >= list.count || knownBounds[index] === 0) return null;
|
|
2728
|
+
const at = index * 4;
|
|
2729
|
+
out.x = bounds[at];
|
|
2730
|
+
out.y = bounds[at + 1];
|
|
2731
|
+
out.width = bounds[at + 2];
|
|
2732
|
+
out.height = bounds[at + 3];
|
|
2733
|
+
return out;
|
|
2734
|
+
},
|
|
2735
|
+
select(damage, scratch, options) {
|
|
2736
|
+
plan.refresh();
|
|
2737
|
+
diagnostics.reusedSelections += 1;
|
|
2738
|
+
selectionOptions.transform = options?.transform;
|
|
2739
|
+
selectionOptions.rasterOutset = options?.rasterOutset;
|
|
2740
|
+
selectionOptions.maxCommands = options?.maxCommands;
|
|
2741
|
+
selectionOptions.unknownBounds = options?.unknownBounds;
|
|
2742
|
+
return scratch.select(list, damage, selectionOptions);
|
|
2743
|
+
}
|
|
2744
|
+
};
|
|
2745
|
+
rebuild();
|
|
2746
|
+
return plan;
|
|
2747
|
+
}
|
|
2748
|
+
/** Convenience for consumers with no existing scratch. Retained loops should
|
|
2749
|
+
* create one scratch once and call `plan.select` instead. */
|
|
2750
|
+
function createCompiledReplayMask(plan, damage, options) {
|
|
2751
|
+
return plan.select(damage, createReplayMaskScratch(plan.list.count), options);
|
|
2752
|
+
}
|
|
2753
|
+
//#endregion
|
|
2754
|
+
//#region src/nine-patch.ts
|
|
2755
|
+
function createNinePatchBand() {
|
|
2756
|
+
return {
|
|
2757
|
+
dstX: 0,
|
|
2758
|
+
dstY: 0,
|
|
2759
|
+
dstW: 0,
|
|
2760
|
+
dstH: 0,
|
|
2761
|
+
srcX: 0,
|
|
2762
|
+
srcY: 0,
|
|
2763
|
+
srcW: 0,
|
|
2764
|
+
srcH: 0
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
/** A reusable output buffer: nine bands is the hard maximum, so it never grows. */
|
|
2768
|
+
function createNinePatchBands() {
|
|
2769
|
+
return Array.from({ length: 9 }, createNinePatchBand);
|
|
2770
|
+
}
|
|
2771
|
+
function createAxisBands() {
|
|
2772
|
+
return {
|
|
2773
|
+
dst: [
|
|
2774
|
+
[0, 0],
|
|
2775
|
+
[0, 0],
|
|
2776
|
+
[0, 0]
|
|
2777
|
+
],
|
|
2778
|
+
src: [
|
|
2779
|
+
[0, 0],
|
|
2780
|
+
[0, 0],
|
|
2781
|
+
[0, 0]
|
|
2782
|
+
]
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
const H_BANDS = createAxisBands();
|
|
2786
|
+
const V_BANDS = createAxisBands();
|
|
2787
|
+
/**
|
|
2788
|
+
* Split ONE axis into its leading / centre / trailing spans, in the shader's
|
|
2789
|
+
* branch order. `size` is the destination extent, `texSize` the source extent,
|
|
2790
|
+
* `begin`/`end` the two margins. Writes into `out` and returns the number of
|
|
2791
|
+
* non-empty spans.
|
|
2792
|
+
*/
|
|
2793
|
+
function splitAxis(size, texSize, begin, end, out) {
|
|
2794
|
+
const marginBegin = Math.max(0, begin);
|
|
2795
|
+
const marginEnd = Math.max(0, end);
|
|
2796
|
+
const leadEnd = Math.min(marginBegin, Math.max(0, size));
|
|
2797
|
+
const trailStart = Math.max(leadEnd, size - marginEnd);
|
|
2798
|
+
let count = 0;
|
|
2799
|
+
if (leadEnd > 0) {
|
|
2800
|
+
out.dst[count][0] = 0;
|
|
2801
|
+
out.dst[count][1] = leadEnd;
|
|
2802
|
+
out.src[count][0] = 0;
|
|
2803
|
+
out.src[count][1] = Math.min(leadEnd, texSize);
|
|
2804
|
+
count += 1;
|
|
2805
|
+
}
|
|
2806
|
+
const centreSrcBegin = marginBegin;
|
|
2807
|
+
const centreSrcEnd = texSize - marginEnd;
|
|
2808
|
+
if (trailStart > leadEnd && centreSrcEnd > centreSrcBegin) {
|
|
2809
|
+
out.dst[count][0] = leadEnd;
|
|
2810
|
+
out.dst[count][1] = trailStart;
|
|
2811
|
+
out.src[count][0] = centreSrcBegin;
|
|
2812
|
+
out.src[count][1] = centreSrcEnd;
|
|
2813
|
+
count += 1;
|
|
2814
|
+
}
|
|
2815
|
+
if (size > trailStart) {
|
|
2816
|
+
const span = size - trailStart;
|
|
2817
|
+
out.dst[count][0] = trailStart;
|
|
2818
|
+
out.dst[count][1] = size;
|
|
2819
|
+
out.src[count][0] = Math.max(0, texSize - span);
|
|
2820
|
+
out.src[count][1] = texSize;
|
|
2821
|
+
count += 1;
|
|
2822
|
+
}
|
|
2823
|
+
return count;
|
|
2824
|
+
}
|
|
2825
|
+
/**
|
|
2826
|
+
* Expand a nine-patch into its bands, filling `out` (use
|
|
2827
|
+
* {@link createNinePatchBands}, which is always big enough) and returning how
|
|
2828
|
+
* many are live. `out` entries past the return value are stale and must not be
|
|
2829
|
+
* read.
|
|
2830
|
+
*
|
|
2831
|
+
* Returns 0 for a patch with no area — a zero-size destination or a zero-size
|
|
2832
|
+
* region draws nothing at all, which is not the same as drawing one empty band.
|
|
2833
|
+
*/
|
|
2834
|
+
function expandNinePatch(patch, out) {
|
|
2835
|
+
const { w, h, srcW, srcH } = patch;
|
|
2836
|
+
if (!(w > 0) || !(h > 0) || !(srcW > 0) || !(srcH > 0)) return 0;
|
|
2837
|
+
const columns = splitAxis(w, srcW, patch.marginLeft, patch.marginRight, H_BANDS);
|
|
2838
|
+
const rows = splitAxis(h, srcH, patch.marginTop, patch.marginBottom, V_BANDS);
|
|
2839
|
+
let count = 0;
|
|
2840
|
+
for (let row = 0; row < rows; row += 1) {
|
|
2841
|
+
const [dstTop, dstBottom] = V_BANDS.dst[row];
|
|
2842
|
+
const [srcTop, srcBottom] = V_BANDS.src[row];
|
|
2843
|
+
for (let column = 0; column < columns; column += 1) {
|
|
2844
|
+
const [dstLeft, dstRight] = H_BANDS.dst[column];
|
|
2845
|
+
const [srcLeft, srcRight] = H_BANDS.src[column];
|
|
2846
|
+
const band = out[count];
|
|
2847
|
+
band.dstX = dstLeft;
|
|
2848
|
+
band.dstY = dstTop;
|
|
2849
|
+
band.dstW = dstRight - dstLeft;
|
|
2850
|
+
band.dstH = dstBottom - dstTop;
|
|
2851
|
+
band.srcX = patch.srcX + srcLeft;
|
|
2852
|
+
band.srcY = patch.srcY + srcTop;
|
|
2853
|
+
band.srcW = srcRight - srcLeft;
|
|
2854
|
+
band.srcH = srcBottom - srcTop;
|
|
2855
|
+
count += 1;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
return count;
|
|
2859
|
+
}
|
|
2860
|
+
//#endregion
|
|
2861
|
+
//#region src/polyline.ts
|
|
2862
|
+
/**
|
|
2863
|
+
* Constant-width polyline -> quads, so a `DRAW_POLYLINE` goes through the SAME
|
|
2864
|
+
* instance buffer, the same shader and the same batch as everything else instead
|
|
2865
|
+
* of forcing a second program and a mid-frame draw break.
|
|
2866
|
+
*
|
|
2867
|
+
* That is possible because the batcher's instance carries four EXPLICIT corners
|
|
2868
|
+
* rather than an affine basis (see `./batcher`): a quad instance is any
|
|
2869
|
+
* quadrilateral, and a quadrilateral with its last two corners coincident is a
|
|
2870
|
+
* triangle. So a stroke is emitted as
|
|
2871
|
+
*
|
|
2872
|
+
* - one parallelogram per segment (the segment offset by ±width/2 along its
|
|
2873
|
+
* normal), and
|
|
2874
|
+
* - one triangle per interior vertex, filling the notch on the OUTSIDE of the
|
|
2875
|
+
* turn.
|
|
2876
|
+
*
|
|
2877
|
+
* JOINS ARE BEVEL, CAPS ARE BUTT — the first pass the wave-1 brief allows, and a
|
|
2878
|
+
* deliberate choice rather than an oversight:
|
|
2879
|
+
*
|
|
2880
|
+
* - a MITER join needs the two segment edges extended to their intersection,
|
|
2881
|
+
* which runs away to infinity as the turn approaches a reversal and therefore
|
|
2882
|
+
* needs a miter limit that itself falls back to... a bevel. Two code paths for
|
|
2883
|
+
* a shape that differs from the bevel only inside a `width/2` disc.
|
|
2884
|
+
* - a ROUND join needs an arc, i.e. a fan of triangles whose count depends on the
|
|
2885
|
+
* turn angle and the on-screen width — the one thing in this file that would
|
|
2886
|
+
* make its output size unpredictable.
|
|
2887
|
+
*
|
|
2888
|
+
* The bevel differs from both only within half a stroke width of a vertex, and
|
|
2889
|
+
* the measured population of polylines in the recorded scenes this executor was
|
|
2890
|
+
* sized against is ZERO (the paint-source mix is texture/text/particles/spine/
|
|
2891
|
+
* solid). Upgrading to round joins is local to this file: emit a fan instead of
|
|
2892
|
+
* the single wedge triangle in the interior-vertex loop.
|
|
2893
|
+
*
|
|
2894
|
+
* Self-overlap: at a sharp turn the two segment parallelograms overlap near the
|
|
2895
|
+
* vertex, so a translucent stroke double-composites there. Godot's own
|
|
2896
|
+
* `draw_polyline` has the same artefact; fixing it needs a stencil or a
|
|
2897
|
+
* single-pass SDF, neither of which belongs in wave 1.
|
|
2898
|
+
*/
|
|
2899
|
+
/** Floats per emitted quad: four `(x, y)` corners in draw-list local space. */
|
|
2900
|
+
const POLYLINE_QUAD_FLOATS = 8;
|
|
2901
|
+
/**
|
|
2902
|
+
* The most quads a `pointCount`-point stroke can produce: one per segment plus
|
|
2903
|
+
* one per interior vertex. Sizes a caller's output buffer exactly.
|
|
2904
|
+
*/
|
|
2905
|
+
function polylineQuadCapacity(pointCount) {
|
|
2906
|
+
if (pointCount < 2) return 0;
|
|
2907
|
+
return pointCount - 1 + Math.max(0, pointCount - 2);
|
|
2908
|
+
}
|
|
2909
|
+
/**
|
|
2910
|
+
* Tessellate `points` (flattened `x, y, x, y, …`, the draw-list's own layout)
|
|
2911
|
+
* into quads, writing `POLYLINE_QUAD_FLOATS` floats per quad into `out` from
|
|
2912
|
+
* `outOffset`. Returns the number of quads written.
|
|
2913
|
+
*
|
|
2914
|
+
* Corners are written in the batcher's unit-square order — `(0,0)`, `(1,0)`,
|
|
2915
|
+
* `(1,1)`, `(0,1)` — so a triangle is spelled by repeating the last corner.
|
|
2916
|
+
*
|
|
2917
|
+
* Zero-length segments are skipped (they have no direction to offset along, and
|
|
2918
|
+
* a duplicated point is a common artefact of a resampled path); a vertex whose
|
|
2919
|
+
* incoming or outgoing segment was skipped gets no join wedge, because there is
|
|
2920
|
+
* no notch to fill.
|
|
2921
|
+
*/
|
|
2922
|
+
function expandPolyline(points, pointCount, width, out, outOffset = 0) {
|
|
2923
|
+
const half = width / 2;
|
|
2924
|
+
if (pointCount < 2 || !(half > 0)) return 0;
|
|
2925
|
+
let at = outOffset;
|
|
2926
|
+
let quads = 0;
|
|
2927
|
+
let previousNormalX = 0;
|
|
2928
|
+
let previousNormalY = 0;
|
|
2929
|
+
let hasPrevious = false;
|
|
2930
|
+
let previousDirX = 0;
|
|
2931
|
+
let previousDirY = 0;
|
|
2932
|
+
for (let i = 0; i + 1 < pointCount; i += 1) {
|
|
2933
|
+
const ax = points[i * 2];
|
|
2934
|
+
const ay = points[i * 2 + 1];
|
|
2935
|
+
const bx = points[i * 2 + 2];
|
|
2936
|
+
const by = points[i * 2 + 3];
|
|
2937
|
+
const dx = bx - ax;
|
|
2938
|
+
const dy = by - ay;
|
|
2939
|
+
const length = Math.hypot(dx, dy);
|
|
2940
|
+
if (!(length > 0)) {
|
|
2941
|
+
hasPrevious = false;
|
|
2942
|
+
continue;
|
|
2943
|
+
}
|
|
2944
|
+
const dirX = dx / length;
|
|
2945
|
+
const dirY = dy / length;
|
|
2946
|
+
const normalX = -dirY * half;
|
|
2947
|
+
const normalY = dirX * half;
|
|
2948
|
+
if (hasPrevious) {
|
|
2949
|
+
const cross = previousDirX * dirY - previousDirY * dirX;
|
|
2950
|
+
if (cross !== 0) {
|
|
2951
|
+
const side = cross > 0 ? -1 : 1;
|
|
2952
|
+
out[at] = ax;
|
|
2953
|
+
out[at + 1] = ay;
|
|
2954
|
+
out[at + 2] = ax + previousNormalX * side;
|
|
2955
|
+
out[at + 3] = ay + previousNormalY * side;
|
|
2956
|
+
out[at + 4] = ax + normalX * side;
|
|
2957
|
+
out[at + 5] = ay + normalY * side;
|
|
2958
|
+
out[at + 6] = ax + normalX * side;
|
|
2959
|
+
out[at + 7] = ay + normalY * side;
|
|
2960
|
+
at += 8;
|
|
2961
|
+
quads += 1;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
out[at] = ax + normalX;
|
|
2965
|
+
out[at + 1] = ay + normalY;
|
|
2966
|
+
out[at + 2] = bx + normalX;
|
|
2967
|
+
out[at + 3] = by + normalY;
|
|
2968
|
+
out[at + 4] = bx - normalX;
|
|
2969
|
+
out[at + 5] = by - normalY;
|
|
2970
|
+
out[at + 6] = ax - normalX;
|
|
2971
|
+
out[at + 7] = ay - normalY;
|
|
2972
|
+
at += 8;
|
|
2973
|
+
quads += 1;
|
|
2974
|
+
previousNormalX = normalX;
|
|
2975
|
+
previousNormalY = normalY;
|
|
2976
|
+
previousDirX = dirX;
|
|
2977
|
+
previousDirY = dirY;
|
|
2978
|
+
hasPrevious = true;
|
|
2979
|
+
}
|
|
2980
|
+
return quads;
|
|
2981
|
+
}
|
|
2982
|
+
//#endregion
|
|
2983
|
+
//#region src/executor-webgl.ts
|
|
2984
|
+
const BLEND_STATES = {
|
|
2985
|
+
[0]: {
|
|
2986
|
+
equationRgb: "FUNC_ADD",
|
|
2987
|
+
equationAlpha: "FUNC_ADD",
|
|
2988
|
+
srcRgb: "ONE",
|
|
2989
|
+
dstRgb: "ONE_MINUS_SRC_ALPHA",
|
|
2990
|
+
srcAlpha: "ONE",
|
|
2991
|
+
dstAlpha: "ONE_MINUS_SRC_ALPHA"
|
|
2992
|
+
},
|
|
2993
|
+
[1]: {
|
|
2994
|
+
equationRgb: "FUNC_ADD",
|
|
2995
|
+
equationAlpha: "FUNC_ADD",
|
|
2996
|
+
srcRgb: "ONE",
|
|
2997
|
+
dstRgb: "ONE",
|
|
2998
|
+
srcAlpha: "ONE",
|
|
2999
|
+
dstAlpha: "ONE"
|
|
3000
|
+
},
|
|
3001
|
+
[2]: {
|
|
3002
|
+
equationRgb: "FUNC_REVERSE_SUBTRACT",
|
|
3003
|
+
equationAlpha: "FUNC_ADD",
|
|
3004
|
+
srcRgb: "ONE",
|
|
3005
|
+
dstRgb: "ONE",
|
|
3006
|
+
srcAlpha: "ONE",
|
|
3007
|
+
dstAlpha: "ONE"
|
|
3008
|
+
},
|
|
3009
|
+
[3]: {
|
|
3010
|
+
equationRgb: "FUNC_ADD",
|
|
3011
|
+
equationAlpha: "FUNC_ADD",
|
|
3012
|
+
srcRgb: "DST_COLOR",
|
|
3013
|
+
dstRgb: "ZERO",
|
|
3014
|
+
srcAlpha: "DST_ALPHA",
|
|
3015
|
+
dstAlpha: "ZERO"
|
|
3016
|
+
}
|
|
3017
|
+
};
|
|
3018
|
+
/** The GL blend state a Godot `CanvasItemMaterial.BlendMode` maps to. */
|
|
3019
|
+
function blendStateFor(blend) {
|
|
3020
|
+
return BLEND_STATES[blend] ?? BLEND_STATES[0];
|
|
3021
|
+
}
|
|
3022
|
+
const VERTEX_SRC = `#version 300 es
|
|
3023
|
+
layout(location = 0) in vec2 a_corner; // unit-square corner, see INSTANCE_CORNERS_OFFSET
|
|
3024
|
+
layout(location = 1) in vec4 a_p01; // p0 (0,0), p1 (1,0)
|
|
3025
|
+
layout(location = 2) in vec4 a_p23; // p2 (1,1), p3 (0,1)
|
|
3026
|
+
layout(location = 3) in vec4 a_uv; // u0, v0, uSpan, vSpan
|
|
3027
|
+
layout(location = 4) in vec4 a_color; // PREMULTIPLIED tint
|
|
3028
|
+
layout(location = 5) in vec2 a_slots; // texture slot, colour-matrix slot
|
|
3029
|
+
uniform vec4 u_projection; // design -> clip: scale.xy, translate.xy
|
|
3030
|
+
out vec2 v_uv;
|
|
3031
|
+
out vec4 v_color;
|
|
3032
|
+
out vec2 v_design;
|
|
3033
|
+
flat out int v_texture;
|
|
3034
|
+
flat out int v_matrix;
|
|
3035
|
+
void main() {
|
|
3036
|
+
// BILINEAR across four explicit corners rather than an affine basis: see the
|
|
3037
|
+
// note in ./batcher. For the affine case (every sprite) this is exactly the
|
|
3038
|
+
// affine map; for a stroke segment it is the quadrilateral itself.
|
|
3039
|
+
vec2 top = mix(a_p01.xy, a_p01.zw, a_corner.x);
|
|
3040
|
+
vec2 bottom = mix(a_p23.zw, a_p23.xy, a_corner.x);
|
|
3041
|
+
vec2 design = mix(top, bottom, a_corner.y);
|
|
3042
|
+
gl_Position = vec4(design * u_projection.xy + u_projection.zw, 0.0, 1.0);
|
|
3043
|
+
v_uv = a_uv.xy + a_uv.zw * a_corner;
|
|
3044
|
+
v_color = a_color;
|
|
3045
|
+
v_design = design;
|
|
3046
|
+
v_texture = int(a_slots.x);
|
|
3047
|
+
v_matrix = int(a_slots.y);
|
|
3048
|
+
}`;
|
|
3049
|
+
const TEXTURED_MESH_VERTEX_SRC = `#version 300 es
|
|
3050
|
+
layout(location = 0) in vec2 a_position;
|
|
3051
|
+
layout(location = 1) in vec2 a_uv;
|
|
3052
|
+
uniform vec4 u_projection;
|
|
3053
|
+
out vec2 v_uv;
|
|
3054
|
+
void main() {
|
|
3055
|
+
gl_Position = vec4(a_position * u_projection.xy + u_projection.zw, 0.0, 1.0);
|
|
3056
|
+
v_uv = a_uv;
|
|
3057
|
+
}`;
|
|
3058
|
+
const TEXTURED_MESH_FRAGMENT_SRC = `#version 300 es
|
|
3059
|
+
precision highp float;
|
|
3060
|
+
uniform sampler2D u_texture;
|
|
3061
|
+
uniform vec4 u_tint;
|
|
3062
|
+
in vec2 v_uv;
|
|
3063
|
+
out vec4 fragColor;
|
|
3064
|
+
void main() {
|
|
3065
|
+
// Both the uploaded texel and tint are premultiplied, so ordinary component
|
|
3066
|
+
// multiplication remains premultiplied for the shared blend contract.
|
|
3067
|
+
fragColor = texture(u_texture, v_uv) * u_tint;
|
|
3068
|
+
}`;
|
|
3069
|
+
/**
|
|
3070
|
+
* The fragment shader, generated for the slot counts THIS context supports.
|
|
3071
|
+
*
|
|
3072
|
+
* The `if` ladder is not laziness: GLSL ES 3.00 allows a sampler array to be
|
|
3073
|
+
* indexed only by a constant expression, so a dynamic slot has to be resolved by
|
|
3074
|
+
* comparison. (The colour-matrix array next to it is a plain uniform array, which
|
|
3075
|
+
* dynamic indexing IS allowed on.)
|
|
3076
|
+
*
|
|
3077
|
+
* `precision highp int` is load-bearing. The default integer precision is `highp`
|
|
3078
|
+
* in the vertex stage and `mediump` in the fragment stage, and a cross-stage
|
|
3079
|
+
* variable whose precision disagrees fails to LINK — silently, in the sense that
|
|
3080
|
+
* the only symptom is a program that does not exist and therefore a canvas that
|
|
3081
|
+
* draws nothing. `highp float` matters for a different reason: `mediump` carries
|
|
3082
|
+
* ~10 bits of mantissa, which cannot address a texel on a 4096-wide atlas page.
|
|
3083
|
+
*/
|
|
3084
|
+
function fragmentSource(textureSlots, colorMatrices) {
|
|
3085
|
+
const ladder = [];
|
|
3086
|
+
for (let i = 0; i < textureSlots; i += 1) ladder.push(` ${i === 0 ? "if" : "} else if"} (slot == ${i}) {\n return texture(u_textures[${i}], uv);`);
|
|
3087
|
+
ladder.push(" }");
|
|
3088
|
+
return `#version 300 es
|
|
3089
|
+
precision highp float;
|
|
3090
|
+
precision highp int;
|
|
3091
|
+
precision highp sampler2D;
|
|
3092
|
+
uniform sampler2D u_textures[${textureSlots}];
|
|
3093
|
+
uniform mat3 u_colorMatrices[${colorMatrices}];
|
|
3094
|
+
uniform vec4 u_roundedRect; // centre.xy, half-extent.xy, DESIGN units
|
|
3095
|
+
uniform float u_roundedRadius; // <= 0 disables the rounded test entirely
|
|
3096
|
+
in vec2 v_uv;
|
|
3097
|
+
in vec4 v_color;
|
|
3098
|
+
in vec2 v_design;
|
|
3099
|
+
flat in int v_texture;
|
|
3100
|
+
flat in int v_matrix;
|
|
3101
|
+
out vec4 fragColor;
|
|
3102
|
+
|
|
3103
|
+
vec4 sampleSlot(int slot, vec2 uv) {
|
|
3104
|
+
${ladder.join("\n")}
|
|
3105
|
+
return vec4(0.0);
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
void main() {
|
|
3109
|
+
if (u_roundedRadius > 0.0) {
|
|
3110
|
+
// Standard rounded-rect distance: shrink the box by the radius, take the
|
|
3111
|
+
// distance to that box, subtract the radius back.
|
|
3112
|
+
vec2 d = abs(v_design - u_roundedRect.xy) - (u_roundedRect.zw - vec2(u_roundedRadius));
|
|
3113
|
+
if (length(max(d, vec2(0.0))) - u_roundedRadius > 0.0) {
|
|
3114
|
+
discard;
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
vec4 texel = sampleSlot(v_texture, v_uv);
|
|
3118
|
+
if (v_matrix != 0) {
|
|
3119
|
+
// The matrix is defined on the texture's OWN colour, so it has to see
|
|
3120
|
+
// straight (un-premultiplied) sRGB — apply it to premultiplied channels and a
|
|
3121
|
+
// half-transparent pixel is transformed as if it were half as bright. No
|
|
3122
|
+
// linearization: the same sRGB-domain transform html's applyColorMatrixToPixels
|
|
3123
|
+
// performs on the CPU, clamped for the same reason (it writes clamped bytes).
|
|
3124
|
+
float alpha = texel.a;
|
|
3125
|
+
vec3 straight = alpha > 0.0 ? texel.rgb / alpha : vec3(0.0);
|
|
3126
|
+
straight = clamp(u_colorMatrices[v_matrix] * straight, 0.0, 1.0);
|
|
3127
|
+
texel = vec4(straight * alpha, alpha);
|
|
3128
|
+
}
|
|
3129
|
+
// Two PREMULTIPLIED colours compose with a plain multiply, and the result is
|
|
3130
|
+
// premultiplied — which is what the blend factors and the canvas expect.
|
|
3131
|
+
fragColor = texel * v_color;
|
|
3132
|
+
}`;
|
|
3133
|
+
}
|
|
3134
|
+
function compileShader(gl, type, source) {
|
|
3135
|
+
const shader = gl.createShader(type);
|
|
3136
|
+
if (!shader) return null;
|
|
3137
|
+
gl.shaderSource(shader, source);
|
|
3138
|
+
gl.compileShader(shader);
|
|
3139
|
+
return shader;
|
|
3140
|
+
}
|
|
3141
|
+
function linkProgram(gl, vertexSrc, fragmentSrc) {
|
|
3142
|
+
const vs = compileShader(gl, gl.VERTEX_SHADER, vertexSrc);
|
|
3143
|
+
const fs = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSrc);
|
|
3144
|
+
if (!vs || !fs) {
|
|
3145
|
+
if (vs) gl.deleteShader(vs);
|
|
3146
|
+
if (fs) gl.deleteShader(fs);
|
|
3147
|
+
return null;
|
|
3148
|
+
}
|
|
3149
|
+
const program = gl.createProgram();
|
|
3150
|
+
if (!program) {
|
|
3151
|
+
gl.deleteShader(vs);
|
|
3152
|
+
gl.deleteShader(fs);
|
|
3153
|
+
return null;
|
|
3154
|
+
}
|
|
3155
|
+
gl.attachShader(program, vs);
|
|
3156
|
+
gl.attachShader(program, fs);
|
|
3157
|
+
gl.linkProgram(program);
|
|
3158
|
+
const linked = gl.getProgramParameter(program, gl.LINK_STATUS);
|
|
3159
|
+
if (!linked) {
|
|
3160
|
+
let compileFailed = false;
|
|
3161
|
+
for (const shader of [vs, fs]) {
|
|
3162
|
+
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) continue;
|
|
3163
|
+
compileFailed = true;
|
|
3164
|
+
console.warn("[gsw canvas] shader compile failed:", gl.getShaderInfoLog(shader));
|
|
3165
|
+
}
|
|
3166
|
+
if (!compileFailed) console.warn("[gsw canvas] program link failed:", gl.getProgramInfoLog(program));
|
|
3167
|
+
gl.deleteProgram(program);
|
|
3168
|
+
}
|
|
3169
|
+
gl.deleteShader(vs);
|
|
3170
|
+
gl.deleteShader(fs);
|
|
3171
|
+
return linked ? program : null;
|
|
3172
|
+
}
|
|
3173
|
+
/** `[x, y]` per corner, in TRIANGLE_STRIP order: `(0,0) (1,0) (0,1) (1,1)`, whose
|
|
3174
|
+
* two triangles are `p0 p1 p3` and `p1 p3 p2`. A quad with `p2 === p3` therefore
|
|
3175
|
+
* degenerates to exactly the triangle `p0 p1 p3` — how `./polyline` spells a join
|
|
3176
|
+
* wedge without a second draw path. */
|
|
3177
|
+
const CORNERS = new Float32Array([
|
|
3178
|
+
0,
|
|
3179
|
+
0,
|
|
3180
|
+
1,
|
|
3181
|
+
0,
|
|
3182
|
+
0,
|
|
3183
|
+
1,
|
|
3184
|
+
1,
|
|
3185
|
+
1
|
|
3186
|
+
]);
|
|
3187
|
+
const NO_CHANGED_COMMANDS = [];
|
|
3188
|
+
const INSTANCE_ATTRIBUTES = [
|
|
3189
|
+
{
|
|
3190
|
+
location: 1,
|
|
3191
|
+
size: 4,
|
|
3192
|
+
offset: 0
|
|
3193
|
+
},
|
|
3194
|
+
{
|
|
3195
|
+
location: 2,
|
|
3196
|
+
size: 4,
|
|
3197
|
+
offset: 4
|
|
3198
|
+
},
|
|
3199
|
+
{
|
|
3200
|
+
location: 3,
|
|
3201
|
+
size: 4,
|
|
3202
|
+
offset: 8
|
|
3203
|
+
},
|
|
3204
|
+
{
|
|
3205
|
+
location: 4,
|
|
3206
|
+
size: 4,
|
|
3207
|
+
offset: 12
|
|
3208
|
+
},
|
|
3209
|
+
{
|
|
3210
|
+
location: 5,
|
|
3211
|
+
size: 2,
|
|
3212
|
+
offset: 16
|
|
3213
|
+
}
|
|
3214
|
+
];
|
|
3215
|
+
function createCanvasExecutor(options) {
|
|
3216
|
+
const gl = options.gl;
|
|
3217
|
+
const contextUnits = Number(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS)) || 8;
|
|
3218
|
+
const maxTextureSlots = Math.max(1, Math.min(16, contextUnits, Math.floor(options.maxTextureSlots ?? 16)));
|
|
3219
|
+
const maxColorMatrices = Math.max(2, Math.floor(options.maxColorMatrices ?? 16));
|
|
3220
|
+
let program = null;
|
|
3221
|
+
let meshProgram = null;
|
|
3222
|
+
let ownedWhite = null;
|
|
3223
|
+
const suppliedWhite = options.white ?? null;
|
|
3224
|
+
const glyphPass = options.glyphs ?? null;
|
|
3225
|
+
const stats = {
|
|
3226
|
+
commands: 0,
|
|
3227
|
+
quads: 0,
|
|
3228
|
+
batches: 0,
|
|
3229
|
+
textureBinds: 0,
|
|
3230
|
+
scissorChanges: 0,
|
|
3231
|
+
blendChanges: 0,
|
|
3232
|
+
ninePatches: 0,
|
|
3233
|
+
ninePatchQuads: 0,
|
|
3234
|
+
polylines: 0,
|
|
3235
|
+
polylineQuads: 0,
|
|
3236
|
+
texturedMeshes: 0,
|
|
3237
|
+
texturedMeshTriangles: 0,
|
|
3238
|
+
texturedMeshDrawCalls: 0,
|
|
3239
|
+
glyphRuns: 0,
|
|
3240
|
+
glyphs: 0,
|
|
3241
|
+
glyphDrawCalls: 0,
|
|
3242
|
+
glyphRunBatches: 0,
|
|
3243
|
+
glyphRunBatchFallbacks: 0,
|
|
3244
|
+
glyphRunsDropped: 0,
|
|
3245
|
+
screenEffects: 0,
|
|
3246
|
+
screenEffectFailures: 0,
|
|
3247
|
+
externalEffects: 0,
|
|
3248
|
+
externalEffectFailures: 0,
|
|
3249
|
+
rotatedClipFallbacks: 0,
|
|
3250
|
+
unbalancedClipPops: 0,
|
|
3251
|
+
unknownCommands: 0,
|
|
3252
|
+
maxBatchQuads: 0,
|
|
3253
|
+
flushes: {
|
|
3254
|
+
textureSlots: 0,
|
|
3255
|
+
colorMatrices: 0,
|
|
3256
|
+
blend: 0,
|
|
3257
|
+
clip: 0,
|
|
3258
|
+
glyphs: 0,
|
|
3259
|
+
effects: 0,
|
|
3260
|
+
meshes: 0,
|
|
3261
|
+
compiled: 0,
|
|
3262
|
+
end: 0
|
|
3263
|
+
},
|
|
3264
|
+
compiledPlanBuilds: 0,
|
|
3265
|
+
compiledPlanReuses: 0,
|
|
3266
|
+
compiledTemplateRangeUpdates: 0,
|
|
3267
|
+
reusedSelections: 0,
|
|
3268
|
+
reusedBatches: 0,
|
|
3269
|
+
compiledGpuFullUploads: 0,
|
|
3270
|
+
compiledGpuRangeUploads: 0,
|
|
3271
|
+
compiledCachedDrawCalls: 0
|
|
3272
|
+
};
|
|
3273
|
+
const clipStack = createClipStack();
|
|
3274
|
+
const scissor = createScissorBox();
|
|
3275
|
+
const damageScissor = createScissorBox();
|
|
3276
|
+
const appliedScissor = createScissorBox();
|
|
3277
|
+
let appliedRoundedRadius = -1;
|
|
3278
|
+
/** The rounded-clip rect last uploaded: centre.xy, half-extent.xy. */
|
|
3279
|
+
const appliedRounded = new Float32Array(4);
|
|
3280
|
+
let appliedBlend = null;
|
|
3281
|
+
const quadView = createQuadView();
|
|
3282
|
+
const patchView = createNinePatchView();
|
|
3283
|
+
const lineView = createPolylineView(64);
|
|
3284
|
+
const clipView = createClipRectView();
|
|
3285
|
+
const glyphsView = createGlyphsView(64);
|
|
3286
|
+
const glyphRunViewPool = [];
|
|
3287
|
+
const glyphRunViews = [];
|
|
3288
|
+
const texturedMeshView = createTexturedMeshView(64, 96);
|
|
3289
|
+
const bands = createNinePatchBands();
|
|
3290
|
+
let strokeQuads = new Float32Array(256 * 8);
|
|
3291
|
+
let meshVertices = new Float32Array(256);
|
|
3292
|
+
const batcher = createQuadBatcher({
|
|
3293
|
+
maxTextureSlots,
|
|
3294
|
+
maxColorMatrices,
|
|
3295
|
+
quadCapacity: options.quadCapacity,
|
|
3296
|
+
draw: drawBatch
|
|
3297
|
+
});
|
|
3298
|
+
const compiledGpuPlans = /* @__PURE__ */ new WeakMap();
|
|
3299
|
+
const liveCompiledGpuPlans = /* @__PURE__ */ new Set();
|
|
3300
|
+
const cachedQuad = createQuadInstance();
|
|
3301
|
+
function whiteTexture() {
|
|
3302
|
+
if (suppliedWhite) return suppliedWhite;
|
|
3303
|
+
if (ownedWhite) return ownedWhite;
|
|
3304
|
+
const texture = gl.createTexture();
|
|
3305
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
3306
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
3307
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
3308
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
3309
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([
|
|
3310
|
+
255,
|
|
3311
|
+
255,
|
|
3312
|
+
255,
|
|
3313
|
+
255
|
|
3314
|
+
]));
|
|
3315
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
3316
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
3317
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
3318
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
3319
|
+
ownedWhite = {
|
|
3320
|
+
texture,
|
|
3321
|
+
width: 1,
|
|
3322
|
+
height: 1
|
|
3323
|
+
};
|
|
3324
|
+
return ownedWhite;
|
|
3325
|
+
}
|
|
3326
|
+
function ensureProgram() {
|
|
3327
|
+
if (program) return program;
|
|
3328
|
+
const linked = linkProgram(gl, VERTEX_SRC, fragmentSource(maxTextureSlots, maxColorMatrices));
|
|
3329
|
+
if (!linked) return null;
|
|
3330
|
+
const vao = gl.createVertexArray();
|
|
3331
|
+
const cornerBuffer = gl.createBuffer();
|
|
3332
|
+
const instanceBuffer = gl.createBuffer();
|
|
3333
|
+
gl.bindVertexArray(vao);
|
|
3334
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, cornerBuffer);
|
|
3335
|
+
gl.bufferData(gl.ARRAY_BUFFER, CORNERS, gl.STATIC_DRAW);
|
|
3336
|
+
gl.enableVertexAttribArray(0);
|
|
3337
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
3338
|
+
gl.vertexAttribDivisor(0, 0);
|
|
3339
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
|
|
3340
|
+
const strideBytes = 72;
|
|
3341
|
+
for (const attribute of INSTANCE_ATTRIBUTES) {
|
|
3342
|
+
gl.enableVertexAttribArray(attribute.location);
|
|
3343
|
+
gl.vertexAttribPointer(attribute.location, attribute.size, gl.FLOAT, false, strideBytes, attribute.offset * 4);
|
|
3344
|
+
gl.vertexAttribDivisor(attribute.location, 1);
|
|
3345
|
+
}
|
|
3346
|
+
gl.bindVertexArray(null);
|
|
3347
|
+
gl.useProgram(linked);
|
|
3348
|
+
const units = new Int32Array(maxTextureSlots);
|
|
3349
|
+
for (let i = 0; i < maxTextureSlots; i += 1) units[i] = i;
|
|
3350
|
+
const uTextures = gl.getUniformLocation(linked, "u_textures[0]");
|
|
3351
|
+
if (uTextures) gl.uniform1iv(uTextures, units);
|
|
3352
|
+
program = {
|
|
3353
|
+
program: linked,
|
|
3354
|
+
vao,
|
|
3355
|
+
cornerBuffer,
|
|
3356
|
+
instanceBuffer,
|
|
3357
|
+
instanceBytes: 0,
|
|
3358
|
+
uProjection: gl.getUniformLocation(linked, "u_projection"),
|
|
3359
|
+
uColorMatrices: gl.getUniformLocation(linked, "u_colorMatrices[0]"),
|
|
3360
|
+
uRoundedRect: gl.getUniformLocation(linked, "u_roundedRect"),
|
|
3361
|
+
uRoundedRadius: gl.getUniformLocation(linked, "u_roundedRadius")
|
|
3362
|
+
};
|
|
3363
|
+
return program;
|
|
3364
|
+
}
|
|
3365
|
+
function ensureMeshProgram() {
|
|
3366
|
+
if (meshProgram) return meshProgram;
|
|
3367
|
+
const linked = linkProgram(gl, TEXTURED_MESH_VERTEX_SRC, TEXTURED_MESH_FRAGMENT_SRC);
|
|
3368
|
+
if (!linked) return null;
|
|
3369
|
+
const vao = gl.createVertexArray();
|
|
3370
|
+
const vertexBuffer = gl.createBuffer();
|
|
3371
|
+
const indexBuffer = gl.createBuffer();
|
|
3372
|
+
gl.bindVertexArray(vao);
|
|
3373
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
|
3374
|
+
gl.enableVertexAttribArray(0);
|
|
3375
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 16, 0);
|
|
3376
|
+
gl.enableVertexAttribArray(1);
|
|
3377
|
+
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 16, 8);
|
|
3378
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
3379
|
+
gl.bindVertexArray(null);
|
|
3380
|
+
gl.useProgram(linked);
|
|
3381
|
+
const uTexture = gl.getUniformLocation(linked, "u_texture");
|
|
3382
|
+
if (uTexture) gl.uniform1i(uTexture, 0);
|
|
3383
|
+
meshProgram = {
|
|
3384
|
+
program: linked,
|
|
3385
|
+
vao,
|
|
3386
|
+
vertexBuffer,
|
|
3387
|
+
indexBuffer,
|
|
3388
|
+
vertexBytes: 0,
|
|
3389
|
+
indexBytes: 0,
|
|
3390
|
+
uProjection: gl.getUniformLocation(linked, "u_projection"),
|
|
3391
|
+
uTint: gl.getUniformLocation(linked, "u_tint"),
|
|
3392
|
+
uTexture
|
|
3393
|
+
};
|
|
3394
|
+
return meshProgram;
|
|
3395
|
+
}
|
|
3396
|
+
function drawBatch(batch) {
|
|
3397
|
+
const current = program;
|
|
3398
|
+
if (!current) return;
|
|
3399
|
+
const floats = batch.quadCount * 18;
|
|
3400
|
+
const bytes = floats * 4;
|
|
3401
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, current.instanceBuffer);
|
|
3402
|
+
if (current.instanceBytes < bytes) {
|
|
3403
|
+
gl.bufferData(gl.ARRAY_BUFFER, bytes, gl.DYNAMIC_DRAW);
|
|
3404
|
+
current.instanceBytes = bytes;
|
|
3405
|
+
}
|
|
3406
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, batch.instances, 0, floats);
|
|
3407
|
+
for (let slot = 0; slot < batch.textureCount; slot += 1) {
|
|
3408
|
+
const entry = batch.textures[slot];
|
|
3409
|
+
gl.activeTexture(gl.TEXTURE0 + slot);
|
|
3410
|
+
gl.bindTexture(gl.TEXTURE_2D, entry ? entry.texture : null);
|
|
3411
|
+
}
|
|
3412
|
+
if (batch.colorMatrixCount > 1 && current.uColorMatrices) gl.uniformMatrix3fv(current.uColorMatrices, true, batch.colorMatrices, 0, batch.colorMatrixCount * 9);
|
|
3413
|
+
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, batch.quadCount);
|
|
3414
|
+
stats.batches += 1;
|
|
3415
|
+
stats.textureBinds += batch.textureCount;
|
|
3416
|
+
if (batch.quadCount > stats.maxBatchQuads) stats.maxBatchQuads = batch.quadCount;
|
|
3417
|
+
stats.flushes[batch.reason] += 1;
|
|
3418
|
+
}
|
|
3419
|
+
function releaseGpuPlan(cache, deleteGl) {
|
|
3420
|
+
if (deleteGl) for (const run of cache.runs) {
|
|
3421
|
+
gl.deleteBuffer(run.buffer);
|
|
3422
|
+
gl.deleteVertexArray(run.vao);
|
|
3423
|
+
}
|
|
3424
|
+
compiledGpuPlans.delete(cache.plan);
|
|
3425
|
+
liveCompiledGpuPlans.delete(cache);
|
|
3426
|
+
}
|
|
3427
|
+
function makeCachedVao(current, buffer) {
|
|
3428
|
+
const vao = gl.createVertexArray();
|
|
3429
|
+
if (!vao) return null;
|
|
3430
|
+
gl.bindVertexArray(vao);
|
|
3431
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, current.cornerBuffer);
|
|
3432
|
+
gl.enableVertexAttribArray(0);
|
|
3433
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
3434
|
+
gl.vertexAttribDivisor(0, 0);
|
|
3435
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
3436
|
+
const strideBytes = 72;
|
|
3437
|
+
for (const attribute of INSTANCE_ATTRIBUTES) {
|
|
3438
|
+
gl.enableVertexAttribArray(attribute.location);
|
|
3439
|
+
gl.vertexAttribPointer(attribute.location, attribute.size, gl.FLOAT, false, strideBytes, attribute.offset * 4);
|
|
3440
|
+
gl.vertexAttribDivisor(attribute.location, 1);
|
|
3441
|
+
}
|
|
3442
|
+
gl.bindVertexArray(current.vao);
|
|
3443
|
+
return vao;
|
|
3444
|
+
}
|
|
3445
|
+
function matrixSlot(matrices, count, source, sourceOffset) {
|
|
3446
|
+
for (let slot = 0; slot < count; slot += 1) if (colorMatricesEqual(matrices, slot * 9, source, sourceOffset)) return slot;
|
|
3447
|
+
return -1;
|
|
3448
|
+
}
|
|
3449
|
+
function writeCachedQuad(run, item, index, plan, textureSlot, matrixSlotIndex) {
|
|
3450
|
+
const texture = run.textures[textureSlot];
|
|
3451
|
+
plan.fillQuad(index, texture.width, texture.height, cachedQuad);
|
|
3452
|
+
const at = item * 18;
|
|
3453
|
+
run.instances[at] = cachedQuad.x0;
|
|
3454
|
+
run.instances[at + 1] = cachedQuad.y0;
|
|
3455
|
+
run.instances[at + 2] = cachedQuad.x1;
|
|
3456
|
+
run.instances[at + 3] = cachedQuad.y1;
|
|
3457
|
+
run.instances[at + 4] = cachedQuad.x2;
|
|
3458
|
+
run.instances[at + 5] = cachedQuad.y2;
|
|
3459
|
+
run.instances[at + 6] = cachedQuad.x3;
|
|
3460
|
+
run.instances[at + 7] = cachedQuad.y3;
|
|
3461
|
+
run.instances[at + 8] = cachedQuad.u0;
|
|
3462
|
+
run.instances[at + 9] = cachedQuad.v0;
|
|
3463
|
+
run.instances[at + 10] = cachedQuad.uSpan;
|
|
3464
|
+
run.instances[at + 11] = cachedQuad.vSpan;
|
|
3465
|
+
run.instances[at + 12] = cachedQuad.r;
|
|
3466
|
+
run.instances[at + 13] = cachedQuad.g;
|
|
3467
|
+
run.instances[at + 14] = cachedQuad.b;
|
|
3468
|
+
run.instances[at + 15] = cachedQuad.a;
|
|
3469
|
+
run.instances[at + 16] = textureSlot;
|
|
3470
|
+
run.instances[at + 17] = matrixSlotIndex;
|
|
3471
|
+
}
|
|
3472
|
+
function buildGpuPlan(plan, current, generation, contentRevision) {
|
|
3473
|
+
const list = plan.list;
|
|
3474
|
+
const runs = [];
|
|
3475
|
+
const runsAt = new Array(list.count).fill(null);
|
|
3476
|
+
const runsForCommand = new Array(list.count).fill(null);
|
|
3477
|
+
const itemsForCommand = new Int32Array(list.count);
|
|
3478
|
+
const discardPartial = () => {
|
|
3479
|
+
for (const run of runs) {
|
|
3480
|
+
gl.deleteBuffer(run.buffer);
|
|
3481
|
+
gl.deleteVertexArray(run.vao);
|
|
3482
|
+
}
|
|
3483
|
+
};
|
|
3484
|
+
for (const descriptor of plan.batches) {
|
|
3485
|
+
let cursor = descriptor.start;
|
|
3486
|
+
while (cursor < descriptor.end) {
|
|
3487
|
+
const commandIndexes = [];
|
|
3488
|
+
const textures = [];
|
|
3489
|
+
const matrices = new Float32Array(maxColorMatrices * 9);
|
|
3490
|
+
matrices.set(IDENTITY_COLOR_MATRIX);
|
|
3491
|
+
let matrixCount = 1;
|
|
3492
|
+
while (cursor < descriptor.end) {
|
|
3493
|
+
const texture = list.textureAt(cursor) ?? whiteTexture();
|
|
3494
|
+
let textureSlot = textures.indexOf(texture);
|
|
3495
|
+
if (textureSlot < 0 && textures.length >= maxTextureSlots) break;
|
|
3496
|
+
const matrixIndex = list.colorMatrixIndexAt(cursor);
|
|
3497
|
+
let slot = 0;
|
|
3498
|
+
if (matrixIndex >= 0) {
|
|
3499
|
+
slot = matrixSlot(matrices, matrixCount, list.colorMatrices, matrixIndex * 9);
|
|
3500
|
+
if (slot < 0 && matrixCount >= maxColorMatrices) break;
|
|
3501
|
+
}
|
|
3502
|
+
if (textureSlot < 0) {
|
|
3503
|
+
textureSlot = textures.length;
|
|
3504
|
+
textures.push(texture);
|
|
3505
|
+
}
|
|
3506
|
+
if (matrixIndex >= 0 && slot < 0) {
|
|
3507
|
+
slot = matrixCount;
|
|
3508
|
+
matrices.set(list.colorMatrices.subarray(matrixIndex * 9, (matrixIndex + 1) * 9), slot * 9);
|
|
3509
|
+
matrixCount += 1;
|
|
3510
|
+
}
|
|
3511
|
+
commandIndexes.push(cursor);
|
|
3512
|
+
cursor += 1;
|
|
3513
|
+
}
|
|
3514
|
+
const buffer = gl.createBuffer();
|
|
3515
|
+
if (!buffer) {
|
|
3516
|
+
discardPartial();
|
|
3517
|
+
return null;
|
|
3518
|
+
}
|
|
3519
|
+
const vao = makeCachedVao(current, buffer);
|
|
3520
|
+
if (!vao) {
|
|
3521
|
+
gl.deleteBuffer(buffer);
|
|
3522
|
+
discardPartial();
|
|
3523
|
+
return null;
|
|
3524
|
+
}
|
|
3525
|
+
const commands = Int32Array.from(commandIndexes);
|
|
3526
|
+
const run = {
|
|
3527
|
+
start: commands[0],
|
|
3528
|
+
end: commands[commands.length - 1] + 1,
|
|
3529
|
+
blend: descriptor.blend,
|
|
3530
|
+
commands,
|
|
3531
|
+
textures,
|
|
3532
|
+
dimensions: new Int32Array(textures.length * 2),
|
|
3533
|
+
itemsByTexture: textures.map(() => []),
|
|
3534
|
+
colorMatrices: matrices.subarray(0, matrixCount * 9),
|
|
3535
|
+
colorMatrixCount: matrixCount,
|
|
3536
|
+
instances: new Float32Array(commands.length * 18),
|
|
3537
|
+
buffer,
|
|
3538
|
+
vao
|
|
3539
|
+
};
|
|
3540
|
+
for (let item = 0; item < commands.length; item += 1) {
|
|
3541
|
+
const index = commands[item];
|
|
3542
|
+
const textureSlot = textures.indexOf(list.textureAt(index) ?? whiteTexture());
|
|
3543
|
+
const matrixIndex = list.colorMatrixIndexAt(index);
|
|
3544
|
+
const slot = matrixIndex < 0 ? 0 : matrixSlot(run.colorMatrices, matrixCount, list.colorMatrices, matrixIndex * 9);
|
|
3545
|
+
writeCachedQuad(run, item, index, plan, textureSlot, slot);
|
|
3546
|
+
run.itemsByTexture[textureSlot].push(item);
|
|
3547
|
+
}
|
|
3548
|
+
for (let slot = 0; slot < textures.length; slot += 1) {
|
|
3549
|
+
run.dimensions[slot * 2] = textures[slot].width;
|
|
3550
|
+
run.dimensions[slot * 2 + 1] = textures[slot].height;
|
|
3551
|
+
}
|
|
3552
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
3553
|
+
gl.bufferData(gl.ARRAY_BUFFER, run.instances, gl.DYNAMIC_DRAW);
|
|
3554
|
+
stats.compiledGpuFullUploads += 1;
|
|
3555
|
+
runs.push(run);
|
|
3556
|
+
runsAt[run.start] = run;
|
|
3557
|
+
for (let item = 0; item < run.commands.length; item += 1) {
|
|
3558
|
+
const index = run.commands[item];
|
|
3559
|
+
runsForCommand[index] = run;
|
|
3560
|
+
itemsForCommand[index] = item;
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
gl.bindVertexArray(current.vao);
|
|
3565
|
+
const cache = {
|
|
3566
|
+
plan,
|
|
3567
|
+
generation,
|
|
3568
|
+
contentRevision,
|
|
3569
|
+
runs,
|
|
3570
|
+
runsAt,
|
|
3571
|
+
runsForCommand,
|
|
3572
|
+
itemsForCommand
|
|
3573
|
+
};
|
|
3574
|
+
compiledGpuPlans.set(plan, cache);
|
|
3575
|
+
liveCompiledGpuPlans.add(cache);
|
|
3576
|
+
return cache;
|
|
3577
|
+
}
|
|
3578
|
+
function updateGpuPlan(cache, current, changedCommands, contentRevision) {
|
|
3579
|
+
const list = cache.plan.list;
|
|
3580
|
+
const updateItem = (run, item) => {
|
|
3581
|
+
const index = run.commands[item];
|
|
3582
|
+
const texture = list.textureAt(index) ?? whiteTexture();
|
|
3583
|
+
const textureSlot = run.textures.indexOf(texture);
|
|
3584
|
+
if (textureSlot < 0) return false;
|
|
3585
|
+
if (run.instances[item * 18 + 16] !== textureSlot) return false;
|
|
3586
|
+
const matrixIndex = list.colorMatrixIndexAt(index);
|
|
3587
|
+
const matrixSlotIndex = matrixIndex < 0 ? 0 : matrixSlot(run.colorMatrices, run.colorMatrixCount, list.colorMatrices, matrixIndex * 9);
|
|
3588
|
+
if (matrixSlotIndex < 0) return false;
|
|
3589
|
+
writeCachedQuad(run, item, index, cache.plan, textureSlot, matrixSlotIndex);
|
|
3590
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, run.buffer);
|
|
3591
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, item * 18 * 4, run.instances, item * 18, 18);
|
|
3592
|
+
stats.compiledGpuRangeUploads += 1;
|
|
3593
|
+
return true;
|
|
3594
|
+
};
|
|
3595
|
+
for (const index of changedCommands) {
|
|
3596
|
+
const run = cache.runsForCommand[index];
|
|
3597
|
+
if (!run) continue;
|
|
3598
|
+
if (!updateItem(run, cache.itemsForCommand[index])) {
|
|
3599
|
+
releaseGpuPlan(cache, true);
|
|
3600
|
+
return buildGpuPlan(cache.plan, current, cache.generation, contentRevision);
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
for (const run of cache.runs) for (let slot = 0; slot < run.textures.length; slot += 1) {
|
|
3604
|
+
const texture = run.textures[slot];
|
|
3605
|
+
if (run.dimensions[slot * 2] === texture.width && run.dimensions[slot * 2 + 1] === texture.height) continue;
|
|
3606
|
+
run.dimensions[slot * 2] = texture.width;
|
|
3607
|
+
run.dimensions[slot * 2 + 1] = texture.height;
|
|
3608
|
+
for (const item of run.itemsByTexture[slot]) if (!updateItem(run, item)) {
|
|
3609
|
+
releaseGpuPlan(cache, true);
|
|
3610
|
+
return buildGpuPlan(cache.plan, current, cache.generation, contentRevision);
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
gl.bindVertexArray(current.vao);
|
|
3614
|
+
cache.contentRevision = contentRevision;
|
|
3615
|
+
return cache;
|
|
3616
|
+
}
|
|
3617
|
+
function drawCachedRange(run, first, count, current) {
|
|
3618
|
+
applyBlend(run.blend);
|
|
3619
|
+
gl.bindVertexArray(run.vao);
|
|
3620
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, run.buffer);
|
|
3621
|
+
const strideBytes = 72;
|
|
3622
|
+
for (const attribute of INSTANCE_ATTRIBUTES) gl.vertexAttribPointer(attribute.location, attribute.size, gl.FLOAT, false, strideBytes, first * strideBytes + attribute.offset * 4);
|
|
3623
|
+
for (let slot = 0; slot < run.textures.length; slot += 1) {
|
|
3624
|
+
gl.activeTexture(gl.TEXTURE0 + slot);
|
|
3625
|
+
gl.bindTexture(gl.TEXTURE_2D, run.textures[slot].texture);
|
|
3626
|
+
}
|
|
3627
|
+
if (run.colorMatrixCount > 1 && current.uColorMatrices) gl.uniformMatrix3fv(current.uColorMatrices, true, run.colorMatrices, 0, run.colorMatrixCount * 9);
|
|
3628
|
+
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, count);
|
|
3629
|
+
stats.batches += 1;
|
|
3630
|
+
stats.textureBinds += run.textures.length;
|
|
3631
|
+
stats.quads += count;
|
|
3632
|
+
stats.maxBatchQuads = Math.max(stats.maxBatchQuads, count);
|
|
3633
|
+
stats.compiledCachedDrawCalls += 1;
|
|
3634
|
+
gl.bindVertexArray(current.vao);
|
|
3635
|
+
}
|
|
3636
|
+
/**
|
|
3637
|
+
* Forget everything this executor believes about the GL state it set.
|
|
3638
|
+
*
|
|
3639
|
+
* TWO CALLERS, ONE RULE: it does not own the context. `execute` calls it on entry because
|
|
3640
|
+
* something else may have drawn into the context since the last frame, and the glyph pass path
|
|
3641
|
+
* calls it on the way back because something else just did, in the middle of this one. Every
|
|
3642
|
+
* cached value here guards a GL call that would otherwise be skipped, so a stale cache is not a
|
|
3643
|
+
* redundant call — it is a call that never happens and state that is silently somebody else's.
|
|
3644
|
+
*
|
|
3645
|
+
* `appliedBlend` is the one that bites hardest today: `@godot-scene-web/hb-gpu` sets the
|
|
3646
|
+
* NON-separate `blendEquation`/`blendFunc`, which write both the RGB and the alpha halves, so
|
|
3647
|
+
* without this the next quad keeps the glyph pass's blend and nothing errors.
|
|
3648
|
+
*/
|
|
3649
|
+
function invalidateAppliedState() {
|
|
3650
|
+
appliedBlend = null;
|
|
3651
|
+
appliedRoundedRadius = -1;
|
|
3652
|
+
appliedScissor.x = -1;
|
|
3653
|
+
appliedScissor.y = -1;
|
|
3654
|
+
appliedScissor.width = -1;
|
|
3655
|
+
appliedScissor.height = -1;
|
|
3656
|
+
}
|
|
3657
|
+
function applyBlend(blend) {
|
|
3658
|
+
if (blend === appliedBlend) return;
|
|
3659
|
+
batcher.setBlend(blend);
|
|
3660
|
+
const state = blendStateFor(blend);
|
|
3661
|
+
gl.blendEquationSeparate(gl[state.equationRgb], gl[state.equationAlpha]);
|
|
3662
|
+
gl.blendFuncSeparate(gl[state.srcRgb], gl[state.dstRgb], gl[state.srcAlpha], gl[state.dstAlpha]);
|
|
3663
|
+
appliedBlend = blend;
|
|
3664
|
+
stats.blendChanges += 1;
|
|
3665
|
+
}
|
|
3666
|
+
function applyClip(transform, width, height, damage) {
|
|
3667
|
+
clipStack.scissor(transform, width, height, scissor);
|
|
3668
|
+
if (damage) {
|
|
3669
|
+
const left = Math.max(0, Math.min(width, Math.floor(damage.x)));
|
|
3670
|
+
const right = Math.max(left, Math.min(width, Math.ceil(damage.x + damage.width)));
|
|
3671
|
+
const top = Math.max(0, Math.min(height, Math.floor(damage.y)));
|
|
3672
|
+
const bottom = Math.max(top, Math.min(height, Math.ceil(damage.y + damage.height)));
|
|
3673
|
+
damageScissor.x = left;
|
|
3674
|
+
damageScissor.y = height - bottom;
|
|
3675
|
+
damageScissor.width = right - left;
|
|
3676
|
+
damageScissor.height = bottom - top;
|
|
3677
|
+
const clippedRight = Math.min(scissor.x + scissor.width, right);
|
|
3678
|
+
const clippedTop = Math.max(scissor.y, damageScissor.y);
|
|
3679
|
+
const clippedBottom = Math.min(scissor.y + scissor.height, damageScissor.y + damageScissor.height);
|
|
3680
|
+
scissor.x = Math.max(scissor.x, left);
|
|
3681
|
+
scissor.y = clippedTop;
|
|
3682
|
+
scissor.width = Math.max(0, clippedRight - scissor.x);
|
|
3683
|
+
scissor.height = Math.max(0, clippedBottom - clippedTop);
|
|
3684
|
+
}
|
|
3685
|
+
if (scissor.x !== appliedScissor.x || scissor.y !== appliedScissor.y || scissor.width !== appliedScissor.width || scissor.height !== appliedScissor.height) {
|
|
3686
|
+
gl.scissor(scissor.x, scissor.y, scissor.width, scissor.height);
|
|
3687
|
+
appliedScissor.x = scissor.x;
|
|
3688
|
+
appliedScissor.y = scissor.y;
|
|
3689
|
+
appliedScissor.width = scissor.width;
|
|
3690
|
+
appliedScissor.height = scissor.height;
|
|
3691
|
+
stats.scissorChanges += 1;
|
|
3692
|
+
}
|
|
3693
|
+
const current = program;
|
|
3694
|
+
if (!current) return;
|
|
3695
|
+
const rounded = clipStack.rounded();
|
|
3696
|
+
const radius = rounded ? rounded.radius : 0;
|
|
3697
|
+
const centerX = rounded ? rounded.centerX : 0;
|
|
3698
|
+
const centerY = rounded ? rounded.centerY : 0;
|
|
3699
|
+
const halfWidth = rounded ? rounded.halfWidth : 0;
|
|
3700
|
+
const halfHeight = rounded ? rounded.halfHeight : 0;
|
|
3701
|
+
if (radius === appliedRoundedRadius && centerX === appliedRounded[0] && centerY === appliedRounded[1] && halfWidth === appliedRounded[2] && halfHeight === appliedRounded[3]) return;
|
|
3702
|
+
if (current.uRoundedRadius) gl.uniform1f(current.uRoundedRadius, radius);
|
|
3703
|
+
if (current.uRoundedRect) gl.uniform4f(current.uRoundedRect, centerX, centerY, halfWidth, halfHeight);
|
|
3704
|
+
appliedRoundedRadius = radius;
|
|
3705
|
+
appliedRounded[0] = centerX;
|
|
3706
|
+
appliedRounded[1] = centerY;
|
|
3707
|
+
appliedRounded[2] = halfWidth;
|
|
3708
|
+
appliedRounded[3] = halfHeight;
|
|
3709
|
+
}
|
|
3710
|
+
/**
|
|
3711
|
+
* Emit one rect of a command: `localX..localH` is the destination in the
|
|
3712
|
+
* command's own local space (`0..w` by `0..h`), which `m` maps into design
|
|
3713
|
+
* space; `srcX..srcH` is the source in page pixels.
|
|
3714
|
+
*/
|
|
3715
|
+
function emitRect(m, localX, localY, localW, localH, srcX, srcY, srcW, srcH, texture, flipH, flipV, r, g, b, a, colorMatrix, colorMatrixOffset) {
|
|
3716
|
+
const quad = batcher.quad;
|
|
3717
|
+
const x1 = localX + localW;
|
|
3718
|
+
const y1 = localY + localH;
|
|
3719
|
+
const xx = m[0];
|
|
3720
|
+
const xy = m[1];
|
|
3721
|
+
const yx = m[2];
|
|
3722
|
+
const yy = m[3];
|
|
3723
|
+
const ox = m[4];
|
|
3724
|
+
const oy = m[5];
|
|
3725
|
+
quad.x0 = xx * localX + yx * localY + ox;
|
|
3726
|
+
quad.y0 = xy * localX + yy * localY + oy;
|
|
3727
|
+
quad.x1 = xx * x1 + yx * localY + ox;
|
|
3728
|
+
quad.y1 = xy * x1 + yy * localY + oy;
|
|
3729
|
+
quad.x2 = xx * x1 + yx * y1 + ox;
|
|
3730
|
+
quad.y2 = xy * x1 + yy * y1 + oy;
|
|
3731
|
+
quad.x3 = xx * localX + yx * y1 + ox;
|
|
3732
|
+
quad.y3 = xy * localX + yy * y1 + oy;
|
|
3733
|
+
const invW = 1 / Math.max(1, texture.width);
|
|
3734
|
+
const invH = 1 / Math.max(1, texture.height);
|
|
3735
|
+
let u0 = srcX * invW;
|
|
3736
|
+
let v0 = srcY * invH;
|
|
3737
|
+
let uSpan = srcW * invW;
|
|
3738
|
+
let vSpan = srcH * invH;
|
|
3739
|
+
if (flipH) {
|
|
3740
|
+
u0 += uSpan;
|
|
3741
|
+
uSpan = -uSpan;
|
|
3742
|
+
}
|
|
3743
|
+
if (flipV) {
|
|
3744
|
+
v0 += vSpan;
|
|
3745
|
+
vSpan = -vSpan;
|
|
3746
|
+
}
|
|
3747
|
+
quad.u0 = u0;
|
|
3748
|
+
quad.v0 = v0;
|
|
3749
|
+
quad.uSpan = uSpan;
|
|
3750
|
+
quad.vSpan = vSpan;
|
|
3751
|
+
quad.r = r;
|
|
3752
|
+
quad.g = g;
|
|
3753
|
+
quad.b = b;
|
|
3754
|
+
quad.a = a;
|
|
3755
|
+
batcher.push(texture, colorMatrix, colorMatrixOffset);
|
|
3756
|
+
stats.quads += 1;
|
|
3757
|
+
}
|
|
3758
|
+
function emitQuadCommand(list, index, compiled) {
|
|
3759
|
+
const texture = list.textureAt(index) ?? whiteTexture();
|
|
3760
|
+
if (compiled?.fillQuad(index, texture.width, texture.height, batcher.quad)) {
|
|
3761
|
+
applyBlend(list.ints[list.intOffsetAt(index)]);
|
|
3762
|
+
const matrixIndex = list.colorMatrixIndexAt(index);
|
|
3763
|
+
batcher.push(texture, matrixIndex >= 0 ? list.colorMatrices : null, matrixIndex >= 0 ? matrixIndex * 9 : 0);
|
|
3764
|
+
stats.quads += 1;
|
|
3765
|
+
return;
|
|
3766
|
+
}
|
|
3767
|
+
list.readQuad(index, quadView);
|
|
3768
|
+
applyBlend(quadView.blend);
|
|
3769
|
+
emitRect(quadView.m, 0, 0, quadView.w, quadView.h, quadView.srcX, quadView.srcY, quadView.srcW, quadView.srcH, texture, quadView.flipH, quadView.flipV, quadView.r, quadView.g, quadView.b, quadView.a, quadView.hasColorMatrix ? quadView.colorMatrix : null, 0);
|
|
3770
|
+
}
|
|
3771
|
+
function emitNinePatchCommand(list, index) {
|
|
3772
|
+
list.readNinePatch(index, patchView);
|
|
3773
|
+
applyBlend(patchView.blend);
|
|
3774
|
+
const texture = list.textureAt(index) ?? whiteTexture();
|
|
3775
|
+
const count = expandNinePatch(patchView, bands);
|
|
3776
|
+
stats.ninePatches += 1;
|
|
3777
|
+
stats.ninePatchQuads += count;
|
|
3778
|
+
for (let i = 0; i < count; i += 1) {
|
|
3779
|
+
const band = bands[i];
|
|
3780
|
+
const localX = patchView.flipH ? patchView.w - band.dstX - band.dstW : band.dstX;
|
|
3781
|
+
const localY = patchView.flipV ? patchView.h - band.dstY - band.dstH : band.dstY;
|
|
3782
|
+
emitRect(patchView.m, localX, localY, band.dstW, band.dstH, band.srcX, band.srcY, band.srcW, band.srcH, texture, patchView.flipH, patchView.flipV, patchView.r, patchView.g, patchView.b, patchView.a, patchView.hasColorMatrix ? patchView.colorMatrix : null, 0);
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
function emitPolylineCommand(list, index) {
|
|
3786
|
+
list.readPolyline(index, lineView);
|
|
3787
|
+
applyBlend(0);
|
|
3788
|
+
const white = whiteTexture();
|
|
3789
|
+
const needed = Math.max(0, 2 * lineView.pointCount - 3) * 8;
|
|
3790
|
+
if (strokeQuads.length < needed) strokeQuads = new Float32Array(Math.max(needed, strokeQuads.length * 2));
|
|
3791
|
+
const count = expandPolyline(lineView.points, lineView.pointCount, lineView.width, strokeQuads, 0);
|
|
3792
|
+
stats.polylines += 1;
|
|
3793
|
+
stats.polylineQuads += count;
|
|
3794
|
+
const quad = batcher.quad;
|
|
3795
|
+
for (let i = 0; i < count; i += 1) {
|
|
3796
|
+
const at = i * 8;
|
|
3797
|
+
quad.x0 = strokeQuads[at];
|
|
3798
|
+
quad.y0 = strokeQuads[at + 1];
|
|
3799
|
+
quad.x1 = strokeQuads[at + 2];
|
|
3800
|
+
quad.y1 = strokeQuads[at + 3];
|
|
3801
|
+
quad.x2 = strokeQuads[at + 4];
|
|
3802
|
+
quad.y2 = strokeQuads[at + 5];
|
|
3803
|
+
quad.x3 = strokeQuads[at + 6];
|
|
3804
|
+
quad.y3 = strokeQuads[at + 7];
|
|
3805
|
+
quad.u0 = 0;
|
|
3806
|
+
quad.v0 = 0;
|
|
3807
|
+
quad.uSpan = 0;
|
|
3808
|
+
quad.vSpan = 0;
|
|
3809
|
+
quad.r = lineView.r;
|
|
3810
|
+
quad.g = lineView.g;
|
|
3811
|
+
quad.b = lineView.b;
|
|
3812
|
+
quad.a = lineView.a;
|
|
3813
|
+
batcher.push(white, null, 0);
|
|
3814
|
+
stats.quads += 1;
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
/**
|
|
3818
|
+
* Execute one true indexed mesh between quad batches. The flush is before any
|
|
3819
|
+
* state mutation, preserving painter order. The mesh then borrows the context
|
|
3820
|
+
* only long enough to upload its pooled data and draw; program and VAO are
|
|
3821
|
+
* rebound to the quad executor before returning, while blend/scissor remain
|
|
3822
|
+
* the executor's own known state for the next command.
|
|
3823
|
+
*/
|
|
3824
|
+
function emitTexturedMeshCommand(list, index, projection, current) {
|
|
3825
|
+
batcher.flush("meshes");
|
|
3826
|
+
list.readTexturedMesh(index, texturedMeshView);
|
|
3827
|
+
applyBlend(texturedMeshView.blend);
|
|
3828
|
+
stats.texturedMeshes += 1;
|
|
3829
|
+
if (texturedMeshView.indexCount === 0) return;
|
|
3830
|
+
const mesh = ensureMeshProgram();
|
|
3831
|
+
if (!mesh) return;
|
|
3832
|
+
const vertexFloats = texturedMeshView.vertexCount * 4;
|
|
3833
|
+
if (meshVertices.length < vertexFloats) meshVertices = new Float32Array(Math.max(vertexFloats, meshVertices.length * 2));
|
|
3834
|
+
const m = texturedMeshView.m;
|
|
3835
|
+
for (let vertex = 0; vertex < texturedMeshView.vertexCount; vertex += 1) {
|
|
3836
|
+
const source = vertex * 2;
|
|
3837
|
+
const target = vertex * 4;
|
|
3838
|
+
const x = texturedMeshView.positions[source];
|
|
3839
|
+
const y = texturedMeshView.positions[source + 1];
|
|
3840
|
+
meshVertices[target] = m[0] * x + m[2] * y + m[4];
|
|
3841
|
+
meshVertices[target + 1] = m[1] * x + m[3] * y + m[5];
|
|
3842
|
+
meshVertices[target + 2] = texturedMeshView.uvs[source];
|
|
3843
|
+
meshVertices[target + 3] = texturedMeshView.uvs[source + 1];
|
|
3844
|
+
}
|
|
3845
|
+
gl.bindVertexArray(mesh.vao);
|
|
3846
|
+
gl.useProgram(mesh.program);
|
|
3847
|
+
if (mesh.uProjection) gl.uniform4f(mesh.uProjection, projection.toClip[0], projection.toClip[1], projection.toClip[2], projection.toClip[3]);
|
|
3848
|
+
if (mesh.uTint) gl.uniform4f(mesh.uTint, texturedMeshView.r, texturedMeshView.g, texturedMeshView.b, texturedMeshView.a);
|
|
3849
|
+
const vertexBytes = vertexFloats * 4;
|
|
3850
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, mesh.vertexBuffer);
|
|
3851
|
+
if (mesh.vertexBytes < vertexBytes) {
|
|
3852
|
+
gl.bufferData(gl.ARRAY_BUFFER, vertexBytes, gl.DYNAMIC_DRAW);
|
|
3853
|
+
mesh.vertexBytes = vertexBytes;
|
|
3854
|
+
}
|
|
3855
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, meshVertices, 0, vertexFloats);
|
|
3856
|
+
const indexBytes = texturedMeshView.indexCount * 4;
|
|
3857
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, mesh.indexBuffer);
|
|
3858
|
+
if (mesh.indexBytes < indexBytes) {
|
|
3859
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexBytes, gl.DYNAMIC_DRAW);
|
|
3860
|
+
mesh.indexBytes = indexBytes;
|
|
3861
|
+
}
|
|
3862
|
+
gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, texturedMeshView.indices, 0, texturedMeshView.indexCount);
|
|
3863
|
+
const texture = list.textureAt(index) ?? whiteTexture();
|
|
3864
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
3865
|
+
gl.bindTexture(gl.TEXTURE_2D, texture.texture);
|
|
3866
|
+
gl.drawElements(gl.TRIANGLES, texturedMeshView.indexCount, gl.UNSIGNED_INT, 0);
|
|
3867
|
+
stats.texturedMeshTriangles += texturedMeshView.indexCount / 3;
|
|
3868
|
+
stats.texturedMeshDrawCalls += 1;
|
|
3869
|
+
gl.bindVertexArray(current.vao);
|
|
3870
|
+
gl.useProgram(current.program);
|
|
3871
|
+
}
|
|
3872
|
+
/**
|
|
3873
|
+
* Hand one glyph run to the installed pass, then put the context back the way it was.
|
|
3874
|
+
*
|
|
3875
|
+
* THE ORDER IS THE MODULE'S OWN RULE (see the note at the top of this file): a flush DRAWS with
|
|
3876
|
+
* whatever GL state is live, so the pending batch has to go out BEFORE the pass replaces the
|
|
3877
|
+
* program and the vertex array — never after. Doing it the other way round draws this frame's
|
|
3878
|
+
* quads through a glyph shader, which is a blank rectangle rather than anything that reads as an
|
|
3879
|
+
* ordering bug.
|
|
3880
|
+
*
|
|
3881
|
+
* WHAT IS RESTORED, AND WHY EACH ONE IS NEEDED. `drawBatch` re-binds `ARRAY_BUFFER` and its
|
|
3882
|
+
* textures per batch, so those look after themselves; the VAO and the program are bound ONCE per
|
|
3883
|
+
* frame, and a pass that leaves the VAO unbound (hb-gpu's `end` does exactly that, deliberately,
|
|
3884
|
+
* because WebGL2 has no cheap read-back of the binding) would make the next `drawArraysInstanced`
|
|
3885
|
+
* read attributes from nothing. The cached state goes through
|
|
3886
|
+
* {@link invalidateAppliedState} rather than being re-applied eagerly, so a run followed by
|
|
3887
|
+
* nothing costs no GL calls at all.
|
|
3888
|
+
*
|
|
3889
|
+
* NOT restored, because the pass is forbidden to touch it: `SCISSOR_TEST`, the scissor box and
|
|
3890
|
+
* the viewport. That is what makes a clip rect clip a glyph run.
|
|
3891
|
+
*/
|
|
3892
|
+
function emitGlyphsCommand(list, index, projection, current) {
|
|
3893
|
+
if (!glyphPass) {
|
|
3894
|
+
stats.glyphRunsDropped += 1;
|
|
3895
|
+
return;
|
|
3896
|
+
}
|
|
3897
|
+
batcher.flush("glyphs");
|
|
3898
|
+
list.readGlyphs(index, glyphsView);
|
|
3899
|
+
const drawn = glyphPass.drawRun(glyphsView, projection);
|
|
3900
|
+
stats.glyphRuns += 1;
|
|
3901
|
+
stats.glyphs += drawn.glyphs;
|
|
3902
|
+
stats.glyphDrawCalls += drawn.drawCalls;
|
|
3903
|
+
gl.bindVertexArray(current.vao);
|
|
3904
|
+
gl.useProgram(current.program);
|
|
3905
|
+
appliedBlend = null;
|
|
3906
|
+
}
|
|
3907
|
+
function emitAdjacentGlyphCommands(list, firstIndex, projection, current, commandMask) {
|
|
3908
|
+
if (!glyphPass || !options.batchAdjacentGlyphRuns || !glyphPass.drawRuns) {
|
|
3909
|
+
emitGlyphsCommand(list, firstIndex, projection, current);
|
|
3910
|
+
return firstIndex;
|
|
3911
|
+
}
|
|
3912
|
+
let count = 0;
|
|
3913
|
+
for (let index = firstIndex; index < list.count; index += 1) {
|
|
3914
|
+
if (list.kindAt(index) !== 5 || commandMask && !commandMask.includes(index)) break;
|
|
3915
|
+
let view = glyphRunViewPool[count];
|
|
3916
|
+
if (!view) {
|
|
3917
|
+
view = createGlyphsView(64);
|
|
3918
|
+
glyphRunViewPool.push(view);
|
|
3919
|
+
}
|
|
3920
|
+
list.readGlyphs(index, view);
|
|
3921
|
+
count += 1;
|
|
3922
|
+
}
|
|
3923
|
+
if (count < 2) {
|
|
3924
|
+
emitGlyphsCommand(list, firstIndex, projection, current);
|
|
3925
|
+
return firstIndex;
|
|
3926
|
+
}
|
|
3927
|
+
glyphRunViews.length = count;
|
|
3928
|
+
for (let i = 0; i < count; i += 1) glyphRunViews[i] = glyphRunViewPool[i];
|
|
3929
|
+
if (glyphPass.canBatchRuns && !glyphPass.canBatchRuns(glyphRunViews)) {
|
|
3930
|
+
stats.glyphRunBatchFallbacks += 1;
|
|
3931
|
+
emitGlyphsCommand(list, firstIndex, projection, current);
|
|
3932
|
+
return firstIndex;
|
|
3933
|
+
}
|
|
3934
|
+
batcher.flush("glyphs");
|
|
3935
|
+
const drawn = glyphPass.drawRuns(glyphRunViews, projection);
|
|
3936
|
+
stats.glyphRuns += count;
|
|
3937
|
+
stats.glyphs += drawn.glyphs;
|
|
3938
|
+
stats.glyphDrawCalls += drawn.drawCalls;
|
|
3939
|
+
stats.glyphRunBatches += 1;
|
|
3940
|
+
gl.bindVertexArray(current.vao);
|
|
3941
|
+
gl.useProgram(current.program);
|
|
3942
|
+
appliedBlend = null;
|
|
3943
|
+
return firstIndex + count - 1;
|
|
3944
|
+
}
|
|
3945
|
+
function emitScreenEffectCommand(list, index, projection, damage, current) {
|
|
3946
|
+
const effect = list.screenEffectAt(index);
|
|
3947
|
+
if (!effect) {
|
|
3948
|
+
stats.unknownCommands += 1;
|
|
3949
|
+
return false;
|
|
3950
|
+
}
|
|
3951
|
+
batcher.flush("effects");
|
|
3952
|
+
const framebuffer = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING);
|
|
3953
|
+
const succeeded = effect.execute({
|
|
3954
|
+
gl,
|
|
3955
|
+
framebuffer,
|
|
3956
|
+
width: projection.framebufferWidth,
|
|
3957
|
+
height: projection.framebufferHeight,
|
|
3958
|
+
damage,
|
|
3959
|
+
scissor: appliedScissor
|
|
3960
|
+
});
|
|
3961
|
+
if (succeeded) stats.screenEffects += 1;
|
|
3962
|
+
else stats.screenEffectFailures += 1;
|
|
3963
|
+
gl.bindVertexArray(current.vao);
|
|
3964
|
+
gl.useProgram(current.program);
|
|
3965
|
+
gl.viewport(0, 0, projection.framebufferWidth, projection.framebufferHeight);
|
|
3966
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
3967
|
+
invalidateAppliedState();
|
|
3968
|
+
applyClip(projection.toFramebuffer, projection.framebufferWidth, projection.framebufferHeight, damage);
|
|
3969
|
+
applyBlend(0);
|
|
3970
|
+
return succeeded;
|
|
3971
|
+
}
|
|
3972
|
+
function emitExternalEffectCommand(list, index, projection, damage, current) {
|
|
3973
|
+
const effect = list.externalEffectAt(index);
|
|
3974
|
+
if (!effect) {
|
|
3975
|
+
stats.unknownCommands += 1;
|
|
3976
|
+
return false;
|
|
3977
|
+
}
|
|
3978
|
+
batcher.flush("effects");
|
|
3979
|
+
const framebuffer = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING);
|
|
3980
|
+
const succeeded = effect.execute({
|
|
3981
|
+
gl,
|
|
3982
|
+
framebuffer,
|
|
3983
|
+
width: projection.framebufferWidth,
|
|
3984
|
+
height: projection.framebufferHeight,
|
|
3985
|
+
damage,
|
|
3986
|
+
scissor: appliedScissor
|
|
3987
|
+
});
|
|
3988
|
+
if (succeeded) stats.externalEffects += 1;
|
|
3989
|
+
else stats.externalEffectFailures += 1;
|
|
3990
|
+
gl.bindVertexArray(current.vao);
|
|
3991
|
+
gl.useProgram(current.program);
|
|
3992
|
+
gl.viewport(0, 0, projection.framebufferWidth, projection.framebufferHeight);
|
|
3993
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
3994
|
+
invalidateAppliedState();
|
|
3995
|
+
applyClip(projection.toFramebuffer, projection.framebufferWidth, projection.framebufferHeight, damage);
|
|
3996
|
+
applyBlend(0);
|
|
3997
|
+
return succeeded;
|
|
3998
|
+
}
|
|
3999
|
+
return {
|
|
4000
|
+
gl,
|
|
4001
|
+
stats,
|
|
4002
|
+
maxTextureSlots,
|
|
4003
|
+
warmUp() {
|
|
4004
|
+
return ensureProgram() !== null && ensureMeshProgram() !== null;
|
|
4005
|
+
},
|
|
4006
|
+
releaseCompiled(plan) {
|
|
4007
|
+
const cache = compiledGpuPlans.get(plan);
|
|
4008
|
+
if (cache) releaseGpuPlan(cache, true);
|
|
4009
|
+
},
|
|
4010
|
+
execute(list, projection, executeOptions) {
|
|
4011
|
+
const current = ensureProgram();
|
|
4012
|
+
if (!current) return false;
|
|
4013
|
+
stats.commands = 0;
|
|
4014
|
+
stats.quads = 0;
|
|
4015
|
+
stats.batches = 0;
|
|
4016
|
+
stats.textureBinds = 0;
|
|
4017
|
+
stats.scissorChanges = 0;
|
|
4018
|
+
stats.blendChanges = 0;
|
|
4019
|
+
stats.ninePatches = 0;
|
|
4020
|
+
stats.ninePatchQuads = 0;
|
|
4021
|
+
stats.polylines = 0;
|
|
4022
|
+
stats.polylineQuads = 0;
|
|
4023
|
+
stats.texturedMeshes = 0;
|
|
4024
|
+
stats.texturedMeshTriangles = 0;
|
|
4025
|
+
stats.texturedMeshDrawCalls = 0;
|
|
4026
|
+
stats.glyphRuns = 0;
|
|
4027
|
+
stats.glyphs = 0;
|
|
4028
|
+
stats.glyphDrawCalls = 0;
|
|
4029
|
+
stats.glyphRunBatches = 0;
|
|
4030
|
+
stats.glyphRunBatchFallbacks = 0;
|
|
4031
|
+
stats.glyphRunsDropped = 0;
|
|
4032
|
+
stats.screenEffects = 0;
|
|
4033
|
+
stats.screenEffectFailures = 0;
|
|
4034
|
+
stats.externalEffects = 0;
|
|
4035
|
+
stats.externalEffectFailures = 0;
|
|
4036
|
+
stats.rotatedClipFallbacks = 0;
|
|
4037
|
+
stats.unbalancedClipPops = 0;
|
|
4038
|
+
stats.unknownCommands = 0;
|
|
4039
|
+
stats.maxBatchQuads = 0;
|
|
4040
|
+
stats.flushes.textureSlots = 0;
|
|
4041
|
+
stats.flushes.colorMatrices = 0;
|
|
4042
|
+
stats.flushes.blend = 0;
|
|
4043
|
+
stats.flushes.clip = 0;
|
|
4044
|
+
stats.flushes.glyphs = 0;
|
|
4045
|
+
stats.flushes.effects = 0;
|
|
4046
|
+
stats.flushes.meshes = 0;
|
|
4047
|
+
stats.flushes.compiled = 0;
|
|
4048
|
+
stats.flushes.end = 0;
|
|
4049
|
+
stats.compiledPlanBuilds = 0;
|
|
4050
|
+
stats.compiledPlanReuses = 0;
|
|
4051
|
+
stats.compiledTemplateRangeUpdates = 0;
|
|
4052
|
+
stats.reusedSelections = 0;
|
|
4053
|
+
stats.reusedBatches = 0;
|
|
4054
|
+
stats.compiledGpuFullUploads = 0;
|
|
4055
|
+
stats.compiledGpuRangeUploads = 0;
|
|
4056
|
+
stats.compiledCachedDrawCalls = 0;
|
|
4057
|
+
const compiled = executeOptions?.compiled?.list === list ? executeOptions.compiled : void 0;
|
|
4058
|
+
let gpuCache = null;
|
|
4059
|
+
if (compiled) {
|
|
4060
|
+
const refresh = compiled.refresh();
|
|
4061
|
+
stats.compiledPlanBuilds = refresh.rebuilt ? 1 : 0;
|
|
4062
|
+
stats.compiledPlanReuses = refresh.rebuilt ? 0 : 1;
|
|
4063
|
+
stats.compiledTemplateRangeUpdates = refresh.rangeUpdates;
|
|
4064
|
+
stats.reusedSelections = 0;
|
|
4065
|
+
stats.reusedBatches = refresh.rebuilt ? 0 : compiled.batches.length;
|
|
4066
|
+
const existing = compiledGpuPlans.get(compiled);
|
|
4067
|
+
if (existing && (existing.generation !== refresh.planGeneration || existing.contentRevision !== refresh.contentRevision && existing.contentRevision !== refresh.deltaBaseRevision)) releaseGpuPlan(existing, true);
|
|
4068
|
+
const currentCache = compiledGpuPlans.get(compiled);
|
|
4069
|
+
gpuCache = currentCache ? currentCache.contentRevision === refresh.contentRevision ? updateGpuPlan(currentCache, current, NO_CHANGED_COMMANDS, refresh.contentRevision) : updateGpuPlan(currentCache, current, refresh.changedCommands, refresh.contentRevision) : buildGpuPlan(compiled, current, refresh.planGeneration, refresh.contentRevision);
|
|
4070
|
+
}
|
|
4071
|
+
clipStack.reset();
|
|
4072
|
+
batcher.reset();
|
|
4073
|
+
const width = projection.framebufferWidth;
|
|
4074
|
+
const height = projection.framebufferHeight;
|
|
4075
|
+
gl.bindVertexArray(current.vao);
|
|
4076
|
+
gl.useProgram(current.program);
|
|
4077
|
+
gl.viewport(0, 0, width, height);
|
|
4078
|
+
if (current.uProjection) gl.uniform4f(current.uProjection, projection.toClip[0], projection.toClip[1], projection.toClip[2], projection.toClip[3]);
|
|
4079
|
+
gl.disable(gl.DEPTH_TEST);
|
|
4080
|
+
gl.disable(gl.CULL_FACE);
|
|
4081
|
+
gl.enable(gl.BLEND);
|
|
4082
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
4083
|
+
invalidateAppliedState();
|
|
4084
|
+
applyClip(projection.toFramebuffer, width, height, executeOptions?.damage);
|
|
4085
|
+
applyBlend(0);
|
|
4086
|
+
if (executeOptions?.clear !== false) {
|
|
4087
|
+
const clearColor = executeOptions?.clearColor;
|
|
4088
|
+
gl.clearColor(clearColor?.[0] ?? 0, clearColor?.[1] ?? 0, clearColor?.[2] ?? 0, clearColor?.[3] ?? 0);
|
|
4089
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
4090
|
+
}
|
|
4091
|
+
let clipSerial = 0;
|
|
4092
|
+
let screenEffectFailed = false;
|
|
4093
|
+
const count = list.count;
|
|
4094
|
+
for (let index = 0; index < count; index += 1) {
|
|
4095
|
+
if (screenEffectFailed) break;
|
|
4096
|
+
if (executeOptions?.commandMask && !executeOptions.commandMask.includes(index)) continue;
|
|
4097
|
+
const cached = executeOptions?.commandMask ? gpuCache?.runsForCommand[index] : gpuCache?.runsAt[index];
|
|
4098
|
+
if (cached) {
|
|
4099
|
+
const first = executeOptions?.commandMask ? gpuCache?.itemsForCommand[index] ?? 0 : 0;
|
|
4100
|
+
let cachedCount = 1;
|
|
4101
|
+
if (!executeOptions?.commandMask) cachedCount = cached.commands.length;
|
|
4102
|
+
else while (first + cachedCount < cached.commands.length && executeOptions.commandMask.includes(cached.commands[first + cachedCount])) cachedCount += 1;
|
|
4103
|
+
batcher.flush("compiled");
|
|
4104
|
+
drawCachedRange(cached, first, cachedCount, current);
|
|
4105
|
+
stats.commands += cachedCount;
|
|
4106
|
+
index = cached.commands[first + cachedCount - 1];
|
|
4107
|
+
continue;
|
|
4108
|
+
}
|
|
4109
|
+
stats.commands += 1;
|
|
4110
|
+
switch (list.kindAt(index)) {
|
|
4111
|
+
case 0:
|
|
4112
|
+
emitQuadCommand(list, index, compiled);
|
|
4113
|
+
break;
|
|
4114
|
+
case 1:
|
|
4115
|
+
emitNinePatchCommand(list, index);
|
|
4116
|
+
break;
|
|
4117
|
+
case 2:
|
|
4118
|
+
emitPolylineCommand(list, index);
|
|
4119
|
+
break;
|
|
4120
|
+
case 5:
|
|
4121
|
+
index = emitAdjacentGlyphCommands(list, index, projection, current, executeOptions?.commandMask);
|
|
4122
|
+
break;
|
|
4123
|
+
case 6:
|
|
4124
|
+
emitTexturedMeshCommand(list, index, projection, current);
|
|
4125
|
+
break;
|
|
4126
|
+
case 7:
|
|
4127
|
+
screenEffectFailed = !emitScreenEffectCommand(list, index, projection, executeOptions?.damage, current);
|
|
4128
|
+
break;
|
|
4129
|
+
case 8:
|
|
4130
|
+
screenEffectFailed = !emitExternalEffectCommand(list, index, projection, executeOptions?.damage, current);
|
|
4131
|
+
break;
|
|
4132
|
+
case 3:
|
|
4133
|
+
list.readClipRect(index, clipView);
|
|
4134
|
+
clipSerial += 1;
|
|
4135
|
+
batcher.setClipEpoch(clipSerial);
|
|
4136
|
+
clipStack.push(clipView);
|
|
4137
|
+
applyClip(projection.toFramebuffer, width, height, executeOptions?.damage);
|
|
4138
|
+
break;
|
|
4139
|
+
case 4:
|
|
4140
|
+
if (clipStack.depth === 0) {
|
|
4141
|
+
stats.unbalancedClipPops += 1;
|
|
4142
|
+
break;
|
|
4143
|
+
}
|
|
4144
|
+
clipSerial += 1;
|
|
4145
|
+
batcher.setClipEpoch(clipSerial);
|
|
4146
|
+
clipStack.pop();
|
|
4147
|
+
applyClip(projection.toFramebuffer, width, height, executeOptions?.damage);
|
|
4148
|
+
break;
|
|
4149
|
+
default:
|
|
4150
|
+
stats.unknownCommands += 1;
|
|
4151
|
+
break;
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
batcher.flush("end");
|
|
4155
|
+
stats.rotatedClipFallbacks = clipStack.rotatedFallbacks;
|
|
4156
|
+
gl.bindVertexArray(null);
|
|
4157
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
4158
|
+
return !screenEffectFailed;
|
|
4159
|
+
},
|
|
4160
|
+
invalidate() {
|
|
4161
|
+
program = null;
|
|
4162
|
+
meshProgram = null;
|
|
4163
|
+
ownedWhite = null;
|
|
4164
|
+
appliedBlend = null;
|
|
4165
|
+
appliedRoundedRadius = -1;
|
|
4166
|
+
for (const cache of [...liveCompiledGpuPlans]) releaseGpuPlan(cache, false);
|
|
4167
|
+
},
|
|
4168
|
+
dispose() {
|
|
4169
|
+
for (const cache of [...liveCompiledGpuPlans]) releaseGpuPlan(cache, true);
|
|
4170
|
+
if (program) {
|
|
4171
|
+
gl.deleteProgram(program.program);
|
|
4172
|
+
gl.deleteVertexArray(program.vao);
|
|
4173
|
+
gl.deleteBuffer(program.cornerBuffer);
|
|
4174
|
+
gl.deleteBuffer(program.instanceBuffer);
|
|
4175
|
+
program = null;
|
|
4176
|
+
}
|
|
4177
|
+
if (ownedWhite) {
|
|
4178
|
+
gl.deleteTexture(ownedWhite.texture);
|
|
4179
|
+
ownedWhite = null;
|
|
4180
|
+
}
|
|
4181
|
+
if (meshProgram) {
|
|
4182
|
+
gl.deleteProgram(meshProgram.program);
|
|
4183
|
+
gl.deleteVertexArray(meshProgram.vao);
|
|
4184
|
+
gl.deleteBuffer(meshProgram.vertexBuffer);
|
|
4185
|
+
gl.deleteBuffer(meshProgram.indexBuffer);
|
|
4186
|
+
meshProgram = null;
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
//#endregion
|
|
4192
|
+
//#region src/headless-effects.ts
|
|
4193
|
+
/** Bind a GPU screen producer at one DrawList painter position. */
|
|
4194
|
+
function createHeadlessScreenEffectCommand(producer, input) {
|
|
4195
|
+
return {
|
|
4196
|
+
screenDependent: true,
|
|
4197
|
+
execute(context) {
|
|
4198
|
+
return producer.renderScreen(input, context).ok;
|
|
4199
|
+
}
|
|
4200
|
+
};
|
|
4201
|
+
}
|
|
4202
|
+
/** Bind a direct GPU particle pass at one DrawList painter position. */
|
|
4203
|
+
function createHeadlessGodotParticleDirectEffect(pass, input) {
|
|
4204
|
+
return { execute(context) {
|
|
4205
|
+
return pass.draw(input, context).ok;
|
|
4206
|
+
} };
|
|
4207
|
+
}
|
|
4208
|
+
//#endregion
|
|
4209
|
+
//#region src/present.ts
|
|
4210
|
+
const CONTEXT_ATTRIBUTES = {
|
|
4211
|
+
alpha: true,
|
|
4212
|
+
premultipliedAlpha: true,
|
|
4213
|
+
stencil: false,
|
|
4214
|
+
depth: false,
|
|
4215
|
+
antialias: false,
|
|
4216
|
+
preserveDrawingBuffer: false
|
|
4217
|
+
};
|
|
4218
|
+
/** The attributes the stage asks for, exported so a test can assert the contract
|
|
4219
|
+
* rather than restate it. */
|
|
4220
|
+
const STAGE_CONTEXT_ATTRIBUTES = CONTEXT_ATTRIBUTES;
|
|
4221
|
+
/**
|
|
4222
|
+
* Create a stage on `canvas`, or `null` when the browser gives no WebGL2 context.
|
|
4223
|
+
*
|
|
4224
|
+
* Note what is NOT here: no software-renderer refusal. `html`'s shared context
|
|
4225
|
+
* declines SwiftShader/llvmpipe because running full-screen procedural fragment
|
|
4226
|
+
* shaders on a CPU rasterizer costs more than the CSS fallback it has. The stage
|
|
4227
|
+
* has no fallback to fall back TO — it is the renderer — and its fragment shader
|
|
4228
|
+
* is a texture fetch and a multiply, which software GL runs perfectly well. The
|
|
4229
|
+
* decision of whether this device should use the canvas renderer at all belongs
|
|
4230
|
+
* to the consumer, one level up, where the alternative is known.
|
|
4231
|
+
*/
|
|
4232
|
+
function createCanvasStage(options) {
|
|
4233
|
+
const canvas = options.canvas;
|
|
4234
|
+
const contextAttributes = options.alpha === void 0 ? CONTEXT_ATTRIBUTES : {
|
|
4235
|
+
...CONTEXT_ATTRIBUTES,
|
|
4236
|
+
alpha: options.alpha
|
|
4237
|
+
};
|
|
4238
|
+
let gl = null;
|
|
4239
|
+
try {
|
|
4240
|
+
gl = canvas.getContext("webgl2", contextAttributes);
|
|
4241
|
+
} catch {
|
|
4242
|
+
gl = null;
|
|
4243
|
+
}
|
|
4244
|
+
if (!gl) return null;
|
|
4245
|
+
const context = gl;
|
|
4246
|
+
const alpha = context.getContextAttributes?.()?.alpha ?? contextAttributes.alpha ?? true;
|
|
4247
|
+
let designWidth = Math.max(1, options.designWidth);
|
|
4248
|
+
let designHeight = Math.max(1, options.designHeight);
|
|
4249
|
+
let stageWidth = 0;
|
|
4250
|
+
let stageHeight = 0;
|
|
4251
|
+
let contextLost = false;
|
|
4252
|
+
const projection = {
|
|
4253
|
+
designWidth,
|
|
4254
|
+
designHeight,
|
|
4255
|
+
toClip: new Float32Array(4),
|
|
4256
|
+
toFramebuffer: new Float32Array([
|
|
4257
|
+
1,
|
|
4258
|
+
0,
|
|
4259
|
+
0,
|
|
4260
|
+
1,
|
|
4261
|
+
0,
|
|
4262
|
+
0
|
|
4263
|
+
]),
|
|
4264
|
+
framebufferWidth: 0,
|
|
4265
|
+
framebufferHeight: 0
|
|
4266
|
+
};
|
|
4267
|
+
function readBackingSize() {
|
|
4268
|
+
const width = context.drawingBufferWidth || canvas.width;
|
|
4269
|
+
const height = context.drawingBufferHeight || canvas.height;
|
|
4270
|
+
stageWidth = Math.max(1, width);
|
|
4271
|
+
stageHeight = Math.max(1, height);
|
|
4272
|
+
}
|
|
4273
|
+
function refreshProjection() {
|
|
4274
|
+
projection.designWidth = designWidth;
|
|
4275
|
+
projection.designHeight = designHeight;
|
|
4276
|
+
projection.toClip[0] = 2 / designWidth;
|
|
4277
|
+
projection.toClip[1] = -2 / designHeight;
|
|
4278
|
+
projection.toClip[2] = -1;
|
|
4279
|
+
projection.toClip[3] = 1;
|
|
4280
|
+
projection.toFramebuffer[0] = stageWidth / designWidth;
|
|
4281
|
+
projection.toFramebuffer[1] = 0;
|
|
4282
|
+
projection.toFramebuffer[2] = 0;
|
|
4283
|
+
projection.toFramebuffer[3] = stageHeight / designHeight;
|
|
4284
|
+
projection.toFramebuffer[4] = 0;
|
|
4285
|
+
projection.toFramebuffer[5] = 0;
|
|
4286
|
+
projection.framebufferWidth = stageWidth;
|
|
4287
|
+
projection.framebufferHeight = stageHeight;
|
|
4288
|
+
}
|
|
4289
|
+
readBackingSize();
|
|
4290
|
+
refreshProjection();
|
|
4291
|
+
const onLost = (event) => {
|
|
4292
|
+
event.preventDefault();
|
|
4293
|
+
contextLost = true;
|
|
4294
|
+
options.onContextLost?.();
|
|
4295
|
+
};
|
|
4296
|
+
const onRestored = () => {
|
|
4297
|
+
contextLost = false;
|
|
4298
|
+
readBackingSize();
|
|
4299
|
+
refreshProjection();
|
|
4300
|
+
options.onContextRestored?.();
|
|
4301
|
+
};
|
|
4302
|
+
canvas.addEventListener("webglcontextlost", onLost);
|
|
4303
|
+
canvas.addEventListener("webglcontextrestored", onRestored);
|
|
4304
|
+
return {
|
|
4305
|
+
canvas,
|
|
4306
|
+
gl: context,
|
|
4307
|
+
get designWidth() {
|
|
4308
|
+
return designWidth;
|
|
4309
|
+
},
|
|
4310
|
+
get designHeight() {
|
|
4311
|
+
return designHeight;
|
|
4312
|
+
},
|
|
4313
|
+
get stageWidth() {
|
|
4314
|
+
return stageWidth;
|
|
4315
|
+
},
|
|
4316
|
+
get stageHeight() {
|
|
4317
|
+
return stageHeight;
|
|
4318
|
+
},
|
|
4319
|
+
get contextLost() {
|
|
4320
|
+
return contextLost;
|
|
4321
|
+
},
|
|
4322
|
+
get alpha() {
|
|
4323
|
+
return alpha;
|
|
4324
|
+
},
|
|
4325
|
+
setStageSize(width, height) {
|
|
4326
|
+
const w = Math.max(1, Math.floor(width));
|
|
4327
|
+
const h = Math.max(1, Math.floor(height));
|
|
4328
|
+
if (canvas.width !== w) canvas.width = w;
|
|
4329
|
+
if (canvas.height !== h) canvas.height = h;
|
|
4330
|
+
readBackingSize();
|
|
4331
|
+
refreshProjection();
|
|
4332
|
+
},
|
|
4333
|
+
setDesignSize(width, height) {
|
|
4334
|
+
designWidth = Math.max(1, width);
|
|
4335
|
+
designHeight = Math.max(1, height);
|
|
4336
|
+
refreshProjection();
|
|
4337
|
+
},
|
|
4338
|
+
projection() {
|
|
4339
|
+
return projection;
|
|
4340
|
+
},
|
|
4341
|
+
applyViewport() {
|
|
4342
|
+
context.viewport(0, 0, stageWidth, stageHeight);
|
|
4343
|
+
},
|
|
4344
|
+
dispose() {
|
|
4345
|
+
canvas.removeEventListener("webglcontextlost", onLost);
|
|
4346
|
+
canvas.removeEventListener("webglcontextrestored", onRestored);
|
|
4347
|
+
}
|
|
4348
|
+
};
|
|
4349
|
+
}
|
|
4350
|
+
//#endregion
|
|
4351
|
+
//#region src/retained-surface.ts
|
|
4352
|
+
/** Snap a CSS/device calculation once at the allocation boundary. */
|
|
4353
|
+
function snapRetainedSize(value) {
|
|
4354
|
+
return Number.isFinite(value) ? Math.max(1, Math.round(value)) : 1;
|
|
4355
|
+
}
|
|
4356
|
+
function isValidDamage(damage, width, height) {
|
|
4357
|
+
return Number.isFinite(damage.x) && Number.isFinite(damage.y) && Number.isFinite(damage.width) && Number.isFinite(damage.height) && damage.width > 0 && damage.height > 0 && damage.x >= 0 && damage.y >= 0 && damage.x + damage.width <= width && damage.y + damage.height <= height;
|
|
4358
|
+
}
|
|
4359
|
+
/**
|
|
4360
|
+
* Create a retained RGBA8 texture/FBO. It deliberately does not create a
|
|
4361
|
+
* canvas or context — a stage owns that DOM-facing concern — and it does not
|
|
4362
|
+
* monkey-patch an executor. Retention is opt-in per replay/present call.
|
|
4363
|
+
*/
|
|
4364
|
+
function createRetainedSurface(gl) {
|
|
4365
|
+
let texture = null;
|
|
4366
|
+
let framebuffer = null;
|
|
4367
|
+
let width = 0;
|
|
4368
|
+
let height = 0;
|
|
4369
|
+
let contentValid = false;
|
|
4370
|
+
function discard(callGl) {
|
|
4371
|
+
if (callGl) {
|
|
4372
|
+
if (framebuffer) gl.deleteFramebuffer(framebuffer);
|
|
4373
|
+
if (texture) gl.deleteTexture(texture);
|
|
4374
|
+
}
|
|
4375
|
+
framebuffer = null;
|
|
4376
|
+
texture = null;
|
|
4377
|
+
width = 0;
|
|
4378
|
+
height = 0;
|
|
4379
|
+
contentValid = false;
|
|
4380
|
+
}
|
|
4381
|
+
function resize(requestedWidth, requestedHeight) {
|
|
4382
|
+
const nextWidth = snapRetainedSize(requestedWidth);
|
|
4383
|
+
const nextHeight = snapRetainedSize(requestedHeight);
|
|
4384
|
+
if (texture && framebuffer && width === nextWidth && height === nextHeight) return true;
|
|
4385
|
+
const nextTexture = gl.createTexture();
|
|
4386
|
+
const nextFramebuffer = gl.createFramebuffer();
|
|
4387
|
+
if (!nextTexture || !nextFramebuffer) {
|
|
4388
|
+
if (nextTexture) gl.deleteTexture(nextTexture);
|
|
4389
|
+
if (nextFramebuffer) gl.deleteFramebuffer(nextFramebuffer);
|
|
4390
|
+
return false;
|
|
4391
|
+
}
|
|
4392
|
+
gl.bindTexture(gl.TEXTURE_2D, nextTexture);
|
|
4393
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
4394
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
4395
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
4396
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
4397
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, nextWidth, nextHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
4398
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, nextFramebuffer);
|
|
4399
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, nextTexture, 0);
|
|
4400
|
+
const complete = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
|
|
4401
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
4402
|
+
if (!complete) {
|
|
4403
|
+
gl.deleteFramebuffer(nextFramebuffer);
|
|
4404
|
+
gl.deleteTexture(nextTexture);
|
|
4405
|
+
return false;
|
|
4406
|
+
}
|
|
4407
|
+
discard(true);
|
|
4408
|
+
texture = nextTexture;
|
|
4409
|
+
framebuffer = nextFramebuffer;
|
|
4410
|
+
width = nextWidth;
|
|
4411
|
+
height = nextHeight;
|
|
4412
|
+
contentValid = false;
|
|
4413
|
+
return true;
|
|
4414
|
+
}
|
|
4415
|
+
return {
|
|
4416
|
+
gl,
|
|
4417
|
+
get width() {
|
|
4418
|
+
return width;
|
|
4419
|
+
},
|
|
4420
|
+
get height() {
|
|
4421
|
+
return height;
|
|
4422
|
+
},
|
|
4423
|
+
get allocated() {
|
|
4424
|
+
return texture !== null && framebuffer !== null;
|
|
4425
|
+
},
|
|
4426
|
+
get contentValid() {
|
|
4427
|
+
return contentValid;
|
|
4428
|
+
},
|
|
4429
|
+
resize,
|
|
4430
|
+
replay(executor, list, projection, options) {
|
|
4431
|
+
if (!texture || !framebuffer) return false;
|
|
4432
|
+
const partial = options?.damage !== void 0 || options?.mask !== void 0;
|
|
4433
|
+
if (partial && (!options?.damage || !options.mask || !isValidDamage(options.damage, width, height) || !isPartialReplayMask(options.mask, list))) return false;
|
|
4434
|
+
if (partial && !contentValid) return false;
|
|
4435
|
+
if (projection.framebufferWidth !== width || projection.framebufferHeight !== height) return false;
|
|
4436
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
4437
|
+
const execute = {
|
|
4438
|
+
...options?.execute,
|
|
4439
|
+
clear: true,
|
|
4440
|
+
damage: options?.damage,
|
|
4441
|
+
commandMask: options?.mask
|
|
4442
|
+
};
|
|
4443
|
+
try {
|
|
4444
|
+
const replayed = executor.execute(list, projection, execute);
|
|
4445
|
+
if (!replayed) {
|
|
4446
|
+
contentValid = false;
|
|
4447
|
+
return false;
|
|
4448
|
+
}
|
|
4449
|
+
if (!partial) contentValid = true;
|
|
4450
|
+
return replayed;
|
|
4451
|
+
} catch (error) {
|
|
4452
|
+
contentValid = false;
|
|
4453
|
+
throw error;
|
|
4454
|
+
} finally {
|
|
4455
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
4456
|
+
}
|
|
4457
|
+
},
|
|
4458
|
+
replayRegions(executor, list, projection, regions, options) {
|
|
4459
|
+
if (!texture || !framebuffer || !contentValid || regions.length === 0 || projection.framebufferWidth !== width || projection.framebufferHeight !== height) {
|
|
4460
|
+
contentValid = false;
|
|
4461
|
+
return false;
|
|
4462
|
+
}
|
|
4463
|
+
for (const region of regions) if (!isValidDamage(region.damage, width, height) || !isPartialReplayMask(region.mask, list)) {
|
|
4464
|
+
contentValid = false;
|
|
4465
|
+
return false;
|
|
4466
|
+
}
|
|
4467
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
4468
|
+
try {
|
|
4469
|
+
for (const region of regions) if (!executor.execute(list, projection, {
|
|
4470
|
+
...options?.execute,
|
|
4471
|
+
clear: true,
|
|
4472
|
+
damage: region.damage,
|
|
4473
|
+
commandMask: region.mask
|
|
4474
|
+
})) {
|
|
4475
|
+
contentValid = false;
|
|
4476
|
+
return false;
|
|
4477
|
+
}
|
|
4478
|
+
return true;
|
|
4479
|
+
} catch (error) {
|
|
4480
|
+
contentValid = false;
|
|
4481
|
+
throw error;
|
|
4482
|
+
} finally {
|
|
4483
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
4484
|
+
}
|
|
4485
|
+
},
|
|
4486
|
+
present() {
|
|
4487
|
+
if (!texture || !framebuffer || !contentValid) return false;
|
|
4488
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
4489
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, framebuffer);
|
|
4490
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
|
|
4491
|
+
gl.blitFramebuffer(0, 0, width, height, 0, 0, width, height, gl.COLOR_BUFFER_BIT, gl.NEAREST);
|
|
4492
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
|
|
4493
|
+
return true;
|
|
4494
|
+
},
|
|
4495
|
+
invalidateContent() {
|
|
4496
|
+
contentValid = false;
|
|
4497
|
+
},
|
|
4498
|
+
invalidate() {
|
|
4499
|
+
discard(false);
|
|
4500
|
+
},
|
|
4501
|
+
dispose() {
|
|
4502
|
+
discard(true);
|
|
4503
|
+
}
|
|
4504
|
+
};
|
|
4505
|
+
}
|
|
4506
|
+
//#endregion
|
|
4507
|
+
//#region src/textures.ts
|
|
4508
|
+
function textureBytes(width, height, mipmap) {
|
|
4509
|
+
if (width <= 0 || height <= 0) return 0;
|
|
4510
|
+
let bytes = 0;
|
|
4511
|
+
while (true) {
|
|
4512
|
+
bytes += width * height * 4;
|
|
4513
|
+
if (!mipmap || width === 1 && height === 1) return bytes;
|
|
4514
|
+
width = Math.max(1, width >> 1);
|
|
4515
|
+
height = Math.max(1, height >> 1);
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
/** Exact `round(channel * alpha / 255)` for all byte pairs. */
|
|
4519
|
+
function premultiplyByte(channel, alpha) {
|
|
4520
|
+
const t = channel * alpha + 128;
|
|
4521
|
+
return t + (t >> 8) >> 8;
|
|
4522
|
+
}
|
|
4523
|
+
/** Pixel dimensions of an upload source, as the source itself reports them —
|
|
4524
|
+
* 0 included. */
|
|
4525
|
+
function rawSourceSize(source) {
|
|
4526
|
+
const any = source;
|
|
4527
|
+
return {
|
|
4528
|
+
width: any.naturalWidth || any.videoWidth || any.width || 0,
|
|
4529
|
+
height: any.naturalHeight || any.videoHeight || any.height || 0
|
|
4530
|
+
};
|
|
4531
|
+
}
|
|
4532
|
+
/** The same, clamped to at least 1x1 — what an entry records and what UV
|
|
4533
|
+
* normalization divides by, neither of which may be zero. */
|
|
4534
|
+
function sourceSize(source) {
|
|
4535
|
+
const raw = rawSourceSize(source);
|
|
4536
|
+
return {
|
|
4537
|
+
width: Math.max(1, raw.width),
|
|
4538
|
+
height: Math.max(1, raw.height)
|
|
4539
|
+
};
|
|
4540
|
+
}
|
|
4541
|
+
function createTextureCache(gl) {
|
|
4542
|
+
const entries = /* @__PURE__ */ new Map();
|
|
4543
|
+
let whiteEntry = null;
|
|
4544
|
+
let premultiplyScratch = new Uint8Array(0);
|
|
4545
|
+
const stats = {
|
|
4546
|
+
entries: 0,
|
|
4547
|
+
uploads: 0,
|
|
4548
|
+
respecs: 0,
|
|
4549
|
+
evictions: 0,
|
|
4550
|
+
bytes: 0
|
|
4551
|
+
};
|
|
4552
|
+
function resolvedOptions(options = {}) {
|
|
4553
|
+
const mipmap = options.mipmap ?? false;
|
|
4554
|
+
return {
|
|
4555
|
+
premultiplied: options.premultiplied ?? false,
|
|
4556
|
+
mipmap,
|
|
4557
|
+
minFilter: options.minFilter ?? (mipmap ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR),
|
|
4558
|
+
magFilter: options.magFilter ?? gl.LINEAR
|
|
4559
|
+
};
|
|
4560
|
+
}
|
|
4561
|
+
function configure(entry) {
|
|
4562
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
4563
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
4564
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, entry.minFilter);
|
|
4565
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, entry.magFilter);
|
|
4566
|
+
}
|
|
4567
|
+
function unpackForSource(premultiplied) {
|
|
4568
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);
|
|
4569
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
4570
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, !premultiplied);
|
|
4571
|
+
}
|
|
4572
|
+
function regenerateMipmap(entry) {
|
|
4573
|
+
if (entry.mipmap && entry.storageW > 0 && entry.storageH > 0) gl.generateMipmap(gl.TEXTURE_2D);
|
|
4574
|
+
}
|
|
4575
|
+
function uploadSource(entry, source) {
|
|
4576
|
+
const raw = rawSourceSize(source);
|
|
4577
|
+
gl.bindTexture(gl.TEXTURE_2D, entry.texture);
|
|
4578
|
+
unpackForSource(entry.premultiplied);
|
|
4579
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
|
4580
|
+
configure(entry);
|
|
4581
|
+
entry.storageW = raw.width;
|
|
4582
|
+
entry.storageH = raw.height;
|
|
4583
|
+
regenerateMipmap(entry);
|
|
4584
|
+
stats.uploads += 1;
|
|
4585
|
+
stats.respecs += 1;
|
|
4586
|
+
}
|
|
4587
|
+
/** Re-upload over storage that is ALREADY the source's size. No `texImage2D`,
|
|
4588
|
+
* so no reallocation; no `configure()` either, because sampler parameters live
|
|
4589
|
+
* on the texture object and nothing here touches them. */
|
|
4590
|
+
function reuploadSource(entry, source) {
|
|
4591
|
+
gl.bindTexture(gl.TEXTURE_2D, entry.texture);
|
|
4592
|
+
unpackForSource(entry.premultiplied);
|
|
4593
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
|
4594
|
+
regenerateMipmap(entry);
|
|
4595
|
+
stats.uploads += 1;
|
|
4596
|
+
}
|
|
4597
|
+
/** Write a source into part of storage that is already there. Like
|
|
4598
|
+
* {@link reuploadSource} it neither allocates nor re-configures; unlike it, the
|
|
4599
|
+
* destination offset is the caller's. */
|
|
4600
|
+
function uploadRegion(entry, source, x, y) {
|
|
4601
|
+
gl.bindTexture(gl.TEXTURE_2D, entry.texture);
|
|
4602
|
+
unpackForSource(entry.premultiplied);
|
|
4603
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
|
4604
|
+
regenerateMipmap(entry);
|
|
4605
|
+
stats.uploads += 1;
|
|
4606
|
+
}
|
|
4607
|
+
function uploadBytes(entry, pixels, width, height, premultiplied) {
|
|
4608
|
+
let data = pixels instanceof Uint8Array ? pixels : new Uint8Array(pixels.buffer, pixels.byteOffset, pixels.byteLength);
|
|
4609
|
+
if (!premultiplied) {
|
|
4610
|
+
if (premultiplyScratch.length < data.length) premultiplyScratch = new Uint8Array(data.length);
|
|
4611
|
+
const premultipliedData = premultiplyScratch.subarray(0, data.length);
|
|
4612
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
4613
|
+
const a = data[i + 3];
|
|
4614
|
+
premultipliedData[i] = premultiplyByte(data[i], a);
|
|
4615
|
+
premultipliedData[i + 1] = premultiplyByte(data[i + 1], a);
|
|
4616
|
+
premultipliedData[i + 2] = premultiplyByte(data[i + 2], a);
|
|
4617
|
+
premultipliedData[i + 3] = a;
|
|
4618
|
+
}
|
|
4619
|
+
data = premultipliedData;
|
|
4620
|
+
}
|
|
4621
|
+
gl.bindTexture(gl.TEXTURE_2D, entry.texture);
|
|
4622
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
4623
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
4624
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
4625
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
|
4626
|
+
configure(entry);
|
|
4627
|
+
entry.storageW = width;
|
|
4628
|
+
entry.storageH = height;
|
|
4629
|
+
regenerateMipmap(entry);
|
|
4630
|
+
stats.uploads += 1;
|
|
4631
|
+
stats.respecs += 1;
|
|
4632
|
+
}
|
|
4633
|
+
function track(entry, width, height) {
|
|
4634
|
+
stats.bytes += textureBytes(width, height, entry.mipmap) - textureBytes(entry.width, entry.height, entry.mipmap);
|
|
4635
|
+
entry.width = width;
|
|
4636
|
+
entry.height = height;
|
|
4637
|
+
}
|
|
4638
|
+
function makeEntry(width, height, options = {}) {
|
|
4639
|
+
const resolved = resolvedOptions(options);
|
|
4640
|
+
const entry = {
|
|
4641
|
+
texture: gl.createTexture(),
|
|
4642
|
+
width: 0,
|
|
4643
|
+
height: 0,
|
|
4644
|
+
refs: 0,
|
|
4645
|
+
storageW: -1,
|
|
4646
|
+
storageH: -1,
|
|
4647
|
+
mipmap: resolved.mipmap,
|
|
4648
|
+
minFilter: resolved.minFilter,
|
|
4649
|
+
magFilter: resolved.magFilter,
|
|
4650
|
+
premultiplied: resolved.premultiplied
|
|
4651
|
+
};
|
|
4652
|
+
track(entry, width, height);
|
|
4653
|
+
stats.entries += 1;
|
|
4654
|
+
return entry;
|
|
4655
|
+
}
|
|
4656
|
+
return {
|
|
4657
|
+
stats,
|
|
4658
|
+
white() {
|
|
4659
|
+
if (whiteEntry) return whiteEntry;
|
|
4660
|
+
const entry = makeEntry(1, 1);
|
|
4661
|
+
uploadBytes(entry, new Uint8Array([
|
|
4662
|
+
255,
|
|
4663
|
+
255,
|
|
4664
|
+
255,
|
|
4665
|
+
255
|
|
4666
|
+
]), 1, 1, true);
|
|
4667
|
+
entry.refs = 1;
|
|
4668
|
+
whiteEntry = entry;
|
|
4669
|
+
return entry;
|
|
4670
|
+
},
|
|
4671
|
+
peek(key) {
|
|
4672
|
+
return entries.get(key);
|
|
4673
|
+
},
|
|
4674
|
+
acquire(key, source, options) {
|
|
4675
|
+
const existing = entries.get(key);
|
|
4676
|
+
if (existing) {
|
|
4677
|
+
existing.refs += 1;
|
|
4678
|
+
return existing;
|
|
4679
|
+
}
|
|
4680
|
+
const { width, height } = sourceSize(source);
|
|
4681
|
+
const entry = makeEntry(width, height, options);
|
|
4682
|
+
uploadSource(entry, source);
|
|
4683
|
+
entry.refs = 1;
|
|
4684
|
+
entries.set(key, entry);
|
|
4685
|
+
return entry;
|
|
4686
|
+
},
|
|
4687
|
+
acquireBytes(key, pixels, width, height, options) {
|
|
4688
|
+
const existing = entries.get(key);
|
|
4689
|
+
if (existing) {
|
|
4690
|
+
existing.refs += 1;
|
|
4691
|
+
return existing;
|
|
4692
|
+
}
|
|
4693
|
+
const entry = makeEntry(Math.max(1, width), Math.max(1, height), options);
|
|
4694
|
+
uploadBytes(entry, pixels, entry.width, entry.height, options?.premultiplied ?? false);
|
|
4695
|
+
entry.refs = 1;
|
|
4696
|
+
entries.set(key, entry);
|
|
4697
|
+
return entry;
|
|
4698
|
+
},
|
|
4699
|
+
retain(key) {
|
|
4700
|
+
const entry = entries.get(key);
|
|
4701
|
+
if (!entry) throw new Error(`texture cache: retain of unknown key "${key}"`);
|
|
4702
|
+
entry.refs += 1;
|
|
4703
|
+
return entry;
|
|
4704
|
+
},
|
|
4705
|
+
release(key) {
|
|
4706
|
+
const entry = entries.get(key);
|
|
4707
|
+
if (!entry) return;
|
|
4708
|
+
entry.refs -= 1;
|
|
4709
|
+
if (entry.refs > 0) return;
|
|
4710
|
+
gl.deleteTexture(entry.texture);
|
|
4711
|
+
entries.delete(key);
|
|
4712
|
+
stats.entries -= 1;
|
|
4713
|
+
stats.evictions += 1;
|
|
4714
|
+
stats.bytes -= textureBytes(entry.width, entry.height, entry.mipmap);
|
|
4715
|
+
},
|
|
4716
|
+
update(key, source) {
|
|
4717
|
+
const raw = rawSourceSize(source);
|
|
4718
|
+
const entry = entries.get(key);
|
|
4719
|
+
if (entry && entry.storageW === raw.width && entry.storageH === raw.height) {
|
|
4720
|
+
reuploadSource(entry, source);
|
|
4721
|
+
return entry;
|
|
4722
|
+
}
|
|
4723
|
+
const width = Math.max(1, raw.width);
|
|
4724
|
+
const height = Math.max(1, raw.height);
|
|
4725
|
+
if (!entry) {
|
|
4726
|
+
const created = makeEntry(width, height);
|
|
4727
|
+
created.refs = 1;
|
|
4728
|
+
entries.set(key, created);
|
|
4729
|
+
uploadSource(created, source);
|
|
4730
|
+
return created;
|
|
4731
|
+
}
|
|
4732
|
+
track(entry, width, height);
|
|
4733
|
+
uploadSource(entry, source);
|
|
4734
|
+
return entry;
|
|
4735
|
+
},
|
|
4736
|
+
updateRegion(key, source, x, y) {
|
|
4737
|
+
const entry = entries.get(key);
|
|
4738
|
+
if (!entry || entry.mipmap) return null;
|
|
4739
|
+
const raw = rawSourceSize(source);
|
|
4740
|
+
if (!Number.isInteger(x) || !Number.isInteger(y) || x < 0 || y < 0 || raw.width <= 0 || raw.height <= 0 || x + raw.width > entry.storageW || y + raw.height > entry.storageH) return null;
|
|
4741
|
+
uploadRegion(entry, source, x, y);
|
|
4742
|
+
return entry;
|
|
4743
|
+
},
|
|
4744
|
+
reset() {
|
|
4745
|
+
entries.clear();
|
|
4746
|
+
whiteEntry = null;
|
|
4747
|
+
stats.entries = 0;
|
|
4748
|
+
stats.bytes = 0;
|
|
4749
|
+
},
|
|
4750
|
+
dispose() {
|
|
4751
|
+
for (const entry of entries.values()) gl.deleteTexture(entry.texture);
|
|
4752
|
+
if (whiteEntry) gl.deleteTexture(whiteEntry.texture);
|
|
4753
|
+
entries.clear();
|
|
4754
|
+
whiteEntry = null;
|
|
4755
|
+
stats.entries = 0;
|
|
4756
|
+
stats.bytes = 0;
|
|
4757
|
+
}
|
|
4758
|
+
};
|
|
4759
|
+
}
|
|
4760
|
+
//#endregion
|
|
4761
|
+
export { BLEND_ADD, BLEND_MIX, BLEND_MUL, BLEND_SUB, COLOR_MATRIX_FLOATS, 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, FLIP_H, FLIP_V, IDENTITY_COLOR_MATRIX, INSTANCE_COLOR_OFFSET, INSTANCE_CORNERS_OFFSET, INSTANCE_FLOATS, INSTANCE_SLOTS_OFFSET, INSTANCE_UV_OFFSET, MAX_TEXTURE_SLOTS, POLYLINE_QUAD_FLOATS, RETAINED_DAMAGE_TILE_SIZE, RETAINED_MAX_DAMAGE_COVERAGE, RETAINED_MAX_REPLAY_FRACTION, STAGE_CONTEXT_ATTRIBUTES, 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 };
|
|
4762
|
+
|
|
4763
|
+
//# sourceMappingURL=index.js.map
|