@godot-scene-web/canvas-effects 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/dist/webgl.mjs ADDED
@@ -0,0 +1,2298 @@
1
+ import { INSTANCE_STRIDE, InstanceBuffer, createParticleState, normalizeParticleRenderConfig, packParticleInstances, preprocessParticles, simulateParticles } from "@godot-scene-web/effects/particles";
2
+ import { UnsupportedShaderError, transpileGodotShader } from "@godot-scene-web/effects/shaders";
3
+ //#region src/particle-webgl.ts
4
+ function compileProgram$2(gl, vertexSource, fragmentSource) {
5
+ const vertex = gl.createShader(gl.VERTEX_SHADER);
6
+ const fragment = gl.createShader(gl.FRAGMENT_SHADER);
7
+ if (!vertex || !fragment) {
8
+ if (vertex) gl.deleteShader(vertex);
9
+ if (fragment) gl.deleteShader(fragment);
10
+ return null;
11
+ }
12
+ gl.shaderSource(vertex, vertexSource);
13
+ gl.shaderSource(fragment, fragmentSource);
14
+ gl.compileShader(vertex);
15
+ gl.compileShader(fragment);
16
+ if (!gl.getShaderParameter(vertex, gl.COMPILE_STATUS) || !gl.getShaderParameter(fragment, gl.COMPILE_STATUS)) {
17
+ gl.deleteShader(vertex);
18
+ gl.deleteShader(fragment);
19
+ return null;
20
+ }
21
+ const program = gl.createProgram();
22
+ if (!program) {
23
+ gl.deleteShader(vertex);
24
+ gl.deleteShader(fragment);
25
+ return null;
26
+ }
27
+ gl.attachShader(program, vertex);
28
+ gl.attachShader(program, fragment);
29
+ gl.linkProgram(program);
30
+ gl.deleteShader(vertex);
31
+ gl.deleteShader(fragment);
32
+ if (gl.getProgramParameter(program, gl.LINK_STATUS)) return program;
33
+ gl.deleteProgram(program);
34
+ return null;
35
+ }
36
+ /** GPU residency is deliberately external to the portable instance data. */
37
+ const instanceAllocations = /* @__PURE__ */ new WeakMap();
38
+ function allocationsFor(gl) {
39
+ let allocations = instanceAllocations.get(gl);
40
+ if (!allocations) {
41
+ allocations = /* @__PURE__ */ new WeakMap();
42
+ instanceAllocations.set(gl, allocations);
43
+ }
44
+ return allocations;
45
+ }
46
+ function uploadInstances(gl, instances) {
47
+ const allocations = allocationsFor(gl);
48
+ let allocation = allocations.get(instances);
49
+ if (!allocation) {
50
+ const buffer = gl.createBuffer();
51
+ if (!buffer) return null;
52
+ allocation = {
53
+ buffer,
54
+ bytes: 0
55
+ };
56
+ allocations.set(instances, allocation);
57
+ }
58
+ gl.bindBuffer(gl.ARRAY_BUFFER, allocation.buffer);
59
+ if (instances.data.byteLength > allocation.bytes) {
60
+ gl.bufferData(gl.ARRAY_BUFFER, instances.data, gl.DYNAMIC_DRAW);
61
+ allocation.bytes = instances.data.byteLength;
62
+ } else gl.bufferSubData(gl.ARRAY_BUFFER, 0, instances.data, 0, instances.count * INSTANCE_STRIDE);
63
+ return allocation.buffer;
64
+ }
65
+ /** Release one portable buffer's WebGL allocation on its owning borrowed context. */
66
+ function disposeParticleInstanceBuffer(gl, instances) {
67
+ const allocations = instanceAllocations.get(gl);
68
+ const allocation = allocations?.get(instances);
69
+ if (!allocation) return;
70
+ gl.deleteBuffer(allocation.buffer);
71
+ allocations?.delete(instances);
72
+ }
73
+ /** Context-loss path: forget driver names without issuing GL commands. */
74
+ function invalidateParticleInstanceBuffer(gl, instances) {
75
+ instanceAllocations.get(gl)?.delete(instances);
76
+ }
77
+ const VERTEX_SRC = `#version 300 es
78
+ layout(location = 0) in vec2 a_corner; // unit quad corner, [-0.5, 0.5]
79
+ layout(location = 1) in vec2 a_center; // particle center, device px
80
+ layout(location = 2) in vec2 a_scale; // sprite size, device px (base * scale)
81
+ layout(location = 3) in float a_rotation;
82
+ layout(location = 4) in vec4 a_color;
83
+ layout(location = 5) in float a_frame;
84
+ uniform vec2 u_viewport; // canvas size, device px
85
+ uniform float u_hframes;
86
+ uniform float u_vframes;
87
+ out vec2 v_uv;
88
+ out vec2 v_quad;
89
+ out vec2 v_cell;
90
+ out vec4 v_color;
91
+ void main() {
92
+ float c = cos(a_rotation);
93
+ float s = sin(a_rotation);
94
+ vec2 rotated = vec2(a_corner.x * c - a_corner.y * s, a_corner.x * s + a_corner.y * c);
95
+ vec2 px = a_center + rotated * a_scale;
96
+ vec2 clip = (px / u_viewport) * 2.0 - 1.0;
97
+ clip.y = -clip.y; // canvas Y-down -> clip Y-up
98
+ gl_Position = vec4(clip, 0.0, 1.0);
99
+ vec2 uv01 = a_corner + 0.5; // 0..1 across the SPRITE quad
100
+ vec2 cell = vec2(mod(a_frame, u_hframes), floor(a_frame / u_hframes));
101
+ v_uv = (cell + uv01) / vec2(u_hframes, u_vframes);
102
+ // The flipbook cell INDEX, so a fragment that re-derives its own sprite-local UV (the polar remap) can map
103
+ // it back into the same cell instead of over the whole sheet.
104
+ v_cell = cell;
105
+ // Quad-local 0..1, INDEPENDENT of the flipbook grid: the untextured dot is drawn from this.
106
+ // Deriving it from the atlas-mapped v_uv put the dot's center at the SHEET's center, so any
107
+ // grid > 1x1 left the fallback dot off-center and clipped to a sliver of one cell.
108
+ v_quad = uv01;
109
+ v_color = a_color;
110
+ }`;
111
+ const FRAGMENT_SRC = `#version 300 es
112
+ precision mediump float;
113
+ uniform sampler2D u_texture;
114
+ uniform sampler2D u_lutTex; // per-TEXEL color LUT, 1px tall (see u_lut)
115
+ uniform sampler2D u_maskTex; // quad-shaped coverage mask, unit 2 (see u_mask)
116
+ // The flipbook grid, ALSO declared in the vertex stage: a uniform shared across stages must match in type AND
117
+ // PRECISION, and the vertex stage's float default is highp while this one's is mediump — declaring these as a
118
+ // plain float here fails to LINK (silently: no particle program, so no particle canvases at all).
119
+ uniform highp float u_hframes;
120
+ uniform highp float u_vframes;
121
+ uniform int u_textured;
122
+ uniform int u_lut; // 1 = recolor through the LUT, indexed by the source RED channel
123
+ uniform int u_additive; // 1 = additive: emit the particle's LIGHT (Godot ADD: src.rgb * src.a)
124
+ uniform int u_alphaFromRed; // 1 = coverage comes from the source RED channel, not its alpha
125
+ uniform int u_erode; // 1 = apply the constant-erosion smoothstep below
126
+ uniform vec2 u_erodeFactors; // (threshold, softness) for that smoothstep
127
+ uniform int u_mask; // 1 = multiply coverage by u_maskTex's red, sampled over the QUAD
128
+ uniform int u_uvPolar; // 1 = sample the sheet through Godot's polar_coordinates remap
129
+ in vec2 v_uv;
130
+ in vec2 v_quad;
131
+ in vec2 v_cell;
132
+ in vec4 v_color;
133
+ out vec4 fragColor;
134
+ void main() {
135
+ vec2 uv = v_uv;
136
+ if (u_uvPolar == 1) {
137
+ // Godot polar_coordinates(UV, vec2(0.5), 1, 1) (shaders/vfx/_util/polar_coordinates.gdshaderinc):
138
+ // x = radius from the sprite center (0..1.41 at the corners), y = the angle mapped to 0..1, both wrapped.
139
+ // A radial sheet (common_ring_polar_a) is a RING only through this; sampled flat it is a vertical BAR.
140
+ vec2 dir = v_quad - 0.5;
141
+ float radius = length(dir) * 2.0;
142
+ float angle = atan(dir.y, dir.x) * (1.0 / (3.1416 * 2.0));
143
+ uv = (v_cell + mod(vec2(radius, angle), 1.0)) / vec2(u_hframes, u_vframes);
144
+ }
145
+ vec4 tex;
146
+ if (u_textured == 1) {
147
+ tex = texture(u_texture, uv);
148
+ } else {
149
+ // Soft round dot when the system has no texture — measured across the QUAD, not the
150
+ // atlas-mapped UV, so it stays centered whatever the (meaningless, textureless) grid is.
151
+ float r = length(v_quad - 0.5) * 2.0;
152
+ tex = vec4(1.0, 1.0, 1.0, 1.0 - smoothstep(0.7, 1.0, r));
153
+ }
154
+ // COVERAGE — taken PRE-LUT, because the LUT is a color lookup INDEXED by that same red channel: reading it
155
+ // afterwards would sample the LUT's own (usually white) output instead of the sheet's shape. STS2's
156
+ // grayscale VFX sheets are alpha-less PNGs, so tex.a is 1.0 everywhere and the alpha branch draws a SQUARE.
157
+ float coverage = u_alphaFromRed == 1 ? tex.r : tex.a;
158
+ if (u_lut == 1) {
159
+ // Godot's VFX particle-shader family: COLOR = vec4(texture(lut, texture_color.rr).rgb,
160
+ // alpha) * vertex_color. The sprite sheet is a single-channel MASK, so its own RGB is
161
+ // meaningless (it reads as a red/orange block); the LUT holds the real colors. Sampled
162
+ // AFTER the texture/dot resolve so both branches are recolored, and the source ALPHA is
163
+ // preserved untouched — only RGB comes from the LUT.
164
+ tex = vec4(texture(u_lutTex, vec2(tex.r, 0.5)).rgb, tex.a);
165
+ }
166
+ if (u_erode == 1) {
167
+ // Godot erosion_from_factors(vec2(threshold, softness), coverage) — a CONSTANT erosion curve, i.e. the
168
+ // threshold does not sweep over the particle's life (see ParticleSpecConfig.alphaErode).
169
+ coverage = smoothstep(u_erodeFactors.x, u_erodeFactors.x + u_erodeFactors.y, coverage);
170
+ }
171
+ if (u_mask == 1) {
172
+ // Godot's mask sampler reads the sprite's own UV, NOT the flipbook cell — it shapes the whole quad.
173
+ coverage *= texture(u_maskTex, v_quad).r;
174
+ }
175
+ tex.a = coverage;
176
+ vec4 col = tex * v_color;
177
+ if (u_additive == 1) {
178
+ // Additive sprites contribute light = color x alpha (Godot BLEND_MODE_ADD adds
179
+ // src.rgb * src.a to the framebuffer, nothing where light is 0 -- so an opaque-black
180
+ // glow background or an alpha-shaped sprite's transparent area both add nothing).
181
+ // Emit that light RAW and SUM it across particles (blendFunc ONE, ONE into the
182
+ // accumulator FBO); the resolve pass then derives the per-pixel TOTAL's peak-channel
183
+ // coverage ONCE and presents the pair premultiplied. Normalizing per PARTICLE here
184
+ // (the old path) amplified every faint texel to full brightness and let overlaps
185
+ // clamp to white while stacking coverage -- a subtle 5-particle fog rendered as an
186
+ // opaque white haze wall.
187
+ fragColor = vec4(col.rgb * tex.a * v_color.a, 0.0);
188
+ } else {
189
+ // PREMULTIPLIED, under blendFactorsFor(0)'s ONE / ONE_MINUS_SRC_ALPHA -- the shared canvas
190
+ // declares premultipliedAlpha: true (../webgl/shared-gl.ts) and this is that contract.
191
+ // Algebraically identical to emitting straight col under SRC_ALPHA / ONE_MINUS_SRC_ALPHA
192
+ // (both land col.rgb*col.a + dst*(1-col.a)), and written THIS way so the fragment and the
193
+ // canvas state the same thing -- the same pairing render-webgpu.ts's fs_particles uses, since
194
+ // a GPUCanvasContext has no straight-alpha mode to differ with.
195
+ fragColor = vec4(col.rgb * col.a, col.a);
196
+ }
197
+ }`;
198
+ const RESOLVE_VERTEX_SRC = `#version 300 es
199
+ layout(location = 0) in vec2 a_pos; // full-screen clip-space quad (shared-gl quad buffer)
200
+ void main() {
201
+ gl_Position = vec4(a_pos, 0.0, 1.0);
202
+ }`;
203
+ const RESOLVE_FRAGMENT_SRC = `#version 300 es
204
+ precision mediump float;
205
+ uniform sampler2D u_accum;
206
+ out vec4 fragColor;
207
+ void main() {
208
+ // Same viewport rect + origin as the accumulate pass, so fragment coords match texels.
209
+ vec3 light = texelFetch(u_accum, ivec2(gl_FragCoord.xy), 0).rgb;
210
+ float cov = max(max(light.r, light.g), light.b);
211
+ fragColor = vec4(light, cov);
212
+ }`;
213
+ const cached = /* @__PURE__ */ new WeakMap();
214
+ function getParticleProgram(gl) {
215
+ const existing = cached.get(gl);
216
+ if (existing) {
217
+ existing.pinned = true;
218
+ return existing.program;
219
+ }
220
+ const program = compileProgram$2(gl, VERTEX_SRC, FRAGMENT_SRC);
221
+ const resolveProgram = compileProgram$2(gl, RESOLVE_VERTEX_SRC, RESOLVE_FRAGMENT_SRC);
222
+ if (!program || !resolveProgram) {
223
+ if (program) gl.deleteProgram(program);
224
+ if (resolveProgram) gl.deleteProgram(resolveProgram);
225
+ cached.set(gl, {
226
+ program: null,
227
+ refs: 0,
228
+ pinned: true
229
+ });
230
+ return null;
231
+ }
232
+ const cornerBuffer = gl.createBuffer();
233
+ if (!cornerBuffer) {
234
+ gl.deleteProgram(program);
235
+ gl.deleteProgram(resolveProgram);
236
+ cached.set(gl, {
237
+ program: null,
238
+ refs: 0,
239
+ pinned: true
240
+ });
241
+ return null;
242
+ }
243
+ gl.bindBuffer(gl.ARRAY_BUFFER, cornerBuffer);
244
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
245
+ -.5,
246
+ -.5,
247
+ .5,
248
+ -.5,
249
+ -.5,
250
+ .5,
251
+ .5,
252
+ .5
253
+ ]), gl.STATIC_DRAW);
254
+ const result = {
255
+ program,
256
+ cornerBuffer,
257
+ uViewport: gl.getUniformLocation(program, "u_viewport"),
258
+ uHframes: gl.getUniformLocation(program, "u_hframes"),
259
+ uVframes: gl.getUniformLocation(program, "u_vframes"),
260
+ uTexture: gl.getUniformLocation(program, "u_texture"),
261
+ uLutTex: gl.getUniformLocation(program, "u_lutTex"),
262
+ uMaskTex: gl.getUniformLocation(program, "u_maskTex"),
263
+ uTextured: gl.getUniformLocation(program, "u_textured"),
264
+ uLut: gl.getUniformLocation(program, "u_lut"),
265
+ uAdditive: gl.getUniformLocation(program, "u_additive"),
266
+ uAlphaFromRed: gl.getUniformLocation(program, "u_alphaFromRed"),
267
+ uErode: gl.getUniformLocation(program, "u_erode"),
268
+ uErodeFactors: gl.getUniformLocation(program, "u_erodeFactors"),
269
+ uMask: gl.getUniformLocation(program, "u_mask"),
270
+ uUvPolar: gl.getUniformLocation(program, "u_uvPolar"),
271
+ resolveProgram,
272
+ uAccum: gl.getUniformLocation(resolveProgram, "u_accum"),
273
+ accum: null
274
+ };
275
+ cached.set(gl, {
276
+ program: result,
277
+ refs: 0,
278
+ pinned: true
279
+ });
280
+ return result;
281
+ }
282
+ /** Acquire a shared program lease for a producer that will later release it. */
283
+ function acquireParticleProgram(gl) {
284
+ const entry = cached.get(gl);
285
+ if (entry) {
286
+ if (!entry.program) return null;
287
+ entry.refs += 1;
288
+ return entry.program;
289
+ }
290
+ const program = getParticleProgram(gl);
291
+ const created = cached.get(gl);
292
+ created.pinned = false;
293
+ created.refs = 1;
294
+ return program;
295
+ }
296
+ function destroyCachedProgram(gl, program) {
297
+ gl.deleteProgram(program.program);
298
+ gl.deleteProgram(program.resolveProgram);
299
+ gl.deleteBuffer(program.cornerBuffer);
300
+ if (program.accum) {
301
+ gl.deleteFramebuffer(program.accum.fbo);
302
+ gl.deleteTexture(program.accum.texture);
303
+ }
304
+ }
305
+ /** Release a producer lease. Pinned HTML consumers keep their shared program alive. */
306
+ function releaseParticleProgram(gl, contextLost = false) {
307
+ const entry = cached.get(gl);
308
+ if (!entry) return;
309
+ entry.refs = Math.max(0, entry.refs - 1);
310
+ if (entry.refs !== 0 || entry.pinned) return;
311
+ cached.delete(gl);
312
+ if (entry.program && !contextLost) destroyCachedProgram(gl, entry.program);
313
+ }
314
+ function ensureAccumTarget(gl, program, w, h) {
315
+ let accum = program.accum;
316
+ if (!accum) {
317
+ const fbo = gl.createFramebuffer();
318
+ const texture = gl.createTexture();
319
+ if (!fbo || !texture) {
320
+ if (fbo) gl.deleteFramebuffer(fbo);
321
+ if (texture) gl.deleteTexture(texture);
322
+ return null;
323
+ }
324
+ accum = {
325
+ fbo,
326
+ texture,
327
+ width: 0,
328
+ height: 0
329
+ };
330
+ program.accum = accum;
331
+ }
332
+ if (accum.width < w || accum.height < h) {
333
+ const width = Math.max(accum.width, w);
334
+ const height = Math.max(accum.height, h);
335
+ gl.bindTexture(gl.TEXTURE_2D, accum.texture);
336
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
337
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
338
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
339
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
340
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
341
+ gl.bindFramebuffer(gl.FRAMEBUFFER, accum.fbo);
342
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, accum.texture, 0);
343
+ if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
344
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
345
+ gl.deleteFramebuffer(accum.fbo);
346
+ gl.deleteTexture(accum.texture);
347
+ program.accum = null;
348
+ return null;
349
+ }
350
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
351
+ accum.width = width;
352
+ accum.height = height;
353
+ }
354
+ return accum;
355
+ }
356
+ /** Allocate additive accumulation storage before a caller mutates simulation state. */
357
+ function prepareParticleDraw(gl, program, opts) {
358
+ if (opts.blendMode !== 1) return true;
359
+ return ensureAccumTarget(gl, program, opts.targetW ?? opts.viewportW, opts.targetH ?? opts.viewportH) !== null;
360
+ }
361
+ function blendFactorsFor(blendMode) {
362
+ if (blendMode === 1) return [
363
+ "ONE",
364
+ "ONE",
365
+ "ONE",
366
+ "ONE"
367
+ ];
368
+ return [
369
+ "ONE",
370
+ "ONE_MINUS_SRC_ALPHA",
371
+ "ONE",
372
+ "ONE_MINUS_SRC_ALPHA"
373
+ ];
374
+ }
375
+ const INSTANCE_ATTRS = [
376
+ {
377
+ loc: 1,
378
+ size: 2,
379
+ offset: 0
380
+ },
381
+ {
382
+ loc: 2,
383
+ size: 2,
384
+ offset: 2
385
+ },
386
+ {
387
+ loc: 3,
388
+ size: 1,
389
+ offset: 4
390
+ },
391
+ {
392
+ loc: 4,
393
+ size: 4,
394
+ offset: 5
395
+ },
396
+ {
397
+ loc: 5,
398
+ size: 1,
399
+ offset: 9
400
+ }
401
+ ];
402
+ function drawParticles(sharedGl, program, buffer, opts) {
403
+ const { gl } = sharedGl;
404
+ if (buffer.count <= 0) return true;
405
+ const instanceGpuBuffer = uploadInstances(gl, buffer);
406
+ if (!instanceGpuBuffer) return false;
407
+ const additive = opts.blendMode === 1;
408
+ const targetW = opts.targetW ?? opts.viewportW;
409
+ const targetH = opts.targetH ?? opts.viewportH;
410
+ if (additive) {
411
+ if (!prepareParticleDraw(gl, program, opts)) return false;
412
+ const accum = program.accum;
413
+ gl.bindFramebuffer(gl.FRAMEBUFFER, accum.fbo);
414
+ gl.viewport(0, 0, targetW, targetH);
415
+ gl.clearColor(0, 0, 0, 0);
416
+ gl.clear(gl.COLOR_BUFFER_BIT);
417
+ }
418
+ gl.useProgram(program.program);
419
+ gl.bindBuffer(gl.ARRAY_BUFFER, program.cornerBuffer);
420
+ gl.enableVertexAttribArray(0);
421
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
422
+ gl.vertexAttribDivisor(0, 0);
423
+ gl.bindBuffer(gl.ARRAY_BUFFER, instanceGpuBuffer);
424
+ const strideBytes = INSTANCE_STRIDE * 4;
425
+ for (let index = 0; index < INSTANCE_ATTRS.length; index += 1) {
426
+ const attr = INSTANCE_ATTRS[index];
427
+ gl.enableVertexAttribArray(attr.loc);
428
+ gl.vertexAttribPointer(attr.loc, attr.size, gl.FLOAT, false, strideBytes, attr.offset * 4);
429
+ gl.vertexAttribDivisor(attr.loc, 1);
430
+ }
431
+ if (program.uViewport) gl.uniform2f(program.uViewport, opts.viewportW, opts.viewportH);
432
+ const textured = Boolean(opts.textured && opts.texture);
433
+ const hframes = textured ? Math.max(1, opts.hframes) : 1;
434
+ const vframes = textured ? Math.max(1, opts.vframes) : 1;
435
+ if (program.uHframes) gl.uniform1f(program.uHframes, hframes);
436
+ if (program.uVframes) gl.uniform1f(program.uVframes, vframes);
437
+ if (program.uTextured) gl.uniform1i(program.uTextured, textured ? 1 : 0);
438
+ if (program.uAdditive) gl.uniform1i(program.uAdditive, additive ? 1 : 0);
439
+ if (program.uAlphaFromRed) gl.uniform1i(program.uAlphaFromRed, opts.alphaFromRed ? 1 : 0);
440
+ const erode = opts.erode ?? null;
441
+ if (program.uErode) gl.uniform1i(program.uErode, erode ? 1 : 0);
442
+ if (program.uErodeFactors) gl.uniform2f(program.uErodeFactors, erode ? erode.threshold : 0, erode ? erode.softness : 0);
443
+ if (program.uUvPolar) gl.uniform1i(program.uUvPolar, opts.uvPolar ? 1 : 0);
444
+ gl.activeTexture(gl.TEXTURE0);
445
+ if (opts.texture && program.uTexture) {
446
+ gl.bindTexture(gl.TEXTURE_2D, opts.texture);
447
+ gl.uniform1i(program.uTexture, 0);
448
+ } else gl.bindTexture(gl.TEXTURE_2D, null);
449
+ const lut = opts.lutTexture ?? null;
450
+ gl.activeTexture(gl.TEXTURE1);
451
+ gl.bindTexture(gl.TEXTURE_2D, lut);
452
+ if (program.uLutTex) gl.uniform1i(program.uLutTex, 1);
453
+ if (program.uLut) gl.uniform1i(program.uLut, lut ? 1 : 0);
454
+ const mask = opts.maskTexture ?? null;
455
+ gl.activeTexture(gl.TEXTURE2);
456
+ gl.bindTexture(gl.TEXTURE_2D, mask);
457
+ if (program.uMaskTex) gl.uniform1i(program.uMaskTex, 2);
458
+ if (program.uMask) gl.uniform1i(program.uMask, mask ? 1 : 0);
459
+ gl.activeTexture(gl.TEXTURE0);
460
+ gl.enable(gl.BLEND);
461
+ gl.blendEquation(gl.FUNC_ADD);
462
+ if (additive) gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
463
+ else gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
464
+ gl.disable(gl.DEPTH_TEST);
465
+ gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, buffer.count);
466
+ for (let index = 0; index < INSTANCE_ATTRS.length; index += 1) {
467
+ const attr = INSTANCE_ATTRS[index];
468
+ gl.vertexAttribDivisor(attr.loc, 0);
469
+ gl.disableVertexAttribArray(attr.loc);
470
+ }
471
+ if (additive && program.accum) {
472
+ gl.bindFramebuffer(gl.FRAMEBUFFER, opts.targetFramebuffer ?? null);
473
+ gl.viewport(0, 0, targetW, targetH);
474
+ gl.useProgram(program.resolveProgram);
475
+ gl.bindBuffer(gl.ARRAY_BUFFER, sharedGl.quad);
476
+ gl.enableVertexAttribArray(0);
477
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
478
+ gl.vertexAttribDivisor(0, 0);
479
+ gl.activeTexture(gl.TEXTURE0);
480
+ gl.bindTexture(gl.TEXTURE_2D, program.accum.texture);
481
+ if (program.uAccum) gl.uniform1i(program.uAccum, 0);
482
+ if (opts.additiveResolveIntoExisting) {
483
+ gl.enable(gl.BLEND);
484
+ gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
485
+ gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
486
+ } else gl.disable(gl.BLEND);
487
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
488
+ }
489
+ return true;
490
+ }
491
+ //#endregion
492
+ //#region src/shader-webgl.ts
493
+ /** Allocate the shared clip-space quad used by full-surface shader draws. */
494
+ function createWebglFullscreenQuad(gl) {
495
+ const quad = gl.createBuffer();
496
+ if (!quad) return null;
497
+ gl.bindBuffer(gl.ARRAY_BUFFER, quad);
498
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
499
+ -1,
500
+ -1,
501
+ 1,
502
+ -1,
503
+ -1,
504
+ 1,
505
+ 1,
506
+ 1
507
+ ]), gl.STATIC_DRAW);
508
+ return quad;
509
+ }
510
+ function createWebglTexture(gl) {
511
+ return gl.createTexture();
512
+ }
513
+ function setTextureSampling(gl, repeat, nearest = false) {
514
+ const wrap = repeat ? gl.REPEAT : gl.CLAMP_TO_EDGE;
515
+ const filter = nearest ? gl.NEAREST : gl.LINEAR;
516
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap);
517
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);
518
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
519
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
520
+ }
521
+ /** Upload bytes, an image, or a canvas with Godot's top-left UV convention. */
522
+ function uploadWebglTexture(gl, texture, source, width, height, opts = {}) {
523
+ gl.bindTexture(gl.TEXTURE_2D, texture);
524
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
525
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
526
+ if (source instanceof Uint8Array) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width ?? 1, height ?? 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, source);
527
+ else gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
528
+ setTextureSampling(gl, opts.repeat === true, opts.nearest === true);
529
+ }
530
+ function createWebglPlaceholderTexture(gl, repeat) {
531
+ const texture = createWebglTexture(gl);
532
+ if (texture) uploadWebglTexture(gl, texture, new Uint8Array([
533
+ 0,
534
+ 0,
535
+ 0,
536
+ 0
537
+ ]), 1, 1, { repeat });
538
+ return texture;
539
+ }
540
+ const COMPILE_POLL_MS = 1;
541
+ const COMPILE_MAX_WAIT_MS = 3e3;
542
+ const parallelCompileExts = /* @__PURE__ */ new WeakMap();
543
+ function parallelCompileExt(gl) {
544
+ const cached = parallelCompileExts.get(gl);
545
+ if (cached !== void 0) return cached;
546
+ let ext = null;
547
+ try {
548
+ const found = gl.getExtension("KHR_parallel_shader_compile");
549
+ ext = found && typeof found.COMPLETION_STATUS_KHR === "number" ? { COMPLETION_STATUS_KHR: found.COMPLETION_STATUS_KHR } : null;
550
+ } catch {
551
+ ext = null;
552
+ }
553
+ parallelCompileExts.set(gl, ext);
554
+ return ext;
555
+ }
556
+ const FAILED_PROGRAM = {
557
+ ready: () => true,
558
+ finish: () => null
559
+ };
560
+ /** Kick compilation and linking without synchronously asking the driver for status. */
561
+ function startProgram(gl, vertexSrc, fragmentSrc, beforeLink) {
562
+ const vs = startShader(gl, gl.VERTEX_SHADER, vertexSrc);
563
+ const fs = startShader(gl, gl.FRAGMENT_SHADER, fragmentSrc);
564
+ if (!vs || !fs) {
565
+ if (vs) gl.deleteShader(vs);
566
+ if (fs) gl.deleteShader(fs);
567
+ return FAILED_PROGRAM;
568
+ }
569
+ const program = gl.createProgram();
570
+ if (!program) {
571
+ gl.deleteShader(vs);
572
+ gl.deleteShader(fs);
573
+ return FAILED_PROGRAM;
574
+ }
575
+ gl.attachShader(program, vs);
576
+ gl.attachShader(program, fs);
577
+ beforeLink?.(program);
578
+ gl.linkProgram(program);
579
+ const ext = parallelCompileExt(gl);
580
+ let completed = false;
581
+ return {
582
+ ready() {
583
+ if (completed || !ext) return true;
584
+ completed = gl.getProgramParameter(program, ext.COMPLETION_STATUS_KHR) === true;
585
+ return completed;
586
+ },
587
+ finish() {
588
+ if (gl.getProgramParameter(program, gl.LINK_STATUS)) {
589
+ gl.deleteShader(vs);
590
+ gl.deleteShader(fs);
591
+ return program;
592
+ }
593
+ let compileFailed = false;
594
+ for (const shader of [vs, fs]) {
595
+ if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) continue;
596
+ compileFailed = true;
597
+ console.warn("[gsw webgl] shader compile failed:", gl.getShaderInfoLog(shader));
598
+ }
599
+ if (!compileFailed) console.warn("[gsw webgl] program link failed:", gl.getProgramInfoLog(program));
600
+ gl.deleteShader(vs);
601
+ gl.deleteShader(fs);
602
+ gl.deleteProgram(program);
603
+ return null;
604
+ }
605
+ };
606
+ }
607
+ /** Compile and link synchronously for callers that cannot yield. */
608
+ function compileProgram(gl, vertexSrc, fragmentSrc, beforeLink) {
609
+ return startProgram(gl, vertexSrc, fragmentSrc, beforeLink).finish();
610
+ }
611
+ const compileWaiters = /* @__PURE__ */ new Set();
612
+ let compilePollTimer = null;
613
+ function performanceNow() {
614
+ return typeof performance !== "undefined" && performance.now ? performance.now() : 0;
615
+ }
616
+ function pollCompiles() {
617
+ compilePollTimer = null;
618
+ const now = performanceNow();
619
+ for (const waiter of [...compileWaiters]) {
620
+ if (!waiter.pending.ready() && now < waiter.deadline) continue;
621
+ compileWaiters.delete(waiter);
622
+ waiter.wake();
623
+ }
624
+ if (compileWaiters.size > 0) scheduleCompilePoll();
625
+ }
626
+ function scheduleCompilePoll() {
627
+ if (compilePollTimer === null) compilePollTimer = setTimeout(pollCompiles, COMPILE_POLL_MS);
628
+ }
629
+ /** Finish when the parallel-compile extension says its status query will not block. */
630
+ async function compileProgramAsync(gl, vertexSrc, fragmentSrc, beforeLink) {
631
+ const pending = startProgram(gl, vertexSrc, fragmentSrc, beforeLink);
632
+ if (!pending.ready() && typeof setTimeout === "function") await new Promise((wake) => {
633
+ compileWaiters.add({
634
+ pending,
635
+ deadline: performanceNow() + COMPILE_MAX_WAIT_MS,
636
+ wake
637
+ });
638
+ scheduleCompilePoll();
639
+ });
640
+ return pending.finish();
641
+ }
642
+ /** Execute one Godot shader frame in a borrowed context; no DOM, scheduling, or capture work. */
643
+ function drawGodotWebglShaderFrame(gl, frame) {
644
+ const loc = (name) => frame.locations.get(name) ?? null;
645
+ const one = (name, value) => {
646
+ const l = loc(name);
647
+ if (l) gl.uniform1f(l, value);
648
+ };
649
+ const two = (name, value) => {
650
+ const l = loc(name);
651
+ if (l) gl.uniform2f(l, value[0] ?? 0, value[1] ?? 0);
652
+ };
653
+ const four = (name, value) => {
654
+ const l = loc(name);
655
+ if (l) gl.uniform4f(l, value[0] ?? 0, value[1] ?? 0, value[2] ?? 0, value[3] ?? 0);
656
+ };
657
+ gl.viewport(0, 0, frame.width, frame.height);
658
+ gl.disable(gl.SCISSOR_TEST);
659
+ gl.disable(gl.DEPTH_TEST);
660
+ gl.disable(gl.BLEND);
661
+ gl.useProgram(frame.program);
662
+ gl.bindBuffer(gl.ARRAY_BUFFER, frame.quad);
663
+ gl.enableVertexAttribArray(0);
664
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
665
+ gl.activeTexture(gl.TEXTURE0);
666
+ gl.bindTexture(gl.TEXTURE_2D, frame.texture);
667
+ {
668
+ const l = loc("TEXTURE");
669
+ if (l) gl.uniform1i(l, 0);
670
+ }
671
+ if (frame.time !== void 0) one("TIME", frame.time);
672
+ if (frame.texturePixelSize) two("TEXTURE_PIXEL_SIZE", frame.texturePixelSize);
673
+ four("MODULATE", frame.modulate);
674
+ two("_godot_uv_fit", frame.uvFit);
675
+ four("_godot_uv_window", frame.uvWindow);
676
+ if (frame.screenOrigin) two("_godot_screen_origin", frame.screenOrigin);
677
+ if (frame.screenSize) two("_godot_screen_size", frame.screenSize);
678
+ if (frame.screenTexture) {
679
+ const unit = frame.screenTextureUnit ?? 1;
680
+ gl.activeTexture(gl.TEXTURE0 + unit);
681
+ gl.bindTexture(gl.TEXTURE_2D, frame.screenTexture);
682
+ const l = loc("SCREEN_TEXTURE");
683
+ if (l) gl.uniform1i(l, unit);
684
+ }
685
+ if (frame.screenPixelSize) two("SCREEN_PIXEL_SIZE", frame.screenPixelSize);
686
+ for (const sampler of frame.samplers) {
687
+ gl.activeTexture(gl.TEXTURE0 + sampler.unit);
688
+ gl.bindTexture(gl.TEXTURE_2D, sampler.texture);
689
+ const l = loc(sampler.name);
690
+ if (l) gl.uniform1i(l, sampler.unit);
691
+ }
692
+ for (const uniform of frame.uniforms) uploadGodotShaderUniform(gl, loc(uniform.name), uniform, frame.params[uniform.name], frame.paramKinds[uniform.name]);
693
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
694
+ }
695
+ /** Exact Godot scalar/vector/array upload rules. Callers retain their own binding model. */
696
+ function uploadGodotShaderUniform(gl, location, uniform, raw, paramKind) {
697
+ if (!location) return;
698
+ const value = raw ?? uniform.default;
699
+ const values = Array.isArray(value) ? [...value] : [];
700
+ const pad = (length) => {
701
+ const out = values.slice(0, length);
702
+ while (out.length < length) out.push(0);
703
+ return out;
704
+ };
705
+ if (uniform.arrayLength) {
706
+ const n = uniform.arrayLength;
707
+ if (uniform.type === "float") gl.uniform1fv(location, new Float32Array(pad(n)));
708
+ else if (uniform.type === "int" || uniform.type === "bool") gl.uniform1iv(location, new Int32Array(pad(n).map(Math.round)));
709
+ else if (uniform.type === "vec2") gl.uniform2fv(location, new Float32Array(pad(n * 2)));
710
+ else if (uniform.type === "vec3") {
711
+ const flat = paramKind === "PackedColorArray" && values.length % 4 === 0 ? values.filter((_, i) => i % 4 !== 3) : values;
712
+ while (flat.length < n * 3) flat.push(0);
713
+ gl.uniform3fv(location, new Float32Array(flat.slice(0, n * 3)));
714
+ } else if (uniform.type === "vec4") gl.uniform4fv(location, new Float32Array(pad(n * 4)));
715
+ return;
716
+ }
717
+ if (uniform.type === "float") gl.uniform1f(location, typeof value === "number" ? value : 0);
718
+ else if (uniform.type === "int" || uniform.type === "bool") gl.uniform1i(location, typeof value === "number" ? Math.round(value) : 0);
719
+ else if (uniform.type === "vec2") gl.uniform2f(location, values[0] ?? 0, values[1] ?? 0);
720
+ else if (uniform.type === "vec3") gl.uniform3f(location, values[0] ?? 0, values[1] ?? 0, values[2] ?? 0);
721
+ else if (uniform.type === "vec4") gl.uniform4f(location, values[0] ?? 0, values[1] ?? 0, values[2] ?? 0, values[3] ?? 0);
722
+ }
723
+ function startShader(gl, type, source) {
724
+ const shader = gl.createShader(type);
725
+ if (!shader) return null;
726
+ gl.shaderSource(shader, source);
727
+ gl.compileShader(shader);
728
+ return shader;
729
+ }
730
+ /** Clear a supplied framebuffer's draw region; callers control its lifetime and binding. */
731
+ function clearWebglSurface(gl, width, height, scissorToViewport = false) {
732
+ gl.viewport(0, 0, width, height);
733
+ if (scissorToViewport) {
734
+ gl.enable(gl.SCISSOR_TEST);
735
+ gl.scissor(0, 0, width, height);
736
+ }
737
+ gl.clearColor(0, 0, 0, 0);
738
+ gl.clear(gl.COLOR_BUFFER_BIT);
739
+ if (scissorToViewport) gl.disable(gl.SCISSOR_TEST);
740
+ }
741
+ /** Release an owned texture. Borrowed texture handles must not be passed here. */
742
+ function deleteWebglTexture(gl, texture) {
743
+ gl.deleteTexture(texture);
744
+ }
745
+ //#endregion
746
+ //#region src/webgl.ts
747
+ /**
748
+ * DOM-free effect targets for a canvas stage.
749
+ *
750
+ * This module deliberately accepts an already-created WebGL2 context. It never
751
+ * asks for a canvas, creates an element, or schedules a frame: the caller owns
752
+ * the stage, painter ordering, and clock. The small raw-GLSL and procedural
753
+ * particle subsets below are useful without claiming to be a Godot material or
754
+ * GPUParticles interpreter.
755
+ */
756
+ /** Allocate reusable, DOM-free simulation and instance storage for a normalized Godot particle spec. */
757
+ function createGodotParticleScratch(config, maxInstances) {
758
+ const normalized = normalizeParticleRenderConfig(config);
759
+ return {
760
+ config: normalized,
761
+ state: createParticleState(normalized, maxInstances),
762
+ instances: new InstanceBuffer(Math.max(1, maxInstances ?? normalized.amount))
763
+ };
764
+ }
765
+ const FULLSCREEN_VERTEX = `#version 300 es
766
+ out vec2 v_uv;
767
+ const vec2 POSITIONS[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
768
+ void main() {
769
+ vec2 position = POSITIONS[gl_VertexID];
770
+ v_uv = position * 0.5 + 0.5;
771
+ gl_Position = vec4(position, 0.0, 1.0);
772
+ }`;
773
+ const GODOT_DEFAULT_FIT = [1, 1];
774
+ const GODOT_DEFAULT_WINDOW = [
775
+ 0,
776
+ 0,
777
+ 1,
778
+ 1
779
+ ];
780
+ const GODOT_DEFAULT_MODULATE = [
781
+ 1,
782
+ 1,
783
+ 1,
784
+ 1
785
+ ];
786
+ const GODOT_DEFAULT_SCREEN_RECT = [
787
+ 0,
788
+ 0,
789
+ 1,
790
+ 1
791
+ ];
792
+ const PARTICLE_VERTEX = `#version 300 es
793
+ uniform float u_time;
794
+ uniform float u_delta;
795
+ uniform float u_seed;
796
+ uniform float u_lifetime;
797
+ uniform float u_emission_rate;
798
+ uniform float u_max_particles;
799
+ uniform vec2 u_position;
800
+ uniform float u_direction;
801
+ uniform float u_spread;
802
+ uniform vec2 u_speed;
803
+ uniform vec2 u_gravity;
804
+ uniform vec2 u_resolution;
805
+ uniform vec2 u_size;
806
+ out float v_progress;
807
+ out float v_alive;
808
+ float hash(float value) { return fract(sin(value) * 43758.5453123); }
809
+ void main() {
810
+ float id = float(gl_VertexID);
811
+ float emission = max(u_emission_rate, 0.0001);
812
+ float active = min(u_max_particles, ceil(emission * u_lifetime));
813
+ float period = active / emission;
814
+ float now = max(0.0, u_time + u_delta);
815
+ float birth = floor((now * emission - id) / active) * period + id / emission;
816
+ float age = now - birth;
817
+ float effectiveLifetime = min(u_lifetime, period);
818
+ float progress = clamp(age / effectiveLifetime, 0.0, 1.0);
819
+ float angle = u_direction + (hash(id + u_seed) - 0.5) * u_spread;
820
+ float speed = mix(u_speed.x, u_speed.y, hash(id * 7.0 + u_seed));
821
+ vec2 velocity = vec2(cos(angle), sin(angle)) * speed;
822
+ vec2 pixel = u_position + velocity * age + 0.5 * u_gravity * age * age;
823
+ vec2 clip = pixel / u_resolution * 2.0 - 1.0;
824
+ gl_Position = vec4(clip * vec2(1.0, -1.0), 0.0, 1.0);
825
+ gl_PointSize = mix(u_size.x, u_size.y, progress);
826
+ v_progress = progress;
827
+ v_alive = step(0.0, birth) * step(0.0, age) * step(age, effectiveLifetime);
828
+ }`;
829
+ /** Safe ceiling for the stateless point-particle subset. Larger systems are refused. */
830
+ const MAX_HEADLESS_PARTICLES = 16384;
831
+ const PARTICLE_FRAGMENT = `#version 300 es
832
+ precision highp float;
833
+ uniform vec4 u_start_color;
834
+ uniform vec4 u_end_color;
835
+ in float v_progress;
836
+ in float v_alive;
837
+ out vec4 fragColor;
838
+ void main() {
839
+ if (v_alive < 0.5) discard;
840
+ vec2 centered = gl_PointCoord * 2.0 - 1.0;
841
+ float coverage = smoothstep(1.0, 0.7, dot(centered, centered));
842
+ vec4 color = mix(u_start_color, u_end_color, v_progress);
843
+ fragColor = vec4(color.rgb * color.a, color.a) * coverage;
844
+ }`;
845
+ function ok(value) {
846
+ return {
847
+ ok: true,
848
+ value
849
+ };
850
+ }
851
+ function fail(code, message, feature) {
852
+ return {
853
+ ok: false,
854
+ diagnostic: {
855
+ code,
856
+ message,
857
+ feature
858
+ }
859
+ };
860
+ }
861
+ function targetDimensions(width, height) {
862
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
863
+ return [Math.max(1, Math.round(width)), Math.max(1, Math.round(height))];
864
+ }
865
+ function directParticleTransform(transform) {
866
+ const values = transform ?? [
867
+ 1,
868
+ 0,
869
+ 0,
870
+ 1,
871
+ 0,
872
+ 0
873
+ ];
874
+ if (!values.every(Number.isFinite)) return null;
875
+ const [xx, xy, yx, yy, originX, originY] = values;
876
+ const xLength = Math.hypot(xx, xy);
877
+ const yLength = Math.hypot(yx, yy);
878
+ const tolerance = Math.max(1, xLength * yLength) * 1e-6;
879
+ if (xLength <= 1e-9 || Math.abs(xLength - yLength) > tolerance || Math.abs(xx * yx + xy * yy) > tolerance || xx * yy - xy * yx <= 0) return null;
880
+ return {
881
+ xx,
882
+ xy,
883
+ yx,
884
+ yy,
885
+ originX,
886
+ originY,
887
+ scale: xLength,
888
+ rotation: Math.atan2(xy, xx)
889
+ };
890
+ }
891
+ function directParticleModulate(modulate) {
892
+ const value = modulate ?? [
893
+ 1,
894
+ 1,
895
+ 1,
896
+ 1
897
+ ];
898
+ return value.every(Number.isFinite) ? [
899
+ value[0],
900
+ value[1],
901
+ value[2],
902
+ value[3]
903
+ ] : null;
904
+ }
905
+ function compileProgram$1(gl, vertexSource, fragmentSource) {
906
+ const vertex = gl.createShader(gl.VERTEX_SHADER);
907
+ const fragment = gl.createShader(gl.FRAGMENT_SHADER);
908
+ if (!vertex || !fragment) {
909
+ if (vertex) gl.deleteShader(vertex);
910
+ if (fragment) gl.deleteShader(fragment);
911
+ return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not allocate an effect shader.");
912
+ }
913
+ gl.shaderSource(vertex, vertexSource);
914
+ gl.shaderSource(fragment, fragmentSource);
915
+ gl.compileShader(vertex);
916
+ gl.compileShader(fragment);
917
+ if (!gl.getShaderParameter(vertex, gl.COMPILE_STATUS) || !gl.getShaderParameter(fragment, gl.COMPILE_STATUS)) {
918
+ const log = gl.getShaderInfoLog(vertex) || gl.getShaderInfoLog(fragment) || "unknown compiler error";
919
+ gl.deleteShader(vertex);
920
+ gl.deleteShader(fragment);
921
+ return fail("SHADER_COMPILE_FAILED", log);
922
+ }
923
+ const program = gl.createProgram();
924
+ if (!program) {
925
+ gl.deleteShader(vertex);
926
+ gl.deleteShader(fragment);
927
+ return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not allocate an effect program.");
928
+ }
929
+ gl.attachShader(program, vertex);
930
+ gl.attachShader(program, fragment);
931
+ gl.linkProgram(program);
932
+ gl.deleteShader(vertex);
933
+ gl.deleteShader(fragment);
934
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
935
+ const log = gl.getProgramInfoLog(program) || "unknown linker error";
936
+ gl.deleteProgram(program);
937
+ return fail("SHADER_COMPILE_FAILED", log);
938
+ }
939
+ const vao = gl.createVertexArray();
940
+ if (!vao) {
941
+ gl.deleteProgram(program);
942
+ return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not allocate an effect vertex array.");
943
+ }
944
+ return ok({
945
+ program,
946
+ vao,
947
+ locations: /* @__PURE__ */ new Map()
948
+ });
949
+ }
950
+ function destroyProgram(gl, program) {
951
+ if (!program) return;
952
+ gl.deleteVertexArray(program.vao);
953
+ gl.deleteProgram(program.program);
954
+ }
955
+ function shaderDiagnostic(pass) {
956
+ const known = new Set([
957
+ "screen-texture",
958
+ "time",
959
+ "delta",
960
+ "seed",
961
+ "external-textures",
962
+ "multiple-render-targets",
963
+ "transform-feedback",
964
+ "godot-source"
965
+ ]);
966
+ const seen = /* @__PURE__ */ new Set();
967
+ for (const feature of pass.features ?? []) {
968
+ if (!known.has(feature)) return {
969
+ code: "UNSUPPORTED_SHADER_FEATURE",
970
+ message: `Unknown headless shader capability ${feature}.`,
971
+ feature
972
+ };
973
+ if (seen.has(feature)) return {
974
+ code: "UNSUPPORTED_SHADER_FEATURE",
975
+ message: `Duplicate headless shader capability ${feature}.`,
976
+ feature
977
+ };
978
+ seen.add(feature);
979
+ }
980
+ for (const [name, value] of Object.entries(pass.uniforms ?? {})) {
981
+ const components = typeof value === "number" ? [value] : value;
982
+ if (!/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(name) || typeof value !== "number" && (value.length < 2 || value.length > 4) || !Array.from(components).every(Number.isFinite)) return {
983
+ code: "UNSUPPORTED_SHADER_FEATURE",
984
+ message: "Shader uniform names and values must be finite GLSL scalar/vector values.",
985
+ feature: `uniform:${name}`
986
+ };
987
+ }
988
+ const unsupported = new Set([
989
+ "external-textures",
990
+ "multiple-render-targets",
991
+ "transform-feedback",
992
+ "godot-source"
993
+ ]);
994
+ const requested = pass.features?.find((feature) => unsupported.has(feature));
995
+ if (requested) return {
996
+ code: "UNSUPPORTED_SHADER_FEATURE",
997
+ message: `Headless shader passes do not support ${requested}.`,
998
+ feature: requested
999
+ };
1000
+ if (!/^\s*#version\s+300\s+es\b/m.test(pass.fragmentSource) || !/\bvoid\s+main\s*\(/.test(pass.fragmentSource)) return {
1001
+ code: "UNSUPPORTED_SHADER_FEATURE",
1002
+ message: "A headless shader pass must be a complete GLSL ES 3 fragment shader.",
1003
+ feature: "raw-glsl300es"
1004
+ };
1005
+ const samplers = [...pass.fragmentSource.matchAll(/uniform\s+sampler\w+\s+(\w+)/g)].map((match) => match[1]);
1006
+ if (samplers.some((name) => name !== "u_screenTexture")) return {
1007
+ code: "UNSUPPORTED_SHADER_FEATURE",
1008
+ message: "Only the explicit u_screenTexture sampler is supported by this headless pass.",
1009
+ feature: "external-textures"
1010
+ };
1011
+ if (samplers.includes("u_screenTexture") && !pass.features?.includes("screen-texture")) return {
1012
+ code: "UNSUPPORTED_SHADER_FEATURE",
1013
+ message: "u_screenTexture requires the explicit screen-texture capability declaration.",
1014
+ feature: "screen-texture"
1015
+ };
1016
+ return null;
1017
+ }
1018
+ function particleDiagnostic(parameters) {
1019
+ const features = parameters.features;
1020
+ if (features?.emissionShape && features.emissionShape !== "point") return {
1021
+ code: "UNSUPPORTED_PARTICLE_FEATURE",
1022
+ message: "Only point particle emission is supported.",
1023
+ feature: `emissionShape:${features.emissionShape}`
1024
+ };
1025
+ for (const feature of [
1026
+ "collision",
1027
+ "attractors",
1028
+ "textureAtlas"
1029
+ ]) if (features?.[feature]) return {
1030
+ code: "UNSUPPORTED_PARTICLE_FEATURE",
1031
+ message: `Particle ${feature} is not supported by the stateless headless path.`,
1032
+ feature
1033
+ };
1034
+ if (![
1035
+ parameters.maxParticles,
1036
+ parameters.emissionRate,
1037
+ parameters.lifetimeSeconds,
1038
+ parameters.position[0],
1039
+ parameters.position[1],
1040
+ parameters.directionRadians,
1041
+ parameters.spreadRadians,
1042
+ parameters.speedMin,
1043
+ parameters.speedMax,
1044
+ parameters.gravity[0],
1045
+ parameters.gravity[1],
1046
+ parameters.startSizePx,
1047
+ parameters.endSizePx,
1048
+ ...parameters.startColor,
1049
+ ...parameters.endColor
1050
+ ].every(Number.isFinite)) return {
1051
+ code: "UNSUPPORTED_PARTICLE_FEATURE",
1052
+ message: "Particle parameters must all be finite numbers.",
1053
+ feature: "parameters"
1054
+ };
1055
+ if (!Number.isInteger(parameters.maxParticles) || parameters.maxParticles <= 0 || parameters.maxParticles > 16384 || parameters.emissionRate <= 0 || parameters.lifetimeSeconds <= 0 || parameters.speedMin < 0 || parameters.speedMax < parameters.speedMin || parameters.startSizePx < 0 || parameters.endSizePx < 0 || parameters.startColor.some((value) => value < 0 || value > 1) || parameters.endColor.some((value) => value < 0 || value > 1)) return {
1056
+ code: "UNSUPPORTED_PARTICLE_FEATURE",
1057
+ message: "Particle counts, emission rate, and lifetime must be positive.",
1058
+ feature: "parameters"
1059
+ };
1060
+ return null;
1061
+ }
1062
+ function renderInputDiagnostic(input) {
1063
+ if (!Number.isFinite(input.time) || !Number.isFinite(input.delta) || input.time < 0 || input.delta < 0 || "seed" in input && input.seed !== void 0 && !Number.isFinite(input.seed)) return {
1064
+ code: "INVALID_RENDER_INPUT",
1065
+ message: "Effect time, delta, and seed must be finite; time and delta must be non-negative.",
1066
+ feature: "render-input"
1067
+ };
1068
+ return null;
1069
+ }
1070
+ function applyGodotBlend(gl, blend) {
1071
+ gl.enable(gl.BLEND);
1072
+ if (blend === "sub") {
1073
+ gl.blendEquationSeparate(gl.FUNC_REVERSE_SUBTRACT, gl.FUNC_ADD);
1074
+ gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
1075
+ return;
1076
+ }
1077
+ gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
1078
+ if (blend === "add") {
1079
+ gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
1080
+ return;
1081
+ }
1082
+ if (blend === "mul") {
1083
+ gl.blendFuncSeparate(gl.DST_COLOR, gl.ZERO, gl.DST_ALPHA, gl.ZERO);
1084
+ return;
1085
+ }
1086
+ gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
1087
+ }
1088
+ /** Create a DOM-free owner for effect FBOs and their ordered GPU passes. */
1089
+ function createHeadlessEffectsStage(gl) {
1090
+ const targets = /* @__PURE__ */ new Set();
1091
+ const invalidatables = /* @__PURE__ */ new Set();
1092
+ let disposed = false;
1093
+ let snapshot = null;
1094
+ let opaqueWhite = null;
1095
+ let opaqueWhiteFilled = false;
1096
+ let opaqueWhiteResult = null;
1097
+ function state() {
1098
+ if (disposed) return fail("DISPOSED", "The headless effects stage has been disposed.");
1099
+ if (gl.isContextLost()) return fail("CONTEXT_LOST", "The WebGL context is lost; invalidate and wait for restoration.");
1100
+ return null;
1101
+ }
1102
+ function makeTarget() {
1103
+ const target = {
1104
+ texture: null,
1105
+ framebuffer: null,
1106
+ width: 0,
1107
+ height: 0
1108
+ };
1109
+ targets.add(target);
1110
+ return target;
1111
+ }
1112
+ function discardTarget(target, callGl) {
1113
+ if (callGl) {
1114
+ if (target.framebuffer) gl.deleteFramebuffer(target.framebuffer);
1115
+ if (target.texture) gl.deleteTexture(target.texture);
1116
+ }
1117
+ target.texture = null;
1118
+ target.framebuffer = null;
1119
+ target.width = 0;
1120
+ target.height = 0;
1121
+ }
1122
+ function resizeTarget(target, width, height) {
1123
+ const current = state();
1124
+ if (current) return current;
1125
+ const dimensions = targetDimensions(width, height);
1126
+ if (!dimensions) return fail("INVALID_DIMENSIONS", "Effect targets need finite dimensions greater than zero.");
1127
+ const [nextWidth, nextHeight] = dimensions;
1128
+ if (target.texture && target.framebuffer && target.width === nextWidth && target.height === nextHeight) return ok(void 0);
1129
+ const texture = gl.createTexture();
1130
+ const framebuffer = gl.createFramebuffer();
1131
+ if (!texture || !framebuffer) {
1132
+ if (texture) gl.deleteTexture(texture);
1133
+ if (framebuffer) gl.deleteFramebuffer(framebuffer);
1134
+ return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not allocate an RGBA8 effect target.");
1135
+ }
1136
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1137
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1138
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1139
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1140
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1141
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, nextWidth, nextHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
1142
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
1143
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
1144
+ const complete = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
1145
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1146
+ if (!complete) {
1147
+ gl.deleteFramebuffer(framebuffer);
1148
+ gl.deleteTexture(texture);
1149
+ return fail("FRAMEBUFFER_INCOMPLETE", "WebGL rejected the RGBA8 effect framebuffer.");
1150
+ }
1151
+ discardTarget(target, true);
1152
+ target.texture = texture;
1153
+ target.framebuffer = framebuffer;
1154
+ target.width = nextWidth;
1155
+ target.height = nextHeight;
1156
+ return ok(void 0);
1157
+ }
1158
+ function copy(source, destination, filter) {
1159
+ const previousRead = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING);
1160
+ const previousDraw = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING);
1161
+ try {
1162
+ gl.disable(gl.SCISSOR_TEST);
1163
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, source.framebuffer);
1164
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, destination.framebuffer);
1165
+ gl.blitFramebuffer(0, 0, source.width, source.height, 0, 0, destination.width, destination.height, gl.COLOR_BUFFER_BIT, filter === "linear" ? gl.LINEAR : gl.NEAREST);
1166
+ } finally {
1167
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, previousRead);
1168
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, previousDraw);
1169
+ }
1170
+ }
1171
+ function snapshotFor(target) {
1172
+ if (!snapshot) snapshot = makeTarget();
1173
+ const resized = resizeTarget(snapshot, target.width, target.height);
1174
+ if (!resized.ok) return resized;
1175
+ copy(target, snapshot, "nearest");
1176
+ return ok(snapshot);
1177
+ }
1178
+ /** A stage-owned, opaque TEXTURE fallback for ColorRect/pure-fill shaders. */
1179
+ function whiteTarget() {
1180
+ if (!opaqueWhite) opaqueWhite = makeTarget();
1181
+ if (!opaqueWhite.texture || !opaqueWhite.framebuffer) {
1182
+ const resized = resizeTarget(opaqueWhite, 1, 1);
1183
+ if (!resized.ok) return resized;
1184
+ opaqueWhiteFilled = false;
1185
+ }
1186
+ if (opaqueWhiteFilled && opaqueWhiteResult) return opaqueWhiteResult;
1187
+ const previous = gl.getParameter(gl.FRAMEBUFFER_BINDING);
1188
+ try {
1189
+ gl.bindFramebuffer(gl.FRAMEBUFFER, opaqueWhite.framebuffer);
1190
+ gl.viewport(0, 0, 1, 1);
1191
+ gl.disable(gl.SCISSOR_TEST);
1192
+ gl.clearColor(1, 1, 1, 1);
1193
+ gl.clear(gl.COLOR_BUFFER_BIT);
1194
+ opaqueWhiteFilled = true;
1195
+ opaqueWhiteResult = ok(opaqueWhite);
1196
+ } finally {
1197
+ gl.bindFramebuffer(gl.FRAMEBUFFER, previous);
1198
+ }
1199
+ return ok(opaqueWhite);
1200
+ }
1201
+ function snapshotFramebuffer(framebuffer, width, height) {
1202
+ if (!snapshot) snapshot = makeTarget();
1203
+ const resized = resizeTarget(snapshot, width, height);
1204
+ if (!resized.ok) return resized;
1205
+ const previousRead = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING);
1206
+ const previousDraw = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING);
1207
+ try {
1208
+ gl.disable(gl.SCISSOR_TEST);
1209
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, framebuffer);
1210
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, snapshot.framebuffer);
1211
+ gl.blitFramebuffer(0, 0, width, height, 0, 0, width, height, gl.COLOR_BUFFER_BIT, gl.NEAREST);
1212
+ } finally {
1213
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, previousRead);
1214
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, previousDraw);
1215
+ }
1216
+ return ok(snapshot);
1217
+ }
1218
+ function targetFor(value) {
1219
+ return targets.has(value) ? value : null;
1220
+ }
1221
+ function textureInputFor(value) {
1222
+ const owned = targetFor(value);
1223
+ if (owned?.texture && owned.width > 0 && owned.height > 0) return {
1224
+ texture: owned.texture,
1225
+ width: owned.width,
1226
+ height: owned.height,
1227
+ owned
1228
+ };
1229
+ const borrowed = value;
1230
+ if (!borrowed.texture || !Number.isFinite(borrowed.width) || !Number.isFinite(borrowed.height) || borrowed.width <= 0 || borrowed.height <= 0) return null;
1231
+ return {
1232
+ texture: borrowed.texture,
1233
+ width: borrowed.width,
1234
+ height: borrowed.height,
1235
+ owned: null
1236
+ };
1237
+ }
1238
+ function uniformLocation(program, name) {
1239
+ const known = program.locations.get(name);
1240
+ if (known !== void 0) return known;
1241
+ if (program.locations.has(name)) return null;
1242
+ const location = gl.getUniformLocation(program.program, name);
1243
+ program.locations.set(name, location);
1244
+ return location;
1245
+ }
1246
+ function applyUniform(program, name, value) {
1247
+ const location = uniformLocation(program, name);
1248
+ if (location === null) return;
1249
+ if (typeof value === "number") gl.uniform1f(location, value);
1250
+ else if (value.length === 2) gl.uniform2f(location, value[0], value[1]);
1251
+ else if (value.length === 3) gl.uniform3f(location, value[0], value[1], value[2]);
1252
+ else gl.uniform4f(location, value[0], value[1], value[2], value[3]);
1253
+ }
1254
+ function createShaderProducer(pass, width, height) {
1255
+ const invalid = state();
1256
+ if (invalid) return invalid;
1257
+ const diagnostic = shaderDiagnostic(pass);
1258
+ if (diagnostic) return {
1259
+ ok: false,
1260
+ diagnostic
1261
+ };
1262
+ const target = makeTarget();
1263
+ const allocated = resizeTarget(target, width, height);
1264
+ if (!allocated.ok) {
1265
+ targets.delete(target);
1266
+ return allocated;
1267
+ }
1268
+ let requestedWidth = target.width;
1269
+ let requestedHeight = target.height;
1270
+ const resolution = [target.width, target.height];
1271
+ const uniformEntries = Object.entries(pass.uniforms ?? {});
1272
+ const rendered = ok(target);
1273
+ let program = null;
1274
+ let producerDisposed = false;
1275
+ const needsScreen = pass.features?.includes("screen-texture") === true;
1276
+ function ensureProgram() {
1277
+ if (program) return ok(program);
1278
+ const compiled = compileProgram$1(gl, FULLSCREEN_VERTEX, pass.fragmentSource);
1279
+ if (compiled.ok) program = compiled.value;
1280
+ return compiled;
1281
+ }
1282
+ const producer = {
1283
+ target,
1284
+ warmUp() {
1285
+ const current = state();
1286
+ if (current) return current;
1287
+ if (producerDisposed) return fail("DISPOSED", "The shader producer has been disposed.");
1288
+ const ready = ensureProgram();
1289
+ return ready.ok ? ok(void 0) : ready;
1290
+ },
1291
+ resize(nextWidth, nextHeight) {
1292
+ if (producerDisposed) return fail("DISPOSED", "The shader producer has been disposed.");
1293
+ const resized = resizeTarget(target, nextWidth, nextHeight);
1294
+ if (resized.ok) {
1295
+ requestedWidth = target.width;
1296
+ requestedHeight = target.height;
1297
+ resolution[0] = target.width;
1298
+ resolution[1] = target.height;
1299
+ }
1300
+ return resized;
1301
+ },
1302
+ render(input) {
1303
+ const inputDiagnostic = renderInputDiagnostic(input);
1304
+ if (inputDiagnostic) return {
1305
+ ok: false,
1306
+ diagnostic: inputDiagnostic
1307
+ };
1308
+ const current = state();
1309
+ if (current) return current;
1310
+ if (producerDisposed) return fail("DISPOSED", "The shader producer has been disposed.");
1311
+ if (!target.texture || !target.framebuffer) {
1312
+ const reallocated = resizeTarget(target, requestedWidth, requestedHeight);
1313
+ if (!reallocated.ok) return reallocated;
1314
+ }
1315
+ let screen = null;
1316
+ if (needsScreen) {
1317
+ if (!input.screenTexture) return fail("SCREEN_TEXTURE_REQUIRED", "This shader pass declared screen-texture but no accumulated target was supplied.");
1318
+ screen = targetFor(input.screenTexture);
1319
+ if (!screen?.texture || !screen.framebuffer) return fail("FOREIGN_TARGET", "screenTexture must be a live target owned by this headless effects stage.");
1320
+ if (screen === target) {
1321
+ const captured = snapshotFor(screen);
1322
+ if (!captured.ok) return captured;
1323
+ screen = captured.value;
1324
+ }
1325
+ }
1326
+ const ready = ensureProgram();
1327
+ if (!ready.ok) return ready;
1328
+ const previousFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
1329
+ try {
1330
+ gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
1331
+ gl.viewport(0, 0, target.width, target.height);
1332
+ gl.disable(gl.SCISSOR_TEST);
1333
+ gl.clearColor(0, 0, 0, 0);
1334
+ gl.clear(gl.COLOR_BUFFER_BIT);
1335
+ gl.enable(gl.BLEND);
1336
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
1337
+ gl.useProgram(ready.value.program);
1338
+ gl.bindVertexArray(ready.value.vao);
1339
+ applyUniform(ready.value, "u_time", input.time);
1340
+ applyUniform(ready.value, "u_delta", input.delta);
1341
+ applyUniform(ready.value, "u_seed", input.seed ?? 0);
1342
+ applyUniform(ready.value, "u_resolution", resolution);
1343
+ for (const [name, value] of uniformEntries) applyUniform(ready.value, name, value);
1344
+ if (screen) {
1345
+ gl.activeTexture(gl.TEXTURE0);
1346
+ gl.bindTexture(gl.TEXTURE_2D, screen.texture);
1347
+ const location = uniformLocation(ready.value, "u_screenTexture");
1348
+ if (location) gl.uniform1i(location, 0);
1349
+ }
1350
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
1351
+ gl.bindVertexArray(null);
1352
+ return rendered;
1353
+ } finally {
1354
+ gl.bindFramebuffer(gl.FRAMEBUFFER, previousFramebuffer);
1355
+ }
1356
+ },
1357
+ renderScreen(input, context) {
1358
+ const inputDiagnostic = renderInputDiagnostic(input);
1359
+ if (inputDiagnostic) return {
1360
+ ok: false,
1361
+ diagnostic: inputDiagnostic
1362
+ };
1363
+ if (!needsScreen) return fail("UNSUPPORTED_SHADER_FEATURE", "Only a screen-texture pass can be recorded as a screen effect.", "screen-texture");
1364
+ if (context.damage) return fail("UNSUPPORTED_SHADER_FEATURE", "Screen-dependent effects decline partial damage execution.", "partial-damage");
1365
+ const resized = producer.resize(context.width, context.height);
1366
+ if (!resized.ok) return resized;
1367
+ const captured = snapshotFramebuffer(context.framebuffer, context.width, context.height);
1368
+ if (!captured.ok) return captured;
1369
+ const renderedScreen = producer.render({
1370
+ time: input.time,
1371
+ delta: input.delta,
1372
+ seed: input.seed,
1373
+ screenTexture: captured.value
1374
+ });
1375
+ if (!renderedScreen.ok) return renderedScreen;
1376
+ const previousRead = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING);
1377
+ const previousDraw = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING);
1378
+ try {
1379
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, target.framebuffer);
1380
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, context.framebuffer);
1381
+ gl.enable(gl.SCISSOR_TEST);
1382
+ gl.scissor(context.scissor.x, context.scissor.y, context.scissor.width, context.scissor.height);
1383
+ gl.blitFramebuffer(0, 0, target.width, target.height, 0, 0, context.width, context.height, gl.COLOR_BUFFER_BIT, gl.NEAREST);
1384
+ } finally {
1385
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, previousRead);
1386
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, previousDraw);
1387
+ }
1388
+ return ok(void 0);
1389
+ },
1390
+ invalidate() {
1391
+ destroyProgram(gl, program);
1392
+ program = null;
1393
+ discardTarget(target, true);
1394
+ },
1395
+ invalidateContextLoss() {
1396
+ program = null;
1397
+ discardTarget(target, false);
1398
+ },
1399
+ dispose() {
1400
+ if (!producerDisposed) {
1401
+ producerDisposed = true;
1402
+ destroyProgram(gl, program);
1403
+ program = null;
1404
+ discardTarget(target, true);
1405
+ targets.delete(target);
1406
+ invalidatables.delete(producer);
1407
+ }
1408
+ }
1409
+ };
1410
+ invalidatables.add(producer);
1411
+ return ok(producer);
1412
+ }
1413
+ function createGodotShaderProducer(pass, width, height) {
1414
+ const invalid = state();
1415
+ if (invalid) return invalid;
1416
+ let transpiled;
1417
+ try {
1418
+ transpiled = transpileGodotShader(pass.source);
1419
+ } catch (error) {
1420
+ return fail("UNSUPPORTED_SHADER_FEATURE", error instanceof UnsupportedShaderError ? error.message : `Godot shader transpilation failed: ${error instanceof Error ? error.message : String(error)}`, "godot-source");
1421
+ }
1422
+ const suppliedSamplers = { ...pass.samplers ?? {} };
1423
+ for (const sampler of transpiled.samplers) {
1424
+ if (!suppliedSamplers[sampler.name]) return fail("UNSUPPORTED_SHADER_FEATURE", `Godot sampler ${sampler.name} needs an explicit stage-owned target.`, `sampler:${sampler.name}`);
1425
+ if (sampler.repeat) return fail("UNSUPPORTED_SHADER_FEATURE", `Godot sampler ${sampler.name} requests repeat sampling, which stage targets cannot mutate.`, `sampler:${sampler.name}:repeat`);
1426
+ }
1427
+ for (const uniform of transpiled.uniforms) if (uniform.arrayLength || ![
1428
+ "float",
1429
+ "int",
1430
+ "bool",
1431
+ "vec2",
1432
+ "vec3",
1433
+ "vec4"
1434
+ ].includes(uniform.type)) return fail("UNSUPPORTED_SHADER_FEATURE", `Godot uniform ${uniform.name} (${uniform.type}) is not supported headlessly.`, `uniform:${uniform.name}`);
1435
+ const componentCount = (type) => type === "vec2" ? 2 : type === "vec3" ? 3 : type === "vec4" ? 4 : 1;
1436
+ const uniformValues = {};
1437
+ for (const uniform of transpiled.uniforms) {
1438
+ const raw = pass.uniforms?.[uniform.name] ?? uniform.default;
1439
+ if (raw === void 0) continue;
1440
+ const expected = componentCount(uniform.type);
1441
+ const values = typeof raw === "number" ? null : raw;
1442
+ if (expected === 1 && typeof raw !== "number" || expected !== 1 && (!values || values.length !== expected) || typeof raw === "number" && !Number.isFinite(raw) || values && !values.every(Number.isFinite)) return fail("UNSUPPORTED_SHADER_FEATURE", `Godot uniform ${uniform.name} must be ${expected} finite numeric component${expected === 1 ? "" : "s"}.`, `uniform:${uniform.name}`);
1443
+ uniformValues[uniform.name] = typeof raw === "number" ? raw : [...raw];
1444
+ }
1445
+ const fit = [...pass.uvFit ?? GODOT_DEFAULT_FIT];
1446
+ const window = [...pass.uvWindow ?? GODOT_DEFAULT_WINDOW];
1447
+ const modulate = [...pass.modulate ?? GODOT_DEFAULT_MODULATE];
1448
+ if (!fit.every(Number.isFinite) || !window.every(Number.isFinite) || !modulate.every(Number.isFinite)) return fail("UNSUPPORTED_SHADER_FEATURE", "Godot shader fit, window, and modulate values must be finite.", "render-parameters");
1449
+ const target = makeTarget();
1450
+ const allocated = resizeTarget(target, width, height);
1451
+ if (!allocated.ok) {
1452
+ targets.delete(target);
1453
+ return allocated;
1454
+ }
1455
+ let requestedWidth = target.width;
1456
+ let requestedHeight = target.height;
1457
+ let program = null;
1458
+ let producerDisposed = false;
1459
+ const rendered = ok(target);
1460
+ const noScreen = ok(null);
1461
+ const texturePixelSize = [1, 1];
1462
+ const screenPixelSize = [1, 1];
1463
+ const screenOrigin = [0, 0];
1464
+ const screenSize = [1, 1];
1465
+ const ensureProgram = () => {
1466
+ if (program) return ok(program);
1467
+ const compiled = compileProgram$1(gl, FULLSCREEN_VERTEX, transpiled.fragmentGlsl);
1468
+ if (compiled.ok) program = compiled.value;
1469
+ return compiled;
1470
+ };
1471
+ const stageTarget = (value, label) => {
1472
+ if (!value) return ok(null);
1473
+ const texture = textureInputFor(value);
1474
+ return texture ? ok(texture) : fail("FOREIGN_TARGET", `${label} must be a live same-context stage target or texture handle.`);
1475
+ };
1476
+ const suppliedTexture = stageTarget(pass.texture, "TEXTURE");
1477
+ if (!suppliedTexture.ok) {
1478
+ discardTarget(target, true);
1479
+ targets.delete(target);
1480
+ return suppliedTexture;
1481
+ }
1482
+ const baseTexture = suppliedTexture.value;
1483
+ const samplerTargets = [];
1484
+ for (const sampler of transpiled.samplers) {
1485
+ const supplied = stageTarget(suppliedSamplers[sampler.name], `sampler:${sampler.name}`);
1486
+ if (!supplied.ok || !supplied.value) {
1487
+ discardTarget(target, true);
1488
+ targets.delete(target);
1489
+ return supplied.ok ? fail("UNSUPPORTED_SHADER_FEATURE", `Godot sampler ${sampler.name} needs an explicit target.`, `sampler:${sampler.name}`) : supplied;
1490
+ }
1491
+ samplerTargets.push(supplied.value);
1492
+ }
1493
+ const applyGodotUniform = (p, uniform) => {
1494
+ const raw = uniformValues[uniform.name];
1495
+ const loc = uniformLocation(p, uniform.name);
1496
+ if (!loc) return;
1497
+ if (typeof raw === "number") {
1498
+ if (uniform.type === "float") gl.uniform1f(loc, raw);
1499
+ else if (uniform.type === "int" || uniform.type === "bool") gl.uniform1i(loc, Math.round(raw));
1500
+ return;
1501
+ }
1502
+ const values = raw ?? [];
1503
+ if (uniform.type === "float") gl.uniform1f(loc, 0);
1504
+ else if (uniform.type === "int" || uniform.type === "bool") gl.uniform1i(loc, 0);
1505
+ else if (uniform.type === "vec2") gl.uniform2f(loc, values[0] ?? 0, values[1] ?? 0);
1506
+ else if (uniform.type === "vec3") gl.uniform3f(loc, values[0] ?? 0, values[1] ?? 0, values[2] ?? 0);
1507
+ else gl.uniform4f(loc, values[0] ?? 0, values[1] ?? 0, values[2] ?? 0, values[3] ?? 0);
1508
+ };
1509
+ const liveDependencies = ok(void 0);
1510
+ const dependenciesLive = () => {
1511
+ if (baseTexture && (baseTexture.owned ? !targets.has(baseTexture.owned) || !baseTexture.owned.texture || !baseTexture.owned.framebuffer : typeof gl.isTexture === "function" && !gl.isTexture(baseTexture.texture))) return fail("FOREIGN_TARGET", "A Godot shader dependency is no longer a live stage-owned target.", "dependency");
1512
+ for (let i = 0; i < samplerTargets.length; i += 1) {
1513
+ const dependency = samplerTargets[i];
1514
+ if (dependency.owned ? !targets.has(dependency.owned) || !dependency.owned.texture || !dependency.owned.framebuffer : typeof gl.isTexture === "function" && !gl.isTexture(dependency.texture)) return fail("FOREIGN_TARGET", "A Godot shader dependency is no longer a live stage-owned target.", "dependency");
1515
+ }
1516
+ return liveDependencies;
1517
+ };
1518
+ const producer = {
1519
+ target,
1520
+ blend: transpiled.blend,
1521
+ warmUp() {
1522
+ const current = state();
1523
+ if (current) return current;
1524
+ if (producerDisposed) return fail("DISPOSED", "The Godot shader producer has been disposed.");
1525
+ const ready = ensureProgram();
1526
+ return ready.ok ? ok(void 0) : ready;
1527
+ },
1528
+ resize(nextWidth, nextHeight) {
1529
+ if (producerDisposed) return fail("DISPOSED", "The Godot shader producer has been disposed.");
1530
+ const resized = resizeTarget(target, nextWidth, nextHeight);
1531
+ if (resized.ok) {
1532
+ requestedWidth = target.width;
1533
+ requestedHeight = target.height;
1534
+ }
1535
+ return resized;
1536
+ },
1537
+ render(input) {
1538
+ const inputDiagnostic = renderInputDiagnostic(input);
1539
+ if (inputDiagnostic) return {
1540
+ ok: false,
1541
+ diagnostic: inputDiagnostic
1542
+ };
1543
+ if (input.screenRect && !input.screenRect.every(Number.isFinite)) return fail("INVALID_RENDER_INPUT", "Godot screenRect values must be finite.", "screen-rect");
1544
+ const current = state();
1545
+ if (current) return current;
1546
+ if (producerDisposed) return fail("DISPOSED", "The Godot shader producer has been disposed.");
1547
+ if (!target.texture || !target.framebuffer) {
1548
+ const resized = resizeTarget(target, requestedWidth, requestedHeight);
1549
+ if (!resized.ok) return resized;
1550
+ }
1551
+ const dependencies = dependenciesLive();
1552
+ if (!dependencies.ok) return dependencies;
1553
+ const white = baseTexture ? null : whiteTarget();
1554
+ if (white && !white.ok) return white;
1555
+ const texture = baseTexture ?? white?.value;
1556
+ const screen = transpiled.usesScreenTexture ? stageTarget(input.screenTexture, "SCREEN_TEXTURE") : noScreen;
1557
+ if (!screen.ok) return screen;
1558
+ if (transpiled.usesScreenTexture && !screen.value) return fail("SCREEN_TEXTURE_REQUIRED", "This Godot shader samples SCREEN_TEXTURE but no accumulated target was supplied.");
1559
+ const screenTarget = screen.value === target ? snapshotFor(target) : screen;
1560
+ if (!screenTarget.ok) return screenTarget;
1561
+ const ready = ensureProgram();
1562
+ if (!ready.ok) return ready;
1563
+ const previous = gl.getParameter(gl.FRAMEBUFFER_BINDING);
1564
+ try {
1565
+ gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
1566
+ gl.viewport(0, 0, target.width, target.height);
1567
+ gl.disable(gl.SCISSOR_TEST);
1568
+ gl.disable(gl.BLEND);
1569
+ gl.clearColor(0, 0, 0, 0);
1570
+ gl.clear(gl.COLOR_BUFFER_BIT);
1571
+ gl.useProgram(ready.value.program);
1572
+ gl.bindVertexArray(ready.value.vao);
1573
+ gl.activeTexture(gl.TEXTURE0);
1574
+ gl.bindTexture(gl.TEXTURE_2D, texture?.texture ?? null);
1575
+ const textureLoc = uniformLocation(ready.value, "TEXTURE");
1576
+ if (textureLoc) gl.uniform1i(textureLoc, 0);
1577
+ if (transpiled.usesTime) applyUniform(ready.value, "TIME", input.time);
1578
+ if (transpiled.usesTexturePixelSize) {
1579
+ texturePixelSize[0] = 1 / Math.max(1, texture?.width ?? 1);
1580
+ texturePixelSize[1] = 1 / Math.max(1, texture?.height ?? 1);
1581
+ applyUniform(ready.value, "TEXTURE_PIXEL_SIZE", texturePixelSize);
1582
+ }
1583
+ applyUniform(ready.value, "MODULATE", modulate);
1584
+ applyUniform(ready.value, "_godot_uv_fit", fit);
1585
+ applyUniform(ready.value, "_godot_uv_window", window);
1586
+ const rect = input.screenRect ?? GODOT_DEFAULT_SCREEN_RECT;
1587
+ if (transpiled.usesScreenUv) {
1588
+ screenOrigin[0] = rect[0];
1589
+ screenOrigin[1] = rect[1];
1590
+ screenSize[0] = rect[2];
1591
+ screenSize[1] = rect[3];
1592
+ applyUniform(ready.value, "_godot_screen_origin", screenOrigin);
1593
+ applyUniform(ready.value, "_godot_screen_size", screenSize);
1594
+ }
1595
+ if (screenTarget.value) {
1596
+ gl.activeTexture(gl.TEXTURE0 + 1);
1597
+ gl.bindTexture(gl.TEXTURE_2D, screenTarget.value.texture);
1598
+ const loc = uniformLocation(ready.value, "SCREEN_TEXTURE");
1599
+ if (loc) gl.uniform1i(loc, 1);
1600
+ }
1601
+ if (transpiled.usesScreenPixelSize) {
1602
+ screenPixelSize[0] = 1 / Math.max(1, screenTarget.value?.width ?? target.width);
1603
+ screenPixelSize[1] = 1 / Math.max(1, screenTarget.value?.height ?? target.height);
1604
+ applyUniform(ready.value, "SCREEN_PIXEL_SIZE", screenPixelSize);
1605
+ }
1606
+ let unit = 2;
1607
+ for (let i = 0; i < transpiled.samplers.length; i += 1) {
1608
+ const sampler = transpiled.samplers[i];
1609
+ const value = samplerTargets[i];
1610
+ gl.activeTexture(gl.TEXTURE0 + unit);
1611
+ gl.bindTexture(gl.TEXTURE_2D, value.texture);
1612
+ const loc = uniformLocation(ready.value, sampler.name);
1613
+ if (loc) gl.uniform1i(loc, unit++);
1614
+ }
1615
+ for (const uniform of transpiled.uniforms) applyGodotUniform(ready.value, uniform);
1616
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
1617
+ gl.bindVertexArray(null);
1618
+ return rendered;
1619
+ } finally {
1620
+ gl.bindFramebuffer(gl.FRAMEBUFFER, previous);
1621
+ }
1622
+ },
1623
+ renderScreen(input, context) {
1624
+ if (!transpiled.usesScreenTexture) return fail("UNSUPPORTED_SHADER_FEATURE", "Only a Godot SCREEN_TEXTURE shader can be recorded as a screen effect.", "screen-texture");
1625
+ if (context.damage) return fail("UNSUPPORTED_SHADER_FEATURE", "Screen-dependent effects decline partial damage execution.", "partial-damage");
1626
+ const dependencies = dependenciesLive();
1627
+ if (!dependencies.ok) return dependencies;
1628
+ const rect = input.screenRect;
1629
+ if (!rect?.every(Number.isFinite) || rect[0] < 0 || rect[1] < 0 || rect[2] <= 0 || rect[3] <= 0 || rect[0] + rect[2] > 1 || rect[1] + rect[3] > 1) return fail("UNSUPPORTED_SHADER_FEATURE", "Screen shaders need a finite, normalized axis-aligned screenRect.", "screen-rect");
1630
+ const resized = producer.resize(context.width, context.height);
1631
+ if (!resized.ok) return resized;
1632
+ const captured = snapshotFramebuffer(context.framebuffer, context.width, context.height);
1633
+ if (!captured.ok) return captured;
1634
+ copy(captured.value, target, "nearest");
1635
+ const x = Math.round(rect[0] * context.width);
1636
+ const x1 = Math.round((rect[0] + rect[2]) * context.width);
1637
+ const topY = Math.round(rect[1] * context.height);
1638
+ const bottomY = Math.round((rect[1] + rect[3]) * context.height);
1639
+ const y = context.height - bottomY;
1640
+ const width = x1 - x;
1641
+ const height = bottomY - topY;
1642
+ const left = Math.max(x, context.scissor.x);
1643
+ const bottom = Math.max(y, context.scissor.y);
1644
+ const right = Math.min(x1, context.scissor.x + context.scissor.width);
1645
+ const top = Math.min(context.height - topY, context.scissor.y + context.scissor.height);
1646
+ if (right <= left || top <= bottom) return ok(void 0);
1647
+ const ready = ensureProgram();
1648
+ if (!ready.ok) return ready;
1649
+ const white = baseTexture ? null : whiteTarget();
1650
+ if (white && !white.ok) return white;
1651
+ const texture = baseTexture ?? white?.value;
1652
+ const previous = gl.getParameter(gl.FRAMEBUFFER_BINDING);
1653
+ try {
1654
+ gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
1655
+ gl.viewport(x, y, width, height);
1656
+ gl.enable(gl.SCISSOR_TEST);
1657
+ gl.scissor(left, bottom, right - left, top - bottom);
1658
+ applyGodotBlend(gl, transpiled.blend);
1659
+ gl.useProgram(ready.value.program);
1660
+ gl.bindVertexArray(ready.value.vao);
1661
+ gl.activeTexture(gl.TEXTURE0);
1662
+ gl.bindTexture(gl.TEXTURE_2D, texture?.texture ?? null);
1663
+ const textureLoc = uniformLocation(ready.value, "TEXTURE");
1664
+ if (textureLoc) gl.uniform1i(textureLoc, 0);
1665
+ if (transpiled.usesTime) applyUniform(ready.value, "TIME", input.time);
1666
+ if (transpiled.usesTexturePixelSize) {
1667
+ texturePixelSize[0] = 1 / Math.max(1, texture?.width ?? 1);
1668
+ texturePixelSize[1] = 1 / Math.max(1, texture?.height ?? 1);
1669
+ applyUniform(ready.value, "TEXTURE_PIXEL_SIZE", texturePixelSize);
1670
+ }
1671
+ applyUniform(ready.value, "MODULATE", modulate);
1672
+ applyUniform(ready.value, "_godot_uv_fit", fit);
1673
+ applyUniform(ready.value, "_godot_uv_window", window);
1674
+ screenOrigin[0] = rect[0];
1675
+ screenOrigin[1] = rect[1];
1676
+ screenSize[0] = rect[2];
1677
+ screenSize[1] = rect[3];
1678
+ if (transpiled.usesScreenUv) {
1679
+ applyUniform(ready.value, "_godot_screen_origin", screenOrigin);
1680
+ applyUniform(ready.value, "_godot_screen_size", screenSize);
1681
+ }
1682
+ gl.activeTexture(gl.TEXTURE0 + 1);
1683
+ gl.bindTexture(gl.TEXTURE_2D, captured.value.texture);
1684
+ const screenLoc = uniformLocation(ready.value, "SCREEN_TEXTURE");
1685
+ if (screenLoc) gl.uniform1i(screenLoc, 1);
1686
+ if (transpiled.usesScreenPixelSize) {
1687
+ screenPixelSize[0] = 1 / context.width;
1688
+ screenPixelSize[1] = 1 / context.height;
1689
+ applyUniform(ready.value, "SCREEN_PIXEL_SIZE", screenPixelSize);
1690
+ }
1691
+ let unit = 2;
1692
+ for (let i = 0; i < transpiled.samplers.length; i += 1) {
1693
+ const sampler = transpiled.samplers[i];
1694
+ gl.activeTexture(gl.TEXTURE0 + unit);
1695
+ gl.bindTexture(gl.TEXTURE_2D, samplerTargets[i].texture);
1696
+ const loc = uniformLocation(ready.value, sampler.name);
1697
+ if (loc) gl.uniform1i(loc, unit++);
1698
+ }
1699
+ for (const uniform of transpiled.uniforms) applyGodotUniform(ready.value, uniform);
1700
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
1701
+ gl.bindVertexArray(null);
1702
+ } finally {
1703
+ gl.bindFramebuffer(gl.FRAMEBUFFER, previous);
1704
+ }
1705
+ const previousRead = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING);
1706
+ const previousDraw = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING);
1707
+ try {
1708
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, target.framebuffer);
1709
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, context.framebuffer);
1710
+ gl.enable(gl.SCISSOR_TEST);
1711
+ gl.scissor(context.scissor.x, context.scissor.y, context.scissor.width, context.scissor.height);
1712
+ gl.blitFramebuffer(0, 0, target.width, target.height, 0, 0, context.width, context.height, gl.COLOR_BUFFER_BIT, gl.NEAREST);
1713
+ } finally {
1714
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, previousRead);
1715
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, previousDraw);
1716
+ }
1717
+ return ok(void 0);
1718
+ },
1719
+ invalidate() {
1720
+ destroyProgram(gl, program);
1721
+ program = null;
1722
+ discardTarget(target, true);
1723
+ },
1724
+ invalidateContextLoss() {
1725
+ program = null;
1726
+ discardTarget(target, false);
1727
+ },
1728
+ dispose() {
1729
+ if (!producerDisposed) {
1730
+ producerDisposed = true;
1731
+ destroyProgram(gl, program);
1732
+ program = null;
1733
+ discardTarget(target, true);
1734
+ targets.delete(target);
1735
+ invalidatables.delete(producer);
1736
+ }
1737
+ }
1738
+ };
1739
+ invalidatables.add(producer);
1740
+ return ok(producer);
1741
+ }
1742
+ function createParticleProducer(parameters, width, height) {
1743
+ const invalid = state();
1744
+ if (invalid) return invalid;
1745
+ const diagnostic = particleDiagnostic(parameters);
1746
+ if (diagnostic) return {
1747
+ ok: false,
1748
+ diagnostic
1749
+ };
1750
+ const target = makeTarget();
1751
+ const allocated = resizeTarget(target, width, height);
1752
+ if (!allocated.ok) {
1753
+ targets.delete(target);
1754
+ return allocated;
1755
+ }
1756
+ let requestedWidth = target.width;
1757
+ let requestedHeight = target.height;
1758
+ const resolution = [target.width, target.height];
1759
+ const speed = [parameters.speedMin, parameters.speedMax];
1760
+ const size = [parameters.startSizePx, parameters.endSizePx];
1761
+ const rendered = ok(target);
1762
+ let program = null;
1763
+ let producerDisposed = false;
1764
+ const count = Math.min(Math.floor(parameters.maxParticles), Math.max(1, Math.ceil(parameters.emissionRate * parameters.lifetimeSeconds)));
1765
+ function ensureProgram() {
1766
+ if (program) return ok(program);
1767
+ const compiled = compileProgram$1(gl, PARTICLE_VERTEX, PARTICLE_FRAGMENT);
1768
+ if (compiled.ok) program = compiled.value;
1769
+ return compiled;
1770
+ }
1771
+ const producer = {
1772
+ target,
1773
+ warmUp() {
1774
+ const current = state();
1775
+ if (current) return current;
1776
+ if (producerDisposed) return fail("DISPOSED", "The particle producer has been disposed.");
1777
+ const ready = ensureProgram();
1778
+ return ready.ok ? ok(void 0) : ready;
1779
+ },
1780
+ resize(nextWidth, nextHeight) {
1781
+ if (producerDisposed) return fail("DISPOSED", "The particle producer has been disposed.");
1782
+ const resized = resizeTarget(target, nextWidth, nextHeight);
1783
+ if (resized.ok) {
1784
+ requestedWidth = target.width;
1785
+ requestedHeight = target.height;
1786
+ resolution[0] = target.width;
1787
+ resolution[1] = target.height;
1788
+ }
1789
+ return resized;
1790
+ },
1791
+ render(input) {
1792
+ const inputDiagnostic = renderInputDiagnostic(input);
1793
+ if (inputDiagnostic) return {
1794
+ ok: false,
1795
+ diagnostic: inputDiagnostic
1796
+ };
1797
+ const current = state();
1798
+ if (current) return current;
1799
+ if (producerDisposed) return fail("DISPOSED", "The particle producer has been disposed.");
1800
+ if (!target.framebuffer) {
1801
+ const reallocated = resizeTarget(target, requestedWidth, requestedHeight);
1802
+ if (!reallocated.ok) return reallocated;
1803
+ }
1804
+ const ready = ensureProgram();
1805
+ if (!ready.ok) return ready;
1806
+ const previousFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
1807
+ try {
1808
+ gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
1809
+ gl.viewport(0, 0, target.width, target.height);
1810
+ gl.disable(gl.SCISSOR_TEST);
1811
+ gl.clearColor(0, 0, 0, 0);
1812
+ gl.clear(gl.COLOR_BUFFER_BIT);
1813
+ gl.enable(gl.BLEND);
1814
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
1815
+ gl.useProgram(ready.value.program);
1816
+ gl.bindVertexArray(ready.value.vao);
1817
+ applyUniform(ready.value, "u_time", input.time);
1818
+ applyUniform(ready.value, "u_delta", input.delta);
1819
+ applyUniform(ready.value, "u_seed", input.seed);
1820
+ applyUniform(ready.value, "u_lifetime", parameters.lifetimeSeconds);
1821
+ applyUniform(ready.value, "u_emission_rate", parameters.emissionRate);
1822
+ applyUniform(ready.value, "u_max_particles", parameters.maxParticles);
1823
+ applyUniform(ready.value, "u_position", parameters.position);
1824
+ applyUniform(ready.value, "u_direction", parameters.directionRadians);
1825
+ applyUniform(ready.value, "u_spread", parameters.spreadRadians);
1826
+ applyUniform(ready.value, "u_speed", speed);
1827
+ applyUniform(ready.value, "u_gravity", parameters.gravity);
1828
+ applyUniform(ready.value, "u_resolution", resolution);
1829
+ applyUniform(ready.value, "u_size", size);
1830
+ applyUniform(ready.value, "u_start_color", parameters.startColor);
1831
+ applyUniform(ready.value, "u_end_color", parameters.endColor);
1832
+ gl.drawArrays(gl.POINTS, 0, count);
1833
+ gl.bindVertexArray(null);
1834
+ return rendered;
1835
+ } finally {
1836
+ gl.bindFramebuffer(gl.FRAMEBUFFER, previousFramebuffer);
1837
+ }
1838
+ },
1839
+ invalidate() {
1840
+ destroyProgram(gl, program);
1841
+ program = null;
1842
+ discardTarget(target, true);
1843
+ },
1844
+ invalidateContextLoss() {
1845
+ program = null;
1846
+ discardTarget(target, false);
1847
+ },
1848
+ dispose() {
1849
+ if (!producerDisposed) {
1850
+ producerDisposed = true;
1851
+ destroyProgram(gl, program);
1852
+ program = null;
1853
+ discardTarget(target, true);
1854
+ targets.delete(target);
1855
+ invalidatables.delete(producer);
1856
+ }
1857
+ }
1858
+ };
1859
+ invalidatables.add(producer);
1860
+ return ok(producer);
1861
+ }
1862
+ function createGodotParticleProducer(pass, width, height) {
1863
+ const invalid = state();
1864
+ if (invalid) return invalid;
1865
+ if (!pass.config && !pass.scratch) return fail("UNSUPPORTED_PARTICLE_FEATURE", "A particle pass needs config or caller-owned scratch.", "config");
1866
+ if (pass.scratch && pass.config) return fail("UNSUPPORTED_PARTICLE_FEATURE", "When scratch is supplied, omit config: scratch is the semantic owner.", "scratch-config");
1867
+ const config = pass.scratch ? pass.scratch.config : normalizeParticleRenderConfig(pass.config);
1868
+ const sprite = pass.spriteTexture ? textureInputFor(pass.spriteTexture) : null;
1869
+ const lut = pass.lutTexture ? textureInputFor(pass.lutTexture) : null;
1870
+ const mask = pass.maskTexture ? textureInputFor(pass.maskTexture) : null;
1871
+ if (pass.spriteTexture && !sprite) return fail("FOREIGN_TARGET", "A textured particle system needs a live sprite target owned by this stage.", "sprite-texture");
1872
+ if (pass.lutTexture && !lut || pass.maskTexture && !mask) return fail("FOREIGN_TARGET", "Particle auxiliary textures must be live targets owned by this stage.", "auxiliary-texture");
1873
+ const unsupported = [
1874
+ "collision",
1875
+ "attractors",
1876
+ "trails",
1877
+ "customMaterial"
1878
+ ].find((feature) => pass.features?.[feature]);
1879
+ if (unsupported) return fail("UNSUPPORTED_PARTICLE_FEATURE", `Headless Godot particles do not support ${unsupported}.`, unsupported);
1880
+ if (config.emissionShape === 4 || config.emissionShape === 5) return fail("UNSUPPORTED_PARTICLE_FEATURE", "Point-list and directed-point emission require their authored point data.", "emission-shape");
1881
+ const scratch = pass.scratch ?? createGodotParticleScratch(config);
1882
+ const ownsScratch = !pass.scratch;
1883
+ if (scratch.instances.data.length / 10 < scratch.state.count) return fail("UNSUPPORTED_PARTICLE_FEATURE", "Scratch cannot hold the configured particle count.", "scratch-capacity");
1884
+ const target = makeTarget();
1885
+ const allocated = resizeTarget(target, width, height);
1886
+ if (!allocated.ok) {
1887
+ targets.delete(target);
1888
+ return allocated;
1889
+ }
1890
+ const semanticConfig = scratch.config;
1891
+ const packing = {
1892
+ state: scratch.state,
1893
+ config: semanticConfig,
1894
+ instances: scratch.instances,
1895
+ textureWidth: 0,
1896
+ textureHeight: 0,
1897
+ origin: void 0,
1898
+ transform: void 0,
1899
+ modulate: void 0
1900
+ };
1901
+ let requestedWidth = target.width, requestedHeight = target.height;
1902
+ let program = null;
1903
+ let quad = null;
1904
+ let producerDisposed = false;
1905
+ let lastTime = null;
1906
+ let preprocessed = false;
1907
+ const rendered = ok(target);
1908
+ const warmed = ok(void 0);
1909
+ const particleGl = {
1910
+ gl,
1911
+ quad: null
1912
+ };
1913
+ const drawOptions = {
1914
+ texture: sprite?.texture ?? null,
1915
+ textured: Boolean(sprite),
1916
+ lutTexture: lut?.texture ?? null,
1917
+ maskTexture: mask?.texture ?? null,
1918
+ hframes: semanticConfig.hframes,
1919
+ vframes: semanticConfig.vframes,
1920
+ blendMode: semanticConfig.blendMode,
1921
+ viewportW: target.width,
1922
+ viewportH: target.height,
1923
+ alphaFromRed: semanticConfig.alphaFromRed,
1924
+ erode: semanticConfig.alphaErode,
1925
+ uvPolar: semanticConfig.uvPolar,
1926
+ targetFramebuffer: target.framebuffer
1927
+ };
1928
+ const ensure = () => {
1929
+ if (program) return readyResult;
1930
+ const compiled = acquireParticleProgram(gl);
1931
+ if (!compiled) return fail("SHADER_COMPILE_FAILED", "WebGL could not compile the shared Godot particle renderer.");
1932
+ quad = gl.createBuffer();
1933
+ if (!quad) {
1934
+ releaseParticleProgram(gl);
1935
+ return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not allocate the particle resolve quad.");
1936
+ }
1937
+ gl.bindBuffer(gl.ARRAY_BUFFER, quad);
1938
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
1939
+ -1,
1940
+ -1,
1941
+ 1,
1942
+ -1,
1943
+ -1,
1944
+ 1,
1945
+ 1,
1946
+ 1
1947
+ ]), gl.STATIC_DRAW);
1948
+ program = compiled;
1949
+ particleGl.quad = quad;
1950
+ readyResult = ok(compiled);
1951
+ return readyResult;
1952
+ };
1953
+ let readyResult = null;
1954
+ const dependencyLive = (value) => !(value && (value.owned ? !targets.has(value.owned) || !value.owned.texture || !value.owned.framebuffer : typeof gl.isTexture === "function" && !gl.isTexture(value.texture)));
1955
+ const reset = () => {
1956
+ const s = scratch.state;
1957
+ s.time = 0;
1958
+ s.cycle = 0;
1959
+ s.remainder = 0;
1960
+ s.emitting = true;
1961
+ for (let index = 0; index < s.particles.length; index += 1) s.particles[index].active = false;
1962
+ };
1963
+ const preprocess = () => {
1964
+ if (preprocessed) return;
1965
+ preprocessParticles(scratch.state);
1966
+ preprocessed = true;
1967
+ };
1968
+ const producer = {
1969
+ target,
1970
+ scratch,
1971
+ warmUp() {
1972
+ const current = state();
1973
+ if (current) return current;
1974
+ if (producerDisposed) return fail("DISPOSED", "The Godot particle producer has been disposed.");
1975
+ const ready = ensure();
1976
+ if (!ready.ok) return ready;
1977
+ if (!prepareParticleDraw(gl, ready.value, drawOptions) || false) return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not prepare particle draw resources.");
1978
+ return warmed;
1979
+ },
1980
+ resize(nextWidth, nextHeight) {
1981
+ if (producerDisposed) return fail("DISPOSED", "The Godot particle producer has been disposed.");
1982
+ const resized = resizeTarget(target, nextWidth, nextHeight);
1983
+ if (resized.ok) {
1984
+ requestedWidth = target.width;
1985
+ requestedHeight = target.height;
1986
+ drawOptions.viewportW = target.width;
1987
+ drawOptions.viewportH = target.height;
1988
+ drawOptions.targetFramebuffer = target.framebuffer;
1989
+ }
1990
+ return resized;
1991
+ },
1992
+ render(input) {
1993
+ if (input.origin !== void 0 && (input.origin.length !== 2 || !input.origin.every(Number.isFinite)) || !Number.isFinite(input.time) || !Number.isFinite(input.delta) || input.time < 0 || input.delta < 0) return fail("INVALID_RENDER_INPUT", "Particle time and delta must be finite and non-negative.", "render-input");
1994
+ if (!input.restart && lastTime !== null) {
1995
+ if (input.time < lastTime) return fail("INVALID_RENDER_INPUT", "Particle time must be monotonic unless restart is set.", "time");
1996
+ if (Math.abs(input.delta - (input.time - lastTime)) > 1e-6) return fail("INVALID_RENDER_INPUT", "Particle delta must reconcile with caller time.", "time-delta");
1997
+ }
1998
+ const current = state();
1999
+ if (current) return current;
2000
+ if (producerDisposed) return fail("DISPOSED", "The Godot particle producer has been disposed.");
2001
+ if (!target.texture || !target.framebuffer) {
2002
+ const resized = resizeTarget(target, requestedWidth, requestedHeight);
2003
+ if (!resized.ok) return resized;
2004
+ }
2005
+ if (!dependencyLive(sprite)) return fail("FOREIGN_TARGET", "Particle sprite texture is no longer live.", "sprite-texture");
2006
+ if (!dependencyLive(lut)) return fail("FOREIGN_TARGET", "Particle LUT texture is no longer live.", "lut-texture");
2007
+ if (!dependencyLive(mask)) return fail("FOREIGN_TARGET", "Particle mask texture is no longer live.", "mask-texture");
2008
+ const ready = ensure();
2009
+ if (!ready.ok) return ready;
2010
+ drawOptions.viewportW = target.width;
2011
+ drawOptions.viewportH = target.height;
2012
+ drawOptions.targetFramebuffer = target.framebuffer;
2013
+ if (!prepareParticleDraw(gl, ready.value, drawOptions)) return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not allocate the additive particle target.");
2014
+ if (input.restart) {
2015
+ reset();
2016
+ preprocessed = false;
2017
+ }
2018
+ preprocess();
2019
+ scratch.state.emitting = input.emitting;
2020
+ simulateParticles(scratch.state, input.delta);
2021
+ lastTime = input.time;
2022
+ const buffer = scratch.instances;
2023
+ packing.textureWidth = semanticConfig.textureWidth > 0 ? semanticConfig.textureWidth : sprite?.width ?? 16;
2024
+ packing.textureHeight = semanticConfig.textureHeight > 0 ? semanticConfig.textureHeight : sprite?.height ?? 16;
2025
+ packing.origin = input.origin;
2026
+ packParticleInstances(packing);
2027
+ const previous = gl.getParameter(gl.FRAMEBUFFER_BINDING);
2028
+ try {
2029
+ gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
2030
+ gl.viewport(0, 0, target.width, target.height);
2031
+ gl.disable(gl.SCISSOR_TEST);
2032
+ gl.clearColor(0, 0, 0, 0);
2033
+ gl.clear(gl.COLOR_BUFFER_BIT);
2034
+ if (buffer.count === 0) return rendered;
2035
+ if (!drawParticles(particleGl, ready.value, buffer, drawOptions)) return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not allocate the additive particle target.");
2036
+ return rendered;
2037
+ } finally {
2038
+ gl.bindFramebuffer(gl.FRAMEBUFFER, previous);
2039
+ }
2040
+ },
2041
+ invalidate() {
2042
+ if (program) releaseParticleProgram(gl);
2043
+ program = null;
2044
+ if (quad) gl.deleteBuffer(quad);
2045
+ quad = null;
2046
+ if (ownsScratch) disposeParticleInstanceBuffer(gl, scratch.instances);
2047
+ discardTarget(target, true);
2048
+ },
2049
+ invalidateContextLoss() {
2050
+ if (program) releaseParticleProgram(gl, true);
2051
+ program = null;
2052
+ quad = null;
2053
+ invalidateParticleInstanceBuffer(gl, scratch.instances);
2054
+ discardTarget(target, false);
2055
+ },
2056
+ dispose() {
2057
+ if (!producerDisposed) {
2058
+ producerDisposed = true;
2059
+ producer.invalidate();
2060
+ targets.delete(target);
2061
+ invalidatables.delete(producer);
2062
+ }
2063
+ }
2064
+ };
2065
+ invalidatables.add(producer);
2066
+ return ok(producer);
2067
+ }
2068
+ function createGodotParticleDirectPass(pass) {
2069
+ const invalid = state();
2070
+ if (invalid) return invalid;
2071
+ if (!pass.config && !pass.scratch) return fail("UNSUPPORTED_PARTICLE_FEATURE", "A particle pass needs config or caller-owned scratch.", "config");
2072
+ if (pass.scratch && pass.config) return fail("UNSUPPORTED_PARTICLE_FEATURE", "When scratch is supplied, omit config: scratch is the semantic owner.", "scratch-config");
2073
+ const config = pass.scratch ? pass.scratch.config : normalizeParticleRenderConfig(pass.config);
2074
+ const sprite = pass.spriteTexture ? textureInputFor(pass.spriteTexture) : null;
2075
+ const lut = pass.lutTexture ? textureInputFor(pass.lutTexture) : null;
2076
+ const mask = pass.maskTexture ? textureInputFor(pass.maskTexture) : null;
2077
+ if (pass.spriteTexture && !sprite) return fail("FOREIGN_TARGET", "A textured particle system needs a live sprite target owned by this stage.", "sprite-texture");
2078
+ if (pass.lutTexture && !lut || pass.maskTexture && !mask) return fail("FOREIGN_TARGET", "Particle auxiliary textures must be live targets owned by this stage.", "auxiliary-texture");
2079
+ const unsupported = [
2080
+ "collision",
2081
+ "attractors",
2082
+ "trails",
2083
+ "customMaterial"
2084
+ ].find((feature) => pass.features?.[feature]);
2085
+ if (unsupported) return fail("UNSUPPORTED_PARTICLE_FEATURE", `Headless Godot particles do not support ${unsupported}.`, unsupported);
2086
+ if (config.emissionShape === 4 || config.emissionShape === 5) return fail("UNSUPPORTED_PARTICLE_FEATURE", "Point-list and directed-point emission require their authored point data.", "emission-shape");
2087
+ const scratch = pass.scratch ?? createGodotParticleScratch(config);
2088
+ const ownsScratch = !pass.scratch;
2089
+ if (scratch.instances.data.length / 10 < scratch.state.count) return fail("UNSUPPORTED_PARTICLE_FEATURE", "Scratch cannot hold the configured particle count.", "scratch-capacity");
2090
+ const semanticConfig = scratch.config;
2091
+ const packing = {
2092
+ state: scratch.state,
2093
+ config: semanticConfig,
2094
+ instances: scratch.instances,
2095
+ textureWidth: 0,
2096
+ textureHeight: 0,
2097
+ origin: void 0,
2098
+ transform: void 0,
2099
+ modulate: void 0
2100
+ };
2101
+ let program = null;
2102
+ let quad = null;
2103
+ let disposedProducer = false;
2104
+ let preprocessed = false;
2105
+ let lastTime = null;
2106
+ const particleGl = {
2107
+ gl,
2108
+ quad: null
2109
+ };
2110
+ const drawOptions = {
2111
+ texture: sprite?.texture ?? null,
2112
+ textured: Boolean(sprite),
2113
+ lutTexture: lut?.texture ?? null,
2114
+ maskTexture: mask?.texture ?? null,
2115
+ hframes: semanticConfig.hframes,
2116
+ vframes: semanticConfig.vframes,
2117
+ blendMode: semanticConfig.blendMode,
2118
+ viewportW: 1,
2119
+ viewportH: 1,
2120
+ alphaFromRed: semanticConfig.alphaFromRed,
2121
+ erode: semanticConfig.alphaErode,
2122
+ uvPolar: semanticConfig.uvPolar,
2123
+ targetFramebuffer: null,
2124
+ additiveResolveIntoExisting: true
2125
+ };
2126
+ let readyResult = null;
2127
+ const ensure = () => {
2128
+ if (program) return readyResult;
2129
+ const compiled = acquireParticleProgram(gl);
2130
+ if (!compiled) return fail("SHADER_COMPILE_FAILED", "WebGL could not compile the shared Godot particle renderer.");
2131
+ quad = gl.createBuffer();
2132
+ if (!quad) {
2133
+ releaseParticleProgram(gl);
2134
+ return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not allocate the particle resolve quad.");
2135
+ }
2136
+ gl.bindBuffer(gl.ARRAY_BUFFER, quad);
2137
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
2138
+ -1,
2139
+ -1,
2140
+ 1,
2141
+ -1,
2142
+ -1,
2143
+ 1,
2144
+ 1,
2145
+ 1
2146
+ ]), gl.STATIC_DRAW);
2147
+ program = compiled;
2148
+ particleGl.quad = quad;
2149
+ readyResult = ok(compiled);
2150
+ return readyResult;
2151
+ };
2152
+ const dependencyLive = (value) => !(value && (value.owned ? !targets.has(value.owned) || !value.owned.texture || !value.owned.framebuffer : typeof gl.isTexture === "function" && !gl.isTexture(value.texture)));
2153
+ const reset = () => {
2154
+ const state = scratch.state;
2155
+ state.time = 0;
2156
+ state.cycle = 0;
2157
+ state.remainder = 0;
2158
+ state.emitting = true;
2159
+ for (const particle of state.particles) particle.active = false;
2160
+ };
2161
+ const preprocess = () => {
2162
+ if (preprocessed) return;
2163
+ preprocessParticles(scratch.state);
2164
+ preprocessed = true;
2165
+ };
2166
+ const producer = {
2167
+ scratch,
2168
+ warmUp() {
2169
+ const current = state();
2170
+ if (current) return current;
2171
+ if (disposedProducer) return fail("DISPOSED", "The Godot particle pass has been disposed.");
2172
+ const ready = ensure();
2173
+ if (!ready.ok) return ready;
2174
+ if (!prepareParticleDraw(gl, ready.value, drawOptions) || false) return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not prepare particle draw resources.");
2175
+ preprocess();
2176
+ return ok(void 0);
2177
+ },
2178
+ draw(input, context) {
2179
+ if (input.origin !== void 0 && (input.origin.length !== 2 || !input.origin.every(Number.isFinite)) || !Number.isFinite(input.time) || !Number.isFinite(input.delta) || input.time < 0 || input.delta < 0 || !Number.isFinite(context.width) || !Number.isFinite(context.height) || context.width <= 0 || context.height <= 0) return fail("INVALID_RENDER_INPUT", "Particle time, delta, and target dimensions must be finite and positive.", "render-input");
2180
+ const transform = directParticleTransform(input.transform);
2181
+ if (!transform) return fail("UNSUPPORTED_PARTICLE_FEATURE", "Direct particles require a finite uniform-scale rotation and translation transform.", "transform");
2182
+ const modulate = directParticleModulate(input.modulate);
2183
+ if (!modulate) return fail("INVALID_RENDER_INPUT", "Direct particle modulation components must be finite.", "modulate");
2184
+ if (!input.restart && lastTime !== null) {
2185
+ if (input.time < lastTime) return fail("INVALID_RENDER_INPUT", "Particle time must be monotonic unless restart is set.", "time");
2186
+ if (Math.abs(input.delta - (input.time - lastTime)) > 1e-6) return fail("INVALID_RENDER_INPUT", "Particle delta must reconcile with caller time.", "time-delta");
2187
+ }
2188
+ const current = state();
2189
+ if (current) return current;
2190
+ if (disposedProducer) return fail("DISPOSED", "The Godot particle pass has been disposed.");
2191
+ if (!dependencyLive(sprite)) return fail("FOREIGN_TARGET", "Particle sprite texture is no longer live.", "sprite-texture");
2192
+ if (!dependencyLive(lut)) return fail("FOREIGN_TARGET", "Particle LUT texture is no longer live.", "lut-texture");
2193
+ if (!dependencyLive(mask)) return fail("FOREIGN_TARGET", "Particle mask texture is no longer live.", "mask-texture");
2194
+ const ready = ensure();
2195
+ if (!ready.ok) return ready;
2196
+ drawOptions.viewportW = Math.floor(context.width);
2197
+ drawOptions.viewportH = Math.floor(context.height);
2198
+ drawOptions.targetFramebuffer = context.framebuffer;
2199
+ if (!prepareParticleDraw(gl, ready.value, drawOptions)) return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not allocate the shared additive particle target.");
2200
+ if (input.restart) {
2201
+ reset();
2202
+ preprocessed = false;
2203
+ }
2204
+ preprocess();
2205
+ scratch.state.emitting = input.emitting;
2206
+ simulateParticles(scratch.state, input.delta);
2207
+ lastTime = input.time;
2208
+ const buffer = scratch.instances;
2209
+ packing.textureWidth = semanticConfig.textureWidth > 0 ? semanticConfig.textureWidth : sprite?.width ?? 16;
2210
+ packing.textureHeight = semanticConfig.textureHeight > 0 ? semanticConfig.textureHeight : sprite?.height ?? 16;
2211
+ packing.origin = input.origin;
2212
+ packing.transform = transform;
2213
+ packing.modulate = modulate;
2214
+ packParticleInstances(packing);
2215
+ if (buffer.count === 0) return ok(void 0);
2216
+ gl.bindFramebuffer(gl.FRAMEBUFFER, context.framebuffer);
2217
+ gl.viewport(0, 0, drawOptions.viewportW, drawOptions.viewportH);
2218
+ return drawParticles(particleGl, ready.value, buffer, drawOptions) ? ok(void 0) : fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not draw the shared particle pass.");
2219
+ },
2220
+ invalidate() {
2221
+ if (program) releaseParticleProgram(gl);
2222
+ program = null;
2223
+ readyResult = null;
2224
+ if (quad) gl.deleteBuffer(quad);
2225
+ quad = null;
2226
+ particleGl.quad = null;
2227
+ if (ownsScratch) disposeParticleInstanceBuffer(gl, scratch.instances);
2228
+ },
2229
+ invalidateContextLoss() {
2230
+ if (program) releaseParticleProgram(gl, true);
2231
+ program = null;
2232
+ readyResult = null;
2233
+ quad = null;
2234
+ particleGl.quad = null;
2235
+ invalidateParticleInstanceBuffer(gl, scratch.instances);
2236
+ },
2237
+ dispose() {
2238
+ if (disposedProducer) return;
2239
+ disposedProducer = true;
2240
+ producer.invalidate();
2241
+ invalidatables.delete(producer);
2242
+ }
2243
+ };
2244
+ invalidatables.add(producer);
2245
+ return ok(producer);
2246
+ }
2247
+ return {
2248
+ gl,
2249
+ createShaderProducer,
2250
+ createGodotShaderProducer,
2251
+ createParticleProducer,
2252
+ createGodotParticleProducer,
2253
+ createGodotParticleDirectPass,
2254
+ executeScreenSample(command) {
2255
+ const current = state();
2256
+ if (current) return current;
2257
+ const source = targetFor(command.source);
2258
+ const destination = targetFor(command.destination);
2259
+ if (!source?.texture || !source.framebuffer || !destination?.texture || !destination.framebuffer) return fail("FOREIGN_TARGET", "Screen sample commands require live targets owned by this headless effects stage.");
2260
+ if (source === destination) {
2261
+ const captured = snapshotFor(source);
2262
+ if (!captured.ok) return captured;
2263
+ copy(captured.value, destination, command.filter ?? "nearest");
2264
+ } else copy(source, destination, command.filter ?? "nearest");
2265
+ return ok(void 0);
2266
+ },
2267
+ invalidate() {
2268
+ for (const item of [...invalidatables]) item.invalidate();
2269
+ if (snapshot) discardTarget(snapshot, true);
2270
+ if (opaqueWhite) discardTarget(opaqueWhite, true);
2271
+ opaqueWhiteFilled = false;
2272
+ opaqueWhiteResult = null;
2273
+ },
2274
+ invalidateContextLoss() {
2275
+ for (const item of [...invalidatables]) item.invalidateContextLoss();
2276
+ if (snapshot) discardTarget(snapshot, false);
2277
+ if (opaqueWhite) discardTarget(opaqueWhite, false);
2278
+ opaqueWhiteFilled = false;
2279
+ opaqueWhiteResult = null;
2280
+ },
2281
+ dispose() {
2282
+ if (disposed) return;
2283
+ disposed = true;
2284
+ for (const item of [...invalidatables]) item.dispose();
2285
+ if (snapshot) discardTarget(snapshot, true);
2286
+ if (opaqueWhite) discardTarget(opaqueWhite, true);
2287
+ snapshot = null;
2288
+ opaqueWhite = null;
2289
+ opaqueWhiteFilled = false;
2290
+ opaqueWhiteResult = null;
2291
+ targets.clear();
2292
+ }
2293
+ };
2294
+ }
2295
+ //#endregion
2296
+ export { MAX_HEADLESS_PARTICLES, blendFactorsFor, clearWebglSurface, compileProgram, compileProgramAsync, createGodotParticleScratch, createHeadlessEffectsStage, createWebglFullscreenQuad, createWebglPlaceholderTexture, createWebglTexture, deleteWebglTexture, disposeParticleInstanceBuffer, drawGodotWebglShaderFrame, drawParticles, getParticleProgram, invalidateParticleInstanceBuffer, prepareParticleDraw, startProgram, uploadGodotShaderUniform, uploadWebglTexture };
2297
+
2298
+ //# sourceMappingURL=webgl.mjs.map