@godot-scene-web/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.
@@ -0,0 +1,1664 @@
1
+ //#region src/shaders/godot-shader.ts
2
+ var UnsupportedShaderError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "UnsupportedShaderError";
6
+ }
7
+ };
8
+ async function expandGodotShaderIncludes(source, resolveInclude, seen = /* @__PURE__ */ new Set(), depth = 0) {
9
+ if (depth > 16) throw new UnsupportedShaderError("shader include depth exceeded");
10
+ const includePattern = /^[ \t]*#include\s+"([^"]+)"[ \t]*$/gm;
11
+ const chunks = [];
12
+ let lastIndex = 0;
13
+ for (const match of source.matchAll(includePattern)) {
14
+ const index = match.index ?? 0;
15
+ chunks.push(source.slice(lastIndex, index));
16
+ const includePath = match[1];
17
+ if (seen.has(includePath)) chunks.push(`\n/* skipped recursive include ${includePath} */\n`);
18
+ else {
19
+ const included = await resolveInclude(includePath);
20
+ if (included === void 0) chunks.push(match[0]);
21
+ else {
22
+ const nextSeen = new Set(seen);
23
+ nextSeen.add(includePath);
24
+ chunks.push(await expandGodotShaderIncludes(included, resolveInclude, nextSeen, depth + 1));
25
+ }
26
+ }
27
+ lastIndex = index + match[0].length;
28
+ }
29
+ chunks.push(source.slice(lastIndex));
30
+ return chunks.join("");
31
+ }
32
+ const UNSUPPORTED_BUILTINS = [
33
+ "FRAGCOORD",
34
+ "NORMAL",
35
+ "NORMAL_TEXTURE",
36
+ "POINT_COORD",
37
+ "VERTEX",
38
+ "INSTANCE_ID",
39
+ "INSTANCE_CUSTOM",
40
+ "SPECULAR_SHININESS",
41
+ "LIGHT",
42
+ "LIGHT_COLOR",
43
+ "AT_LIGHT_PASS",
44
+ "CUSTOM0",
45
+ "CUSTOM1"
46
+ ];
47
+ const GLSL_SAMPLER_TYPES = new Set(["sampler2D"]);
48
+ const SCALAR_OR_VECTOR = /^(float|int|bool|vec2|vec3|vec4|mat2|mat3|mat4|ivec2|ivec3|ivec4|bvec2|bvec3|bvec4)$/;
49
+ /** Transpile a `.gdshader` source string. Throws `UnsupportedShaderError`. */
50
+ function transpileGodotShader(source) {
51
+ const parsed = parseShader(sanitizeReservedIdentifiers(stripComments(unwrapShaderResource(source))));
52
+ rejectUnsupported(shaderLogic(parsed), parsed);
53
+ const analysis = analyzeShader(parsed);
54
+ return {
55
+ vertexGlsl: VERTEX_GLSL,
56
+ fragmentGlsl: assembleFragment({
57
+ parsed,
58
+ analysis,
59
+ varyingLocals: analysis.varyingHoists.map((v) => `${v.type} ${v.name} = ${v.expr};`)
60
+ }),
61
+ uniforms: parsed.uniforms,
62
+ samplers: parsed.samplers,
63
+ blend: parsed.blend,
64
+ usesTime: analysis.usesTime,
65
+ usesTexturePixelSize: analysis.usesTexturePixelSize,
66
+ usesScreenUv: analysis.usesScreenUv,
67
+ usesScreenTexture: analysis.usesScreenTexture,
68
+ usesScreenPixelSize: analysis.usesScreenPixelSize
69
+ };
70
+ }
71
+ const VERTEX_GLSL = `#version 300 es
72
+ in vec2 a_pos;
73
+ out vec2 v_uv;
74
+ void main() {
75
+ v_uv = a_pos * 0.5 + 0.5;
76
+ gl_Position = vec4(a_pos, 0.0, 1.0);
77
+ }
78
+ `;
79
+ function assembleFragment(input) {
80
+ const { parsed, analysis } = input;
81
+ const decls = [
82
+ "uniform sampler2D TEXTURE;",
83
+ "uniform vec2 _godot_uv_fit;",
84
+ "uniform vec4 _godot_uv_window;",
85
+ ...analysis.usesTime ? ["uniform float TIME;"] : [],
86
+ ...analysis.usesTexturePixelSize ? ["uniform vec2 TEXTURE_PIXEL_SIZE;"] : [],
87
+ ...analysis.needsModulate ? ["uniform vec4 MODULATE;"] : [],
88
+ ...analysis.usesScreenUv ? ["uniform vec2 _godot_screen_origin;", "uniform vec2 _godot_screen_size;"] : [],
89
+ ...analysis.usesScreenTexture ? ["uniform sampler2D SCREEN_TEXTURE;", ...parsed.screenTextureNames.filter((name) => name !== "SCREEN_TEXTURE").map((name) => `#define ${name} SCREEN_TEXTURE`)] : [],
90
+ ...analysis.usesScreenPixelSize ? ["uniform vec2 SCREEN_PIXEL_SIZE;"] : [],
91
+ ...analysis.usesPi ? ["const float PI = 3.141592653589793;"] : []
92
+ ];
93
+ for (const u of parsed.uniforms) decls.push(`uniform ${u.type} ${u.name}${u.arrayLength ? `[${u.arrayLength}]` : ""};`);
94
+ for (const s of parsed.samplers) decls.push(`uniform sampler2D ${s.name};`);
95
+ const helpers = promoteIntLiterals(parsed.helpers, parsed.intIdentifiers).trim();
96
+ const body = promoteIntLiterals(parsed.fragmentBody, parsed.intIdentifiers);
97
+ return `#version 300 es
98
+ precision highp float;
99
+ in vec2 v_uv;
100
+ out vec4 fragColor;
101
+ ${decls.join("\n")}
102
+ ${helpers ? `${helpers}\n` : ""}
103
+ void main() {
104
+ vec2 GODOT_UV = _godot_uv_window.xy + vec2(v_uv.x, 1.0 - v_uv.y) * _godot_uv_window.zw;
105
+ vec2 UV = (GODOT_UV - 0.5) / _godot_uv_fit + 0.5;
106
+ ${analysis.usesScreenUv ? " vec2 SCREEN_UV = _godot_screen_origin + GODOT_UV * _godot_screen_size;\n" : ""} vec4 COLOR = ${analysis.opaqueColor ? "vec4(texture(TEXTURE, UV).rgb, 1.0)" : "texture(TEXTURE, UV)"};
107
+ ${input.varyingLocals.map((l) => ` ${l}`).join("\n")}
108
+ ${indent(body)}
109
+ ${analysis.autoModulate ? " COLOR *= MODULATE;" : ""}
110
+ // PREMULTIPLIED. The shared canvas declares \`premultipliedAlpha: true\` (webgl/shared-gl.ts), so
111
+ // this is THE canvas contract: return rgb*a with a. The shader backend draws with BLEND OFF, so
112
+ // whatever this writes IS the buffer — returning straight COLOR here halos every partially
113
+ // transparent node, and neither the compiler nor a readback of the canvas would say so.
114
+ // Character-for-character the WGSL emitter's return (webgpu/transpile-wgsl.ts), on purpose: one
115
+ // contract, two languages.
116
+ fragColor = vec4(COLOR.rgb * COLOR.a, COLOR.a);
117
+ }
118
+ `;
119
+ }
120
+ /** The text every built-in flag is scanned over. The vertex body and the top-level
121
+ * helpers count, not just `fragment()`: a helper may read TIME/PI, and a varying
122
+ * computed in `vertex()` is hoisted into the fragment. `rejectUnsupported` must be
123
+ * given this same region, or a guard would scan less text than the flags did. */
124
+ function shaderLogic(parsed) {
125
+ return `${parsed.helpers}\n${parsed.vertexBody ?? ""}\n${parsed.fragmentBody}`;
126
+ }
127
+ /** Derive the emitter-neutral facts about a parsed shader (see `ShaderAnalysis`).
128
+ * Pure, and deliberately NOT a validator: run `rejectUnsupported(analysis.logic,
129
+ * parsed)` for that, before analyzing, so an unsupported built-in outranks a
130
+ * varying-hoist complaint. Throws `UnsupportedShaderError` only for a `vertex()`
131
+ * body that cannot be reduced to constant varying assignments. */
132
+ function analyzeShader(parsed) {
133
+ const logic = shaderLogic(parsed);
134
+ const usesTime = hasToken(logic, "TIME");
135
+ const usesTexturePixelSize = hasToken(logic, "TEXTURE_PIXEL_SIZE");
136
+ const usesScreenUv = hasToken(logic, "SCREEN_UV");
137
+ const usesScreenTexture = hasToken(logic, "SCREEN_TEXTURE") || parsed.screenTextureNames.some((name) => hasToken(logic, name));
138
+ const usesScreenPixelSize = hasToken(logic, "SCREEN_PIXEL_SIZE");
139
+ const usesPi = hasToken(logic, "PI");
140
+ const usesModulateBuiltin = hasToken(parsed.fragmentBody, "MODULATE");
141
+ const vertexReadsModulate = parsed.vertexBody !== null && hasToken(parsed.vertexBody, "COLOR");
142
+ const overwritesColorRgb = /\bCOLOR(\.rgb)?\s*=(?!=)/.test(parsed.fragmentBody);
143
+ const bodyReadsColor = hasToken(parsed.fragmentBody.replace(/\bCOLOR(?:\.[rgba]+)?\s*=(?!=)/g, ""), "COLOR");
144
+ const pureFillOverwrite = overwritesColorRgb && !bodyReadsColor;
145
+ const autoModulate = !usesModulateBuiltin && !vertexReadsModulate && !pureFillOverwrite;
146
+ const usesColorAlpha = /\bCOLOR\.a\b/.test(parsed.fragmentBody);
147
+ const samplesNodeTexture = hasToken(parsed.fragmentBody, "TEXTURE");
148
+ const opaqueColor = pureFillOverwrite && !usesColorAlpha && !samplesNodeTexture;
149
+ const varyingHoists = buildVaryingHoists(parsed);
150
+ return {
151
+ logic,
152
+ usesTime,
153
+ usesTexturePixelSize,
154
+ usesScreenUv,
155
+ usesScreenTexture,
156
+ usesScreenPixelSize,
157
+ usesPi,
158
+ needsModulate: usesModulateBuiltin || autoModulate || varyingHoists.length > 0,
159
+ autoModulate,
160
+ opaqueColor,
161
+ varyingHoists
162
+ };
163
+ }
164
+ function buildVaryingHoists(parsed) {
165
+ if (parsed.vertexBody === null || parsed.varyings.length === 0) return [];
166
+ const typeByName = new Map(parsed.varyings.map((v) => [v.name, v.type]));
167
+ const hoists = [];
168
+ const statements = parsed.vertexBody.split(";").map((s) => s.trim()).filter(Boolean);
169
+ for (const statement of statements) {
170
+ const eq = statement.indexOf("=");
171
+ if (eq < 0) throw new UnsupportedShaderError(`vertex(): only constant varying assignments are supported, got "${statement}"`);
172
+ const lhs = statement.slice(0, eq).trim();
173
+ const rhs = statement.slice(eq + 1).trim();
174
+ const type = typeByName.get(lhs);
175
+ if (!type) throw new UnsupportedShaderError(`vertex(): assignment to non-varying "${lhs}" is not supported`);
176
+ const expr = replaceToken(promoteIntLiterals(rhs), "COLOR", "MODULATE");
177
+ hoists.push({
178
+ type,
179
+ name: lhs,
180
+ expr
181
+ });
182
+ }
183
+ return hoists;
184
+ }
185
+ /** Parse cleaned Godot source (run `stripComments` + `sanitizeReservedIdentifiers`
186
+ * first) into the emitter-neutral `ParsedShader`. Throws `UnsupportedShaderError`. */
187
+ function parseShader(src) {
188
+ const shaderType = /shader_type\s+([a-z_]+)\s*;/.exec(src);
189
+ if (!shaderType) throw new UnsupportedShaderError("missing shader_type declaration");
190
+ if (shaderType[1] !== "canvas_item") throw new UnsupportedShaderError(`unsupported shader_type "${shaderType[1]}" (only canvas_item)`);
191
+ const blend = parseBlendMode(src);
192
+ const { uniforms, samplers, screenTextureNames } = parseUniforms(src);
193
+ const varyings = parseVaryings(src);
194
+ const fragment = extractFunction(src, "fragment");
195
+ if (fragment === null) throw new UnsupportedShaderError("missing fragment() function");
196
+ const vertex = extractFunction(src, "vertex");
197
+ let helpers = src.replace(/shader_type\s+[a-z_]+\s*;/g, "").replace(/render_mode[^;]*;/g, "").replace(/uniform[^;]*;/g, "").replace(/varying[^;]*;/g, "");
198
+ helpers = removeFunction(helpers, "fragment");
199
+ helpers = removeFunction(helpers, "vertex");
200
+ const stray = RESOURCE_SECTION_RE.exec(helpers);
201
+ if (stray) throw new UnsupportedShaderError(`source is a Godot resource container, not shader code (found "${stray[0]}")`);
202
+ return {
203
+ blend,
204
+ uniforms,
205
+ samplers,
206
+ screenTextureNames,
207
+ varyings,
208
+ vertexBody: vertex,
209
+ fragmentBody: fragment,
210
+ helpers: helpers.trim(),
211
+ intIdentifiers: parseIntIdentifiers(src, uniforms)
212
+ };
213
+ }
214
+ function parseBlendMode(src) {
215
+ const match = /render_mode\s+([^;]+);/.exec(src);
216
+ if (!match) return "mix";
217
+ const modes = match[1].split(",").map((m) => m.trim());
218
+ for (const mode of modes) {
219
+ if (mode === "blend_add") return "add";
220
+ if (mode === "blend_sub") return "sub";
221
+ if (mode === "blend_mul") return "mul";
222
+ if (mode === "blend_premul_alpha") return "premul_alpha";
223
+ if (mode === "blend_mix") return "mix";
224
+ }
225
+ return "mix";
226
+ }
227
+ function parseUniforms(src) {
228
+ const uniforms = [];
229
+ const samplers = [];
230
+ const screenTextureNames = [];
231
+ for (const match of src.matchAll(/uniform\s+([a-zA-Z0-9_]+)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*([^;]*);/g)) {
232
+ const type = match[1];
233
+ const name = match[2];
234
+ const rest = match[3].trim();
235
+ const arrayLength = parseArrayLength(rest, src);
236
+ if (GLSL_SAMPLER_TYPES.has(type)) {
237
+ if (/\bhint_screen_texture\b/.test(rest)) {
238
+ screenTextureNames.push(name);
239
+ continue;
240
+ }
241
+ samplers.push({
242
+ name,
243
+ repeat: /\brepeat_enable\b/.test(rest)
244
+ });
245
+ continue;
246
+ }
247
+ if (!SCALAR_OR_VECTOR.test(type)) throw new UnsupportedShaderError(`unsupported uniform type "${type}"`);
248
+ uniforms.push({
249
+ type,
250
+ name,
251
+ ...arrayLength ? { arrayLength } : {},
252
+ default: parseUniformDefault(rest)
253
+ });
254
+ }
255
+ return {
256
+ uniforms,
257
+ samplers,
258
+ screenTextureNames
259
+ };
260
+ }
261
+ function parseArrayLength(rest, src) {
262
+ const match = /^\[\s*([^\]]+)\s*\]/.exec(rest);
263
+ if (!match) return;
264
+ const constants = parseConstInts(src);
265
+ const expr = match[1].replace(/[A-Za-z_][A-Za-z0-9_]*/g, (name) => constants.has(name) ? String(constants.get(name)) : "NaN");
266
+ if (!/^[0-9+\-*/ ().NaN]+$/.test(expr)) return;
267
+ try {
268
+ const value = Function(`"use strict"; return (${expr});`)();
269
+ return Number.isFinite(value) && value > 0 ? Math.round(value) : void 0;
270
+ } catch {
271
+ return;
272
+ }
273
+ }
274
+ function parseConstInts(src) {
275
+ const out = /* @__PURE__ */ new Map();
276
+ for (const match of src.matchAll(/\bconst\s+int\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(-?\d+)\s*;/g)) out.set(match[1], Number.parseInt(match[2], 10));
277
+ return out;
278
+ }
279
+ function parseIntIdentifiers(src, uniforms) {
280
+ const out = new Set(uniforms.filter((u) => u.type === "int").map((u) => u.name));
281
+ for (const match of src.matchAll(/\b(?:const\s+)?int\s+([A-Za-z_][A-Za-z0-9_]*)/g)) out.add(match[1]);
282
+ return out;
283
+ }
284
+ function parseUniformDefault(rest) {
285
+ const eq = rest.indexOf("=");
286
+ if (eq < 0) return;
287
+ const value = rest.slice(eq + 1).trim();
288
+ const ctor = /^[a-z0-9]*vec[234]\s*\(([^)]*)\)$/.exec(value);
289
+ if (ctor) return ctor[1].split(",").map((c) => scalarLiteral(c.trim()) ?? NaN);
290
+ return scalarLiteral(value);
291
+ }
292
+ function scalarLiteral(value) {
293
+ if (value === "true") return 1;
294
+ if (value === "false") return 0;
295
+ const num = Number.parseFloat(value);
296
+ return Number.isFinite(num) ? num : void 0;
297
+ }
298
+ function parseVaryings(src) {
299
+ const out = [];
300
+ for (const match of src.matchAll(/varying\s+([a-zA-Z0-9_]+)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*;/g)) out.push({
301
+ type: match[1],
302
+ name: match[2]
303
+ });
304
+ return out;
305
+ }
306
+ /** Throw `UnsupportedShaderError` for constructs no emitter supports. Give it
307
+ * `shaderLogic(parsed)` — the same region the analysis flags are scanned over. */
308
+ function rejectUnsupported(logic, parsed) {
309
+ for (const builtin of UNSUPPORTED_BUILTINS) if (hasToken(logic, builtin)) throw new UnsupportedShaderError(`unsupported built-in "${builtin}"`);
310
+ if (/\bwhile\b/.test(logic)) throw new UnsupportedShaderError("while loops are not supported");
311
+ if (/\b(uint|uvec[234])\b/.test(logic)) throw new UnsupportedShaderError("unsigned integer variables are not supported");
312
+ }
313
+ /** The escapes Godot writes into a `.tres` string literal. */
314
+ function unescapeResourceString(text) {
315
+ return text.replace(/\\(u[0-9a-fA-F]{4}|[\s\S])/g, (_all, esc) => {
316
+ if (esc[0] === "u") return String.fromCharCode(Number.parseInt(esc.slice(1), 16));
317
+ switch (esc) {
318
+ case "n": return "\n";
319
+ case "t": return " ";
320
+ case "r": return "\r";
321
+ case "b": return "\b";
322
+ case "f": return "\f";
323
+ default: return esc;
324
+ }
325
+ });
326
+ }
327
+ /**
328
+ * The shader source inside a Godot text-resource container, or `source` unchanged.
329
+ *
330
+ * Throws {@link UnsupportedShaderError} for a container with no usable `code` property —
331
+ * NEVER returns the container. That refusal is the whole point: before it existed, every
332
+ * parse step happened to succeed against a `.tres` (the `shader_type`, the `uniform`
333
+ * declarations and the `fragment()` body all match INSIDE the escaped `code` string), and
334
+ * then the subtractive `helpers` residual in `parseShader` carried the entire container
335
+ * into the emitted GLSL — declarations, then `[gd_resource type="VisualShader" …` as the
336
+ * first line of what should have been shader code. The driver reported a syntax error at
337
+ * a `[`, which is a long way from "this file is not a shader".
338
+ */
339
+ function unwrapShaderResource(source) {
340
+ if (!/^\s*\[gd_resource\b/.test(source)) return source;
341
+ const match = /^code\s*=\s*"((?:[^"\\]|\\[\s\S])*)"/m.exec(source);
342
+ if (!match) throw new UnsupportedShaderError("Godot resource container has no `code` property (not a shader resource)");
343
+ const code = unescapeResourceString(match[1]).trim();
344
+ if (code === "") throw new UnsupportedShaderError("Godot resource container has an empty `code` property");
345
+ return code;
346
+ }
347
+ /** Sections that can only come from a resource container — never legal shader source. */
348
+ const RESOURCE_SECTION_RE = /\[(?:gd_resource|sub_resource|ext_resource|resource)\b/;
349
+ function stripComments(src) {
350
+ return src.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, "");
351
+ }
352
+ function sanitizeReservedIdentifiers(src) {
353
+ return replaceToken(src, "input", "inputValue");
354
+ }
355
+ function extractFunction(src, name) {
356
+ const head = new RegExp(`void\\s+${name}\\s*\\(\\s*\\)\\s*\\{`).exec(src);
357
+ if (!head) return null;
358
+ const open = head.index + head[0].length - 1;
359
+ const end = matchBrace(src, open);
360
+ return src.slice(open + 1, end);
361
+ }
362
+ function removeFunction(src, name) {
363
+ const head = new RegExp(`void\\s+${name}\\s*\\(\\s*\\)\\s*\\{`).exec(src);
364
+ if (!head) return src;
365
+ const end = matchBrace(src, head.index + head[0].length - 1);
366
+ return src.slice(0, head.index) + src.slice(end + 1);
367
+ }
368
+ /** Index of the `}` closing the `{` at `openIndex`. Brace depth only — enough for the
369
+ * supported subset, which has no braces inside strings (Godot shaders have none). */
370
+ function matchBrace(src, openIndex) {
371
+ let depth = 0;
372
+ for (let i = openIndex; i < src.length; i += 1) if (src[i] === "{") depth += 1;
373
+ else if (src[i] === "}") {
374
+ depth -= 1;
375
+ if (depth === 0) return i;
376
+ }
377
+ throw new UnsupportedShaderError("unbalanced braces");
378
+ }
379
+ function promoteIntLiterals(src, intIdentifiers = []) {
380
+ const ranges = skipRanges(src);
381
+ const intNames = [...intIdentifiers];
382
+ return src.replace(/(?<![\w.])(\d+)(?![\w.])/g, (match, _digits, offset) => {
383
+ if (ranges.some(([start, end]) => offset >= start && offset < end)) return match;
384
+ if (intNames.length > 0 && isIntegerComparisonLiteral(src, offset, intNames)) return match;
385
+ return `${match}.0`;
386
+ });
387
+ }
388
+ function skipRanges(src) {
389
+ const ranges = [];
390
+ for (const re of [
391
+ /\[[^\]]*\]/g,
392
+ /\bfor\s*\([^)]*\)/g,
393
+ /\b(?:const\s+)?int\s+[^;]+;/g
394
+ ]) for (const match of src.matchAll(re)) {
395
+ const index = match.index ?? 0;
396
+ ranges.push([index, index + match[0].length]);
397
+ }
398
+ return ranges;
399
+ }
400
+ function isIntegerComparisonLiteral(src, offset, tokens) {
401
+ let start = offset;
402
+ while (start > 0 && !";{}\n".includes(src[start - 1])) start -= 1;
403
+ let end = offset;
404
+ while (end < src.length && !";{}\n".includes(src[end])) end += 1;
405
+ const statement = src.slice(start, end);
406
+ const localOffset = offset - start;
407
+ const before = statement.slice(0, localOffset);
408
+ const after = statement.slice(localOffset + String(src.slice(offset).match(/^\d+/)?.[0] ?? "").length);
409
+ const comparison = "(?:==|!=|<=|>=|<|>)";
410
+ return tokens.some((token) => {
411
+ const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
412
+ return new RegExp(`\\b${escaped}\\b\\s*${comparison}\\s*$`).test(before) || new RegExp(`^\\s*${comparison}\\s*\\b${escaped}\\b`).test(after);
413
+ });
414
+ }
415
+ /** Whole-identifier match: `TIME` must not fire on `LIFETIME`, `COLOR` not on
416
+ * `COLOR_KEY`. Every built-in probe in this file goes through it. */
417
+ function hasToken(src, token) {
418
+ return new RegExp(`(?<![\\w])${escapeRegExp(token)}(?![\\w])`).test(src);
419
+ }
420
+ /** Whole-identifier replace, same boundary rule as `hasToken`. */
421
+ function replaceToken(src, token, replacement) {
422
+ return src.replace(new RegExp(`(?<![\\w])${escapeRegExp(token)}(?![\\w])`, "g"), replacement);
423
+ }
424
+ function escapeRegExp(value) {
425
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
426
+ }
427
+ function indent(text) {
428
+ return text.split("\n").map((line) => line.trim() ? ` ${line.trim()}` : "").join("\n");
429
+ }
430
+ //#endregion
431
+ //#region src/shaders/transpile-wgsl.ts
432
+ /** "WGSL can't, WebGL can." Thrown for shapes the GLSL emitter renders happily; the
433
+ * runtime answers it with a per-binding WebGL fallback, not with the CSS fallback. */
434
+ var UnsupportedWgslShaderError = class extends UnsupportedShaderError {
435
+ constructor(message) {
436
+ super(message);
437
+ this.name = "UnsupportedWgslShaderError";
438
+ }
439
+ };
440
+ const VERTEX_ENTRY = "vs_main";
441
+ const FRAGMENT_ENTRY = "fs_main";
442
+ const BINDINGS = {
443
+ uniform: 0,
444
+ texture: 1,
445
+ textureSampler: 2,
446
+ userSamplersBase: 3
447
+ };
448
+ /** Transpile a `.gdshader` source string to one WGSL module plus its uniform layout.
449
+ * Throws `UnsupportedShaderError` (no backend can) or `UnsupportedWgslShaderError`
450
+ * (this backend can't — fall the binding back to WebGL). */
451
+ function transpileGodotShaderWgsl(source) {
452
+ const parsed = parseShader(sanitizeReservedIdentifiers(stripComments(unwrapShaderResource(source))));
453
+ rejectUnsupported(shaderLogic(parsed), parsed);
454
+ const analysis = analyzeShader(parsed);
455
+ rejectScreenCapture(parsed, analysis);
456
+ const defines = extractDefines(parsed);
457
+ const renames = buildRenameMap(parsed, defines);
458
+ const plan = buildUniformPlan(parsed, analysis, renames);
459
+ const globals = (text) => applyGlobals(text, defines, renames, plan);
460
+ const helpersText = globals(parsed.helpers);
461
+ const bodyText = globals(parsed.fragmentBody);
462
+ rejectStrayDirectives(helpersText, bodyText);
463
+ rejectFragmentLocalsInHelpers(parsed.helpers, plan);
464
+ const ctx = createContext(parsed, plan, renames);
465
+ const helpers = translateHelpers(helpersText, ctx);
466
+ const fragmentCtx = childContext(ctx);
467
+ seedFragmentScope(fragmentCtx, analysis);
468
+ const bodyLines = translateBlock(bodyText, fragmentCtx);
469
+ return {
470
+ wgsl: assembleModule({
471
+ parsed,
472
+ analysis,
473
+ plan,
474
+ renames,
475
+ ctx,
476
+ helpers,
477
+ varyingLines: analysis.varyingHoists.map((hoist) => {
478
+ const name = renameOf(hoist.name, renames);
479
+ const type = localWgslType(hoist.type, `varying "${hoist.name}"`);
480
+ fragmentCtx.scope.set(name, type);
481
+ return `var ${name}: ${type} = ${translateExpr(globals(hoist.expr), fragmentCtx)};`;
482
+ }),
483
+ bodyLines
484
+ }),
485
+ vertexEntry: VERTEX_ENTRY,
486
+ fragmentEntry: FRAGMENT_ENTRY,
487
+ uniformStructSizeBytes: plan.layout.sizeBytes,
488
+ builtinOffsets: plan.builtinOffsets,
489
+ uniforms: plan.uniformFields,
490
+ samplers: parsed.samplers,
491
+ bindings: BINDINGS,
492
+ blend: parsed.blend,
493
+ usesTime: analysis.usesTime,
494
+ usesTexturePixelSize: analysis.usesTexturePixelSize,
495
+ usesScreenUv: analysis.usesScreenUv
496
+ };
497
+ }
498
+ const SCALAR_LAYOUT = {
499
+ f32: {
500
+ align: 4,
501
+ size: 4
502
+ },
503
+ i32: {
504
+ align: 4,
505
+ size: 4
506
+ },
507
+ u32: {
508
+ align: 4,
509
+ size: 4
510
+ },
511
+ vec2f: {
512
+ align: 8,
513
+ size: 8
514
+ },
515
+ vec2i: {
516
+ align: 8,
517
+ size: 8
518
+ },
519
+ vec3f: {
520
+ align: 16,
521
+ size: 12
522
+ },
523
+ vec3i: {
524
+ align: 16,
525
+ size: 12
526
+ },
527
+ vec4f: {
528
+ align: 16,
529
+ size: 16
530
+ },
531
+ vec4i: {
532
+ align: 16,
533
+ size: 16
534
+ },
535
+ mat2x2f: {
536
+ align: 8,
537
+ size: 16
538
+ },
539
+ mat3x3f: {
540
+ align: 16,
541
+ size: 48
542
+ },
543
+ mat4x4f: {
544
+ align: 16,
545
+ size: 64
546
+ }
547
+ };
548
+ function roundUp(multiple, value) {
549
+ return Math.ceil(value / multiple) * multiple;
550
+ }
551
+ /** Lay out a WGSL `var<uniform>` struct by the uniform address space rules: members keep
552
+ * DECLARATION ORDER (a reorder would silently move every offset a writer already holds),
553
+ * each is placed at the next multiple of its alignment, and the struct size is rounded up
554
+ * to the struct alignment. Arrays get an element stride of `roundUp(align(E), size(E))`,
555
+ * which the uniform address space additionally requires to be a multiple of 16 — so
556
+ * `array<vec3f, N>` and `array<vec4f, N>` are natively fine and `array<f32, N>` /
557
+ * `array<vec2f, N>` are refused (they would need per-element padding on the host side).
558
+ *
559
+ * These are exactly WGSL's own natural layout rules, applied to the members in the order
560
+ * the module declares them, which is why the emitted struct carries no explicit padding
561
+ * members: the compiler computes the same offsets this function records. */
562
+ function wgslStructLayout(fields) {
563
+ const members = [];
564
+ let offset = 0;
565
+ let structAlign = 16;
566
+ for (const field of fields) {
567
+ const base = SCALAR_LAYOUT[field.type];
568
+ if (!base) throw new UnsupportedWgslShaderError(`uniform type "${field.type}" has no WGSL uniform layout`);
569
+ let align = base.align;
570
+ let size = base.size;
571
+ if (field.arrayLength !== void 0) {
572
+ const stride = roundUp(base.align, base.size);
573
+ if (stride % 16 !== 0) throw new UnsupportedWgslShaderError(`uniform array "${field.name}" of ${field.type} needs a 16-byte element stride in the WGSL uniform address space (got ${stride}); array-of-scalar uniforms are deferred`);
574
+ align = Math.max(16, base.align);
575
+ size = stride * field.arrayLength;
576
+ }
577
+ offset = roundUp(align, offset);
578
+ members.push({
579
+ name: field.name,
580
+ type: field.type,
581
+ ...field.arrayLength !== void 0 ? { arrayLength: field.arrayLength } : {},
582
+ offsetBytes: offset,
583
+ sizeBytes: size,
584
+ alignBytes: align
585
+ });
586
+ offset += size;
587
+ structAlign = Math.max(structAlign, align);
588
+ }
589
+ return {
590
+ members,
591
+ sizeBytes: roundUp(structAlign, offset),
592
+ alignBytes: structAlign
593
+ };
594
+ }
595
+ function rejectScreenCapture(parsed, analysis) {
596
+ if (parsed.screenTextureNames.length > 0) throw new UnsupportedWgslShaderError(`hint_screen_texture sampler "${parsed.screenTextureNames[0]}" is not supported on WebGPU: capturing what was already composited is a runtime architecture question, so this binding falls back to WebGL`);
597
+ if (analysis.usesScreenTexture) throw new UnsupportedWgslShaderError("SCREEN_TEXTURE is not supported on WebGPU: capturing what was already composited is a runtime architecture question, so this binding falls back to WebGL");
598
+ if (analysis.usesScreenPixelSize) throw new UnsupportedWgslShaderError("SCREEN_PIXEL_SIZE is not supported on WebGPU: it sizes the screen capture, which this backend does not produce");
599
+ }
600
+ /** Simple `#define A B` token aliases, pulled out of the helper text so they can be
601
+ * substituted (WGSL has no preprocessor). Anything more than a single-token alias is
602
+ * named and refused. */
603
+ function extractDefines(parsed) {
604
+ const out = [];
605
+ for (const match of parsed.helpers.matchAll(/^[ \t]*#define\s+([A-Za-z_]\w*)\s+([^\n]*)$/gm)) {
606
+ const value = match[2].trim();
607
+ if (!/^[A-Za-z_]\w*$/.test(value)) throw new UnsupportedWgslShaderError(`#define "${match[1]}" is not a simple token alias and WGSL has no preprocessor`);
608
+ out.push([match[1], value]);
609
+ }
610
+ return out;
611
+ }
612
+ function rejectStrayDirectives(helpers, body) {
613
+ for (const text of [helpers, body]) {
614
+ const stray = /^[ \t]*#\s*([a-z_]+)/m.exec(text);
615
+ if (stray) throw new UnsupportedWgslShaderError(`preprocessor directive "#${stray[1]}" has no WGSL equivalent`);
616
+ }
617
+ }
618
+ /** `UV` / `SCREEN_UV` / `COLOR` are fragment-LOCAL in both emitters (the GLSL one declares
619
+ * them inside `main()` too), so a top-level helper that reads one cannot be given them
620
+ * without inventing a calling convention. Refuse per-binding rather than emit a module
621
+ * that names an undeclared identifier. */
622
+ function rejectFragmentLocalsInHelpers(helpers, plan) {
623
+ for (const token of [
624
+ "UV",
625
+ "SCREEN_UV",
626
+ "COLOR",
627
+ "GODOT_UV"
628
+ ]) if (hasToken(helpers, token)) throw new UnsupportedWgslShaderError(`helper function reads the fragment-local built-in "${token}"`);
629
+ for (const name of plan.boolUniformNames) if (hasToken(helpers, name)) throw new UnsupportedWgslShaderError(`helper function reads the bool uniform "${name}", which is hoisted as a fragment-local alias (WGSL bool is not host-shareable)`);
630
+ }
631
+ const WGSL_RESERVED = new Set([
632
+ "alias",
633
+ "break",
634
+ "case",
635
+ "const",
636
+ "const_assert",
637
+ "continue",
638
+ "continuing",
639
+ "default",
640
+ "diagnostic",
641
+ "discard",
642
+ "else",
643
+ "enable",
644
+ "false",
645
+ "fn",
646
+ "for",
647
+ "if",
648
+ "let",
649
+ "loop",
650
+ "override",
651
+ "requires",
652
+ "return",
653
+ "struct",
654
+ "switch",
655
+ "true",
656
+ "var",
657
+ "while",
658
+ "NULL",
659
+ "Self",
660
+ "abstract",
661
+ "active",
662
+ "alignas",
663
+ "alignof",
664
+ "as",
665
+ "asm",
666
+ "asm_fragment",
667
+ "async",
668
+ "attribute",
669
+ "auto",
670
+ "await",
671
+ "become",
672
+ "binding_array",
673
+ "cast",
674
+ "catch",
675
+ "class",
676
+ "co_await",
677
+ "co_return",
678
+ "co_yield",
679
+ "coherent",
680
+ "column_major",
681
+ "common",
682
+ "compile",
683
+ "compile_fragment",
684
+ "concept",
685
+ "const_cast",
686
+ "consteval",
687
+ "constexpr",
688
+ "constinit",
689
+ "crate",
690
+ "debugger",
691
+ "decltype",
692
+ "delete",
693
+ "demote",
694
+ "demote_to_helper",
695
+ "do",
696
+ "dynamic_cast",
697
+ "enum",
698
+ "explicit",
699
+ "export",
700
+ "extends",
701
+ "extern",
702
+ "external",
703
+ "fallthrough",
704
+ "filter",
705
+ "final",
706
+ "finally",
707
+ "friend",
708
+ "from",
709
+ "fxgroup",
710
+ "get",
711
+ "goto",
712
+ "groupshared",
713
+ "highp",
714
+ "impl",
715
+ "implements",
716
+ "import",
717
+ "inline",
718
+ "instanceof",
719
+ "interface",
720
+ "layout",
721
+ "lowp",
722
+ "macro",
723
+ "macro_rules",
724
+ "match",
725
+ "mediump",
726
+ "meta",
727
+ "mod",
728
+ "module",
729
+ "move",
730
+ "mut",
731
+ "mutable",
732
+ "namespace",
733
+ "new",
734
+ "nil",
735
+ "noexcept",
736
+ "noinline",
737
+ "nointerpolation",
738
+ "non_coherent",
739
+ "noncoherent",
740
+ "noperspective",
741
+ "null",
742
+ "nullptr",
743
+ "of",
744
+ "operator",
745
+ "package",
746
+ "packoffset",
747
+ "partition",
748
+ "pass",
749
+ "patch",
750
+ "pixelfragment",
751
+ "precise",
752
+ "precision",
753
+ "premerge",
754
+ "priv",
755
+ "protected",
756
+ "pub",
757
+ "public",
758
+ "readonly",
759
+ "ref",
760
+ "regardless",
761
+ "register",
762
+ "reinterpret_cast",
763
+ "require",
764
+ "resource",
765
+ "restrict",
766
+ "self",
767
+ "set",
768
+ "shared",
769
+ "sizeof",
770
+ "smooth",
771
+ "snorm",
772
+ "static",
773
+ "static_assert",
774
+ "static_cast",
775
+ "std",
776
+ "subroutine",
777
+ "super",
778
+ "target",
779
+ "template",
780
+ "this",
781
+ "thread_local",
782
+ "throw",
783
+ "trait",
784
+ "try",
785
+ "type",
786
+ "typedef",
787
+ "typeid",
788
+ "typename",
789
+ "union",
790
+ "unless",
791
+ "unorm",
792
+ "unsafe",
793
+ "unsized",
794
+ "use",
795
+ "using",
796
+ "varying",
797
+ "virtual",
798
+ "volatile",
799
+ "wgsl",
800
+ "where",
801
+ "write",
802
+ "writeonly",
803
+ "yield"
804
+ ]);
805
+ const EMITTER_OWNED = new Set([
806
+ "_u",
807
+ "Uniforms",
808
+ "VsOut",
809
+ "raw_uv",
810
+ VERTEX_ENTRY,
811
+ FRAGMENT_ENTRY,
812
+ "godot_mod",
813
+ "godot_mod2",
814
+ "godot_mod3",
815
+ "godot_mod4",
816
+ "godot_inverse3",
817
+ "TEXTURE_smp",
818
+ "GODOT_UV",
819
+ "uv_fit",
820
+ "uv_window",
821
+ "time",
822
+ "texture_pixel_size",
823
+ "modulate",
824
+ "screen_origin",
825
+ "screen_size"
826
+ ]);
827
+ const RENAME_SUFFIX = "_gsw";
828
+ function renameOf(name, renames) {
829
+ return renames.get(name) ?? name;
830
+ }
831
+ function buildRenameMap(parsed, defines) {
832
+ const text = `${parsed.helpers}\n${parsed.fragmentBody}`;
833
+ if (/(?<![\w])_swz\d*(?![\w])/.test(text)) throw new UnsupportedWgslShaderError("the identifier prefix \"_swz\" is reserved for swizzle-assignment temporaries");
834
+ const candidates = /* @__PURE__ */ new Set();
835
+ for (const match of text.matchAll(/(?<![\w])(?:const\s+)?(?:void|float|int|bool|uint|vec[234]|ivec[234]|bvec[234]|mat[234])\s+([A-Za-z_]\w*)/g)) candidates.add(match[1]);
836
+ for (const uniform of parsed.uniforms) candidates.add(uniform.name);
837
+ for (const sampler of parsed.samplers) candidates.add(sampler.name);
838
+ for (const varying of parsed.varyings) candidates.add(varying.name);
839
+ for (const [name] of defines) candidates.delete(name);
840
+ const renames = /* @__PURE__ */ new Map();
841
+ for (const name of candidates) if (WGSL_RESERVED.has(name) || EMITTER_OWNED.has(name)) renames.set(name, `${name}${RENAME_SUFFIX}`);
842
+ return renames;
843
+ }
844
+ /** The two whole-text passes every chunk of shader logic goes through before it is
845
+ * parsed statement by statement: resolve `#define` aliases and reserved-word renames,
846
+ * then bind every uniform / built-in read to its member of the one uniform struct. */
847
+ function applyGlobals(text, defines, renames, plan) {
848
+ let out = text.replace(/^[ \t]*#define\s+[A-Za-z_]\w*\s+[^\n]*$/gm, "");
849
+ for (const [name, value] of defines) out = replaceToken(out, name, value);
850
+ for (const [from, to] of renames) out = replaceToken(out, from, to);
851
+ for (const [wgslName, member] of plan.memberByWgslName) {
852
+ if (plan.boolUniformNames.has(wgslName)) continue;
853
+ if (member.builtin) continue;
854
+ out = replaceToken(out, wgslName, `_u.${wgslName}`);
855
+ }
856
+ if (plan.builtinOffsets.time !== void 0) out = replaceToken(out, "TIME", "_u.time");
857
+ if (plan.builtinOffsets.texturePixelSize !== void 0) out = replaceToken(out, "TEXTURE_PIXEL_SIZE", "_u.texture_pixel_size");
858
+ if (plan.builtinOffsets.modulate !== void 0) out = replaceToken(out, "MODULATE", "_u.modulate");
859
+ return out;
860
+ }
861
+ const UNIFORM_WGSL_TYPE = {
862
+ float: "f32",
863
+ int: "i32",
864
+ bool: "f32",
865
+ vec2: "vec2f",
866
+ vec3: "vec3f",
867
+ vec4: "vec4f",
868
+ ivec2: "vec2i",
869
+ ivec3: "vec3i",
870
+ ivec4: "vec4i",
871
+ mat2: "mat2x2f",
872
+ mat3: "mat3x3f",
873
+ mat4: "mat4x4f"
874
+ };
875
+ function buildUniformPlan(parsed, analysis, renames) {
876
+ const members = [{
877
+ wgslName: "uv_window",
878
+ type: "vec4f",
879
+ builtin: true
880
+ }, {
881
+ wgslName: "uv_fit",
882
+ type: "vec2f",
883
+ builtin: true
884
+ }];
885
+ if (analysis.usesTime) members.push({
886
+ wgslName: "time",
887
+ type: "f32",
888
+ builtin: true
889
+ });
890
+ if (analysis.usesTexturePixelSize) members.push({
891
+ wgslName: "texture_pixel_size",
892
+ type: "vec2f",
893
+ builtin: true
894
+ });
895
+ if (analysis.usesScreenUv) {
896
+ members.push({
897
+ wgslName: "screen_origin",
898
+ type: "vec2f",
899
+ builtin: true
900
+ });
901
+ members.push({
902
+ wgslName: "screen_size",
903
+ type: "vec2f",
904
+ builtin: true
905
+ });
906
+ }
907
+ if (analysis.needsModulate) members.push({
908
+ wgslName: "modulate",
909
+ type: "vec4f",
910
+ builtin: true
911
+ });
912
+ const boolUniformNames = /* @__PURE__ */ new Set();
913
+ for (const uniform of parsed.uniforms) {
914
+ const type = UNIFORM_WGSL_TYPE[uniform.type];
915
+ if (!type) throw new UnsupportedWgslShaderError(`uniform type "${uniform.type}" has no WGSL uniform equivalent`);
916
+ if (/^bvec[234]$/.test(uniform.type)) throw new UnsupportedWgslShaderError(`bool-vector uniform "${uniform.name}" is not host-shareable in WGSL`);
917
+ const wgslName = renameOf(uniform.name, renames);
918
+ if (uniform.type === "bool") {
919
+ if (uniform.arrayLength !== void 0) throw new UnsupportedWgslShaderError(`bool array uniform "${uniform.name}" is not supported`);
920
+ boolUniformNames.add(wgslName);
921
+ }
922
+ members.push({
923
+ wgslName,
924
+ type,
925
+ builtin: false,
926
+ ...uniform.arrayLength !== void 0 ? { arrayLength: uniform.arrayLength } : {}
927
+ });
928
+ }
929
+ const layout = wgslStructLayout(members.map((m) => ({
930
+ name: m.wgslName,
931
+ type: m.type,
932
+ ...m.arrayLength !== void 0 ? { arrayLength: m.arrayLength } : {}
933
+ })));
934
+ const offsetOf = new Map(layout.members.map((m) => [m.name, m.offsetBytes]));
935
+ const sizeOf = new Map(layout.members.map((m) => [m.name, m.sizeBytes]));
936
+ const builtinOffsets = {
937
+ uvFit: offsetOf.get("uv_fit") ?? 0,
938
+ uvWindow: offsetOf.get("uv_window") ?? 0
939
+ };
940
+ if (analysis.usesTime) builtinOffsets.time = offsetOf.get("time");
941
+ if (analysis.usesTexturePixelSize) builtinOffsets.texturePixelSize = offsetOf.get("texture_pixel_size");
942
+ if (analysis.usesScreenUv) {
943
+ builtinOffsets.screenOrigin = offsetOf.get("screen_origin");
944
+ builtinOffsets.screenSize = offsetOf.get("screen_size");
945
+ }
946
+ if (analysis.needsModulate) builtinOffsets.modulate = offsetOf.get("modulate");
947
+ const uniformFields = parsed.uniforms.map((uniform) => {
948
+ const wgslName = renameOf(uniform.name, renames);
949
+ return {
950
+ name: uniform.name,
951
+ type: UNIFORM_WGSL_TYPE[uniform.type],
952
+ godotType: uniform.type,
953
+ ...uniform.arrayLength !== void 0 ? { arrayLength: uniform.arrayLength } : {},
954
+ offsetBytes: offsetOf.get(wgslName) ?? 0,
955
+ sizeBytes: sizeOf.get(wgslName) ?? 0,
956
+ ...uniform.default !== void 0 ? { default: uniform.default } : {}
957
+ };
958
+ });
959
+ const memberByWgslName = new Map(members.map((m) => [m.wgslName, m]));
960
+ const memberTypes = /* @__PURE__ */ new Map();
961
+ for (const member of members) memberTypes.set(`_u.${member.wgslName}`, member.arrayLength !== void 0 ? `array<${member.type},${member.arrayLength}>` : member.type);
962
+ return {
963
+ layout,
964
+ builtinOffsets,
965
+ uniformFields,
966
+ members,
967
+ memberByWgslName,
968
+ boolUniformNames,
969
+ memberTypes
970
+ };
971
+ }
972
+ function createContext(parsed, plan, renames) {
973
+ const scope = new Map(plan.memberTypes);
974
+ for (const name of plan.boolUniformNames) scope.set(name, "bool");
975
+ const textures = new Set(["TEXTURE"]);
976
+ for (const sampler of parsed.samplers) textures.add(renameOf(sampler.name, renames));
977
+ const uniformNames = /* @__PURE__ */ new Set();
978
+ for (const uniform of parsed.uniforms) uniformNames.add(renameOf(uniform.name, renames));
979
+ for (const match of `${parsed.helpers}\n${parsed.fragmentBody}`.matchAll(/(?<![\w])(?:const\s+)?(?:float|int|bool|uint|vec[234]|ivec[234]|bvec[234]|mat[234])\s+([A-Za-z_]\w*)/g)) {
980
+ const name = renameOf(match[1], renames);
981
+ if (uniformNames.has(name)) throw new UnsupportedWgslShaderError(`local "${match[1]}" shadows the uniform of the same name`);
982
+ }
983
+ return {
984
+ scope,
985
+ fnReturns: /* @__PURE__ */ new Map(),
986
+ needMod: /* @__PURE__ */ new Set(),
987
+ needInverse: { mat3: false },
988
+ swizzle: { next: 0 },
989
+ textures,
990
+ uniformNames
991
+ };
992
+ }
993
+ function childContext(ctx) {
994
+ return {
995
+ ...ctx,
996
+ scope: new Map(ctx.scope)
997
+ };
998
+ }
999
+ function seedFragmentScope(ctx, analysis) {
1000
+ ctx.scope.set("COLOR", "vec4f");
1001
+ ctx.scope.set("UV", "vec2f");
1002
+ ctx.scope.set("GODOT_UV", "vec2f");
1003
+ ctx.scope.set("PI", "f32");
1004
+ if (analysis.usesScreenUv) ctx.scope.set("SCREEN_UV", "vec2f");
1005
+ }
1006
+ function translateHelpers(helpers, ctx) {
1007
+ const consts = [];
1008
+ const fns = [];
1009
+ const parsedFns = [];
1010
+ let i = 0;
1011
+ while (i < helpers.length) {
1012
+ while (i < helpers.length && /\s/.test(helpers[i])) i += 1;
1013
+ if (i >= helpers.length) break;
1014
+ if (helpers[i] === ";") {
1015
+ i += 1;
1016
+ continue;
1017
+ }
1018
+ const rest = helpers.slice(i);
1019
+ const constMatch = /^const\s+([A-Za-z_]\w*)\s+([A-Za-z_]\w*)\s*=([\s\S]*?);/.exec(rest);
1020
+ if (constMatch) {
1021
+ const type = localWgslType(constMatch[1], `const "${constMatch[2]}"`);
1022
+ ctx.scope.set(constMatch[2], type);
1023
+ consts.push(`const ${constMatch[2]}: ${type} = ${translateExpr(constMatch[3], ctx)};`);
1024
+ i += constMatch[0].length;
1025
+ continue;
1026
+ }
1027
+ const fnMatch = /^([A-Za-z_]\w*)\s+([A-Za-z_]\w*)\s*\(/.exec(rest);
1028
+ if (fnMatch) {
1029
+ const open = i + fnMatch[0].length - 1;
1030
+ const closeParen = matchParen(helpers, open);
1031
+ let k = closeParen + 1;
1032
+ while (k < helpers.length && /\s/.test(helpers[k])) k += 1;
1033
+ if (helpers[k] !== "{") throw new UnsupportedWgslShaderError(`helper "${fnMatch[2]}" has no body (forward declarations are not supported)`);
1034
+ const closeBrace = matchBrace(helpers, k);
1035
+ parsedFns.push({
1036
+ ret: fnMatch[1],
1037
+ name: fnMatch[2],
1038
+ params: helpers.slice(open + 1, closeParen),
1039
+ body: helpers.slice(k + 1, closeBrace)
1040
+ });
1041
+ i = closeBrace + 1;
1042
+ continue;
1043
+ }
1044
+ throw new UnsupportedWgslShaderError(`unsupported top-level construct "${rest.slice(0, 40).trim()}"`);
1045
+ }
1046
+ for (const fn of parsedFns) if (fn.ret !== "void") ctx.fnReturns.set(fn.name, localWgslType(fn.ret, `helper "${fn.name}"`));
1047
+ for (const fn of parsedFns) {
1048
+ const fnCtx = childContext(ctx);
1049
+ const params = parseParams(fn.params, fn.name, fnCtx);
1050
+ const signature = fn.ret === "void" ? `fn ${fn.name}(${params.join(", ")}) {` : `fn ${fn.name}(${params.join(", ")}) -> ${ctx.fnReturns.get(fn.name)} {`;
1051
+ fns.push([
1052
+ signature,
1053
+ ...indentLines(translateBlock(fn.body, fnCtx)),
1054
+ "}"
1055
+ ].join("\n"));
1056
+ }
1057
+ return {
1058
+ consts,
1059
+ fns
1060
+ };
1061
+ }
1062
+ function parseParams(params, fnName, ctx) {
1063
+ const trimmed = params.trim();
1064
+ if (!trimmed || trimmed === "void") return [];
1065
+ return splitTopLevel(trimmed, ",").map((param) => {
1066
+ const match = /^(?:(in|out|inout)\s+)?([A-Za-z_]\w*)\s+([A-Za-z_]\w*)$/.exec(param.trim());
1067
+ if (!match) throw new UnsupportedWgslShaderError(`helper "${fnName}" has an unsupported parameter "${param.trim()}"`);
1068
+ if (match[1] === "out" || match[1] === "inout") throw new UnsupportedWgslShaderError(`helper "${fnName}" uses an ${match[1]} parameter; WGSL would need a pointer`);
1069
+ const type = localWgslType(match[2], `parameter "${match[3]}"`);
1070
+ if (ctx.uniformNames.has(match[3])) throw new UnsupportedWgslShaderError(`parameter "${match[3]}" of helper "${fnName}" shadows a uniform`);
1071
+ ctx.scope.set(match[3], type);
1072
+ return `${match[3]}: ${type}`;
1073
+ });
1074
+ }
1075
+ const LOCAL_WGSL_TYPE = {
1076
+ ...UNIFORM_WGSL_TYPE,
1077
+ bool: "bool",
1078
+ bvec2: "vec2<bool>",
1079
+ bvec3: "vec3<bool>",
1080
+ bvec4: "vec4<bool>"
1081
+ };
1082
+ function localWgslType(godotType, what) {
1083
+ const type = LOCAL_WGSL_TYPE[godotType];
1084
+ if (!type) throw new UnsupportedWgslShaderError(`${what} has unsupported type "${godotType}"`);
1085
+ return type;
1086
+ }
1087
+ const DECLARATION = /^(const\s+)?(float|int|bool|uint|vec[234]|ivec[234]|bvec[234]|mat[234])\s+([A-Za-z_]\w*)\s*(\[[^\]]*\])?\s*(?:=([\s\S]*))?$/;
1088
+ function translateBlock(src, ctx) {
1089
+ const lines = [];
1090
+ let i = 0;
1091
+ while (i < src.length) {
1092
+ while (i < src.length && /\s/.test(src[i])) i += 1;
1093
+ if (i >= src.length) break;
1094
+ if (src[i] === ";") {
1095
+ i += 1;
1096
+ continue;
1097
+ }
1098
+ if (src[i] === "{") {
1099
+ const close = matchBrace(src, i);
1100
+ lines.push("{");
1101
+ lines.push(...indentLines(translateBlock(src.slice(i + 1, close), ctx)));
1102
+ lines.push("}");
1103
+ i = close + 1;
1104
+ continue;
1105
+ }
1106
+ let depth = 0;
1107
+ let stop = -1;
1108
+ let stopChar = "";
1109
+ for (let j = i; j < src.length; j += 1) {
1110
+ const c = src[j];
1111
+ if (c === "(" || c === "[") depth += 1;
1112
+ else if (c === ")" || c === "]") depth -= 1;
1113
+ else if (depth === 0 && (c === ";" || c === "{" || c === "}")) {
1114
+ stop = j;
1115
+ stopChar = c;
1116
+ break;
1117
+ }
1118
+ }
1119
+ if (stop < 0) {
1120
+ const tail = src.slice(i).trim();
1121
+ if (tail) throw new UnsupportedWgslShaderError(`unterminated statement "${tail}"`);
1122
+ break;
1123
+ }
1124
+ if (stopChar === "}") throw new UnsupportedWgslShaderError(`unbalanced braces near "${src.slice(i, stop).trim()}"`);
1125
+ if (stopChar === "{") {
1126
+ const header = src.slice(i, stop).trim();
1127
+ const close = matchBrace(src, stop);
1128
+ lines.push(...translateControl(header, src.slice(stop + 1, close), ctx));
1129
+ i = close + 1;
1130
+ while (i < src.length && /\s/.test(src[i])) i += 1;
1131
+ if (src[i] === ";") i += 1;
1132
+ continue;
1133
+ }
1134
+ lines.push(...translateStatement(src.slice(i, stop).trim(), ctx));
1135
+ i = stop + 1;
1136
+ }
1137
+ return mergeElse(lines);
1138
+ }
1139
+ function translateControl(header, inner, ctx) {
1140
+ if (/^while\b/.test(header)) throw new UnsupportedShaderError("while loops are not supported");
1141
+ if (/^for\b/.test(header)) {
1142
+ const open = header.indexOf("(");
1143
+ if (open < 0 || matchParen(header, open) !== header.length - 1) throw new UnsupportedWgslShaderError(`unsupported for header "${header}"`);
1144
+ const clauses = splitTopLevel(header.slice(open + 1, header.length - 1), ";");
1145
+ if (clauses.length !== 3) throw new UnsupportedWgslShaderError(`for loop needs init/condition/increment, got "${header}"`);
1146
+ const loop = childContext(ctx);
1147
+ return [
1148
+ `for (${oneLine(clauses[0], loop)}; ${clauses[1].trim() ? translateExpr(clauses[1], loop) : ""}; ${oneLine(clauses[2], loop)}) {`,
1149
+ ...indentLines(translateBlock(inner, loop)),
1150
+ "}"
1151
+ ];
1152
+ }
1153
+ const ifMatch = /^(else\s+if|if)\b/.exec(header);
1154
+ if (ifMatch) {
1155
+ const open = header.indexOf("(");
1156
+ if (open < 0) throw new UnsupportedWgslShaderError(`unsupported if header "${header}"`);
1157
+ const close = matchParen(header, open);
1158
+ const cond = translateExpr(header.slice(open + 1, close), ctx);
1159
+ return [
1160
+ `${ifMatch[1].startsWith("else") ? "else if" : "if"} (${cond}) {`,
1161
+ ...indentLines(translateBlock(inner, childContext(ctx))),
1162
+ "}"
1163
+ ];
1164
+ }
1165
+ if (/^else$/.test(header)) return [
1166
+ "else {",
1167
+ ...indentLines(translateBlock(inner, childContext(ctx))),
1168
+ "}"
1169
+ ];
1170
+ throw new UnsupportedWgslShaderError(`unsupported block header "${header}"`);
1171
+ }
1172
+ /** A for-clause: one statement, rendered without its terminating `;`. */
1173
+ function oneLine(clause, ctx) {
1174
+ const trimmed = clause.trim();
1175
+ if (!trimmed) return "";
1176
+ const lines = translateStatement(trimmed, ctx);
1177
+ if (lines.length !== 1) throw new UnsupportedWgslShaderError(`for clause "${trimmed}" does not translate to a single statement`);
1178
+ return lines[0].replace(/;$/, "");
1179
+ }
1180
+ function translateStatement(stmt, ctx) {
1181
+ if (!stmt) return [];
1182
+ const controlMatch = /^(if|for|while|else\s+if|else)\b/.exec(stmt);
1183
+ if (controlMatch) {
1184
+ if (controlMatch[1] === "while") throw new UnsupportedShaderError("while loops are not supported");
1185
+ if (controlMatch[1] === "else") return translateControl("else", `${stmt.slice(4).trim()};`, ctx);
1186
+ const open = stmt.indexOf("(");
1187
+ if (open < 0) throw new UnsupportedWgslShaderError(`unsupported statement "${stmt}"`);
1188
+ const close = matchParen(stmt, open);
1189
+ return translateControl(stmt.slice(0, close + 1).trim(), `${stmt.slice(close + 1).trim()};`, ctx);
1190
+ }
1191
+ if (stmt === "continue" || stmt === "break" || stmt === "discard") return [`${stmt};`];
1192
+ if (/^return\b/.test(stmt)) {
1193
+ const value = stmt.slice(6).trim();
1194
+ return value ? [`return ${translateExpr(value, ctx)};`] : ["return;"];
1195
+ }
1196
+ const decl = DECLARATION.exec(stmt);
1197
+ if (decl) {
1198
+ if (decl[4]) throw new UnsupportedWgslShaderError(`local array declaration "${decl[3]}" is not supported`);
1199
+ const type = localWgslType(decl[2], `local "${decl[3]}"`);
1200
+ ctx.scope.set(decl[3], type);
1201
+ if (decl[5] === void 0) return [`var ${decl[3]}: ${type};`];
1202
+ return [`${decl[1] ? "let" : "var"} ${decl[3]}: ${type} = ${translateExpr(decl[5], ctx)};`];
1203
+ }
1204
+ if (/^[A-Za-z_][\w.[\]]*\s*(\+\+|--)$/.test(stmt)) return [`${stmt.replace(/\s+/g, "")};`];
1205
+ const prefixInc = /^(\+\+|--)\s*([A-Za-z_][\w.[\]]*)$/.exec(stmt);
1206
+ if (prefixInc) return [`${prefixInc[2]}${prefixInc[1]};`];
1207
+ const assign = findAssignment(stmt);
1208
+ if (assign) return translateAssignment(assign, ctx);
1209
+ if (/^[A-Za-z_]\w*\s*\(/.test(stmt)) return [`${translateExpr(stmt, ctx)};`];
1210
+ throw new UnsupportedWgslShaderError(`unsupported statement "${stmt}"`);
1211
+ }
1212
+ function findAssignment(stmt) {
1213
+ let depth = 0;
1214
+ for (let i = 0; i < stmt.length; i += 1) {
1215
+ const c = stmt[i];
1216
+ if (c === "(" || c === "[") depth += 1;
1217
+ else if (c === ")" || c === "]") depth -= 1;
1218
+ else if (depth === 0 && c === "=") {
1219
+ if (stmt[i + 1] === "=") {
1220
+ i += 1;
1221
+ continue;
1222
+ }
1223
+ const prev = stmt[i - 1];
1224
+ if (prev === "=" || prev === "!" || prev === "<" || prev === ">") continue;
1225
+ if (prev === "+" || prev === "-" || prev === "*" || prev === "/") return {
1226
+ lvalue: stmt.slice(0, i - 1),
1227
+ op: `${prev}=`,
1228
+ rvalue: stmt.slice(i + 1)
1229
+ };
1230
+ return {
1231
+ lvalue: stmt.slice(0, i),
1232
+ op: "=",
1233
+ rvalue: stmt.slice(i + 1)
1234
+ };
1235
+ }
1236
+ }
1237
+ return null;
1238
+ }
1239
+ const LVALUE = /^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*|\[[^\]]*\])*$/;
1240
+ const COMPONENT = {
1241
+ x: "x",
1242
+ y: "y",
1243
+ z: "z",
1244
+ w: "w",
1245
+ r: "x",
1246
+ g: "y",
1247
+ b: "z",
1248
+ a: "w"
1249
+ };
1250
+ function translateAssignment(assign, ctx) {
1251
+ const lvalue = assign.lvalue.trim();
1252
+ if (lvalue.startsWith("_u.")) throw new UnsupportedWgslShaderError(`assignment to the uniform "${lvalue.slice(3)}"`);
1253
+ if (!LVALUE.test(lvalue)) throw new UnsupportedWgslShaderError(`unsupported assignment target "${lvalue}"`);
1254
+ const rvalue = translateExpr(assign.rvalue, ctx);
1255
+ const swizzle = /^(.+)\.([xyzwrgba]{2,4})$/.exec(lvalue);
1256
+ if (!swizzle) return [`${lvalue} ${assign.op} ${rvalue};`];
1257
+ const base = swizzle[1];
1258
+ const comps = [...swizzle[2]];
1259
+ const ctor = swizzleConstructor(base, comps.length, ctx);
1260
+ const temp = `_swz${ctx.swizzle.next}`;
1261
+ ctx.swizzle.next += 1;
1262
+ const lines = [`let ${temp} = ${ctor}(${rvalue});`];
1263
+ comps.forEach((component, index) => {
1264
+ const dst = `${base}.${COMPONENT[component]}`;
1265
+ const src = `${temp}.${"xyzw"[index]}`;
1266
+ lines.push(assign.op === "=" ? `${dst} = ${src};` : `${dst} = ${dst} ${assign.op[0]} ${src};`);
1267
+ });
1268
+ return lines;
1269
+ }
1270
+ function swizzleConstructor(base, width, ctx) {
1271
+ const type = inferType(base, ctx);
1272
+ return `vec${width}${type && /^vec[234]i$/.test(type) ? "i" : "f"}`;
1273
+ }
1274
+ function translateExpr(expr, ctx) {
1275
+ const trimmed = expr.trim();
1276
+ const ternary = splitTernary(trimmed);
1277
+ if (ternary) return `select(${translateExpr(ternary.whenFalse, ctx)}, ${translateExpr(ternary.whenTrue, ctx)}, ${translateExpr(ternary.cond, ctx)})`;
1278
+ let out = rewriteTextureCalls(trimmed, ctx);
1279
+ out = rewriteIntrinsics(out, ctx);
1280
+ out = renameConstructors(out);
1281
+ if (out.includes("?")) throw new UnsupportedWgslShaderError(`ternary "?:" is only supported as a complete right-hand side, got "${trimmed}"`);
1282
+ return out.trim();
1283
+ }
1284
+ function splitTernary(expr) {
1285
+ let depth = 0;
1286
+ let question = -1;
1287
+ for (let i = 0; i < expr.length; i += 1) {
1288
+ const c = expr[i];
1289
+ if (c === "(" || c === "[") depth += 1;
1290
+ else if (c === ")" || c === "]") depth -= 1;
1291
+ else if (depth === 0 && c === "?") {
1292
+ question = i;
1293
+ break;
1294
+ }
1295
+ }
1296
+ if (question < 0) return null;
1297
+ depth = 0;
1298
+ let pending = 0;
1299
+ for (let i = question + 1; i < expr.length; i += 1) {
1300
+ const c = expr[i];
1301
+ if (c === "(" || c === "[") depth += 1;
1302
+ else if (c === ")" || c === "]") depth -= 1;
1303
+ else if (depth === 0 && c === "?") pending += 1;
1304
+ else if (depth === 0 && c === ":") {
1305
+ if (pending > 0) {
1306
+ pending -= 1;
1307
+ continue;
1308
+ }
1309
+ return {
1310
+ cond: expr.slice(0, question),
1311
+ whenTrue: expr.slice(question + 1, i),
1312
+ whenFalse: expr.slice(i + 1)
1313
+ };
1314
+ }
1315
+ }
1316
+ throw new UnsupportedWgslShaderError(`ternary without a ":" in "${expr}"`);
1317
+ }
1318
+ /** `texture(t, uv)` -> `textureSampleLevel(t, t_smp, uv, 0.0)`. Explicit level 0 rather
1319
+ * than `textureSample`: it sidesteps WGSL's non-uniform-control-flow rule for implicit
1320
+ * derivatives (the corpus samples inside `if`/`for`), and the canvas textures this
1321
+ * runtime binds are single-mip, so level 0 IS the only level — visually identical. */
1322
+ function rewriteTextureCalls(src, ctx) {
1323
+ return mapCalls(src, "texture", (args, raw) => {
1324
+ if (args.length !== 2) throw new UnsupportedWgslShaderError(`texture() with ${args.length} arguments is not supported ("${raw}")`);
1325
+ const sampler = args[0].trim();
1326
+ if (!ctx.textures.has(sampler)) throw new UnsupportedWgslShaderError(`texture() on "${sampler}", which is not a declared sampler2D uniform`);
1327
+ return `textureSampleLevel(${sampler}, ${sampler}_smp, ${args[1].trim()}, 0.0)`;
1328
+ });
1329
+ }
1330
+ function rewriteIntrinsics(src, ctx) {
1331
+ let out = mapCalls(src, "mod", (args, raw) => {
1332
+ if (args.length !== 2) throw new UnsupportedWgslShaderError(`mod() with ${args.length} arguments`);
1333
+ const left = widthOf(args[0], ctx, raw);
1334
+ const right = widthOf(args[1], ctx, raw);
1335
+ const width = Math.max(left, right);
1336
+ ctx.needMod.add(width);
1337
+ return `${width === 1 ? "godot_mod" : `godot_mod${width}`}(${broadcast(args[0].trim(), left, width)}, ${broadcast(args[1].trim(), right, width)})`;
1338
+ });
1339
+ out = mapCalls(out, "atan", (args) => args.length === 2 ? `atan2(${args[0].trim()}, ${args[1].trim()})` : `atan(${args[0].trim()})`);
1340
+ out = mapCalls(out, "inverse", (args, raw) => {
1341
+ const type = inferType(args[0], ctx);
1342
+ if (type !== "mat3x3f") throw new UnsupportedWgslShaderError(`inverse() is only supported on a mat3 (got ${type ?? "an untyped expression"} in "${raw}")`);
1343
+ ctx.needInverse.mat3 = true;
1344
+ return `godot_inverse3(${args[0].trim()})`;
1345
+ });
1346
+ return out;
1347
+ }
1348
+ function broadcast(expr, from, to) {
1349
+ return from === to ? expr : `vec${to}f(${expr})`;
1350
+ }
1351
+ function widthOf(expr, ctx, raw) {
1352
+ const type = inferType(expr, ctx);
1353
+ const width = type ? typeWidth(type) : null;
1354
+ if (width === null) throw new UnsupportedWgslShaderError(`cannot infer the component count of "${expr.trim()}" in "${raw}" (WGSL has no function overloading, so mod()'s polyfill is chosen by width)`);
1355
+ return width;
1356
+ }
1357
+ const CONSTRUCTORS = {
1358
+ float: "f32",
1359
+ int: "i32",
1360
+ vec2: "vec2f",
1361
+ vec3: "vec3f",
1362
+ vec4: "vec4f",
1363
+ ivec2: "vec2i",
1364
+ ivec3: "vec3i",
1365
+ ivec4: "vec4i",
1366
+ bvec2: "vec2<bool>",
1367
+ bvec3: "vec3<bool>",
1368
+ bvec4: "vec4<bool>",
1369
+ mat2: "mat2x2f",
1370
+ mat3: "mat3x3f",
1371
+ mat4: "mat4x4f"
1372
+ };
1373
+ function renameConstructors(src) {
1374
+ return src.replace(/(?<![\w.])(float|int|vec[234]|ivec[234]|bvec[234]|mat[234])(\s*)\(/g, (_match, name, space) => `${CONSTRUCTORS[name]}${space}(`);
1375
+ }
1376
+ const VECTOR_TYPE = /^vec([234])([fi])$/;
1377
+ function typeWidth(type) {
1378
+ if (type === "f32" || type === "i32" || type === "u32" || type === "bool") return 1;
1379
+ const match = VECTOR_TYPE.exec(type);
1380
+ return match ? Number(match[1]) : null;
1381
+ }
1382
+ const SCALAR_RETURN = new Set([
1383
+ "length",
1384
+ "dot",
1385
+ "distance",
1386
+ "determinant"
1387
+ ]);
1388
+ const VEC4_RETURN = new Set([
1389
+ "texture",
1390
+ "textureSampleLevel",
1391
+ "textureLoad"
1392
+ ]);
1393
+ const BOOL_RETURN = new Set(["any", "all"]);
1394
+ function inferType(expr, ctx) {
1395
+ let e = expr.trim();
1396
+ while (e.startsWith("(") && matchParen(e, 0) === e.length - 1) e = e.slice(1, -1).trim();
1397
+ if (!e) return null;
1398
+ if (hasTopLevelComparison(e)) return "bool";
1399
+ const operands = splitOperands(e);
1400
+ if (operands.length > 1) {
1401
+ let best = null;
1402
+ for (const operand of operands) {
1403
+ const type = inferType(operand, ctx);
1404
+ if (!type) continue;
1405
+ const width = typeWidth(type);
1406
+ const bestWidth = best ? typeWidth(best) : null;
1407
+ if (best === null || (width ?? 0) > (bestWidth ?? 0)) best = type;
1408
+ }
1409
+ return best;
1410
+ }
1411
+ if (/^[-+!~]/.test(e)) return inferType(e.slice(1), ctx);
1412
+ if (/^\d/.test(e) || /^\.\d/.test(e)) return "f32";
1413
+ const call = /^([A-Za-z_]\w*)\s*\(/.exec(e);
1414
+ if (call && matchParen(e, e.indexOf("(")) === e.length - 1) return inferCallType(call[1], e, ctx);
1415
+ const swizzle = /^(.+)\.([xyzwrgba]+)$/.exec(e);
1416
+ if (swizzle) {
1417
+ const baseType = inferType(swizzle[1], ctx);
1418
+ const match = baseType ? VECTOR_TYPE.exec(baseType) : null;
1419
+ if (match) {
1420
+ const width = swizzle[2].length;
1421
+ return width === 1 ? scalarOf(match[2]) : `vec${width}${match[2]}`;
1422
+ }
1423
+ }
1424
+ const index = /^(.+)\[[^\]]*\]$/.exec(e);
1425
+ if (index) {
1426
+ const arrayType = inferType(index[1], ctx);
1427
+ const element = arrayType ? /^array<([^,]+),/.exec(arrayType) : null;
1428
+ if (element) return element[1];
1429
+ if (arrayType === "mat3x3f") return "vec3f";
1430
+ if (arrayType === "mat2x2f") return "vec2f";
1431
+ if (arrayType === "mat4x4f") return "vec4f";
1432
+ }
1433
+ return ctx.scope.get(e) ?? null;
1434
+ }
1435
+ function scalarOf(suffix) {
1436
+ return suffix === "i" ? "i32" : "f32";
1437
+ }
1438
+ function inferCallType(name, expr, ctx) {
1439
+ const ctor = CONSTRUCTORS[name];
1440
+ if (ctor) return ctor;
1441
+ if (/^(f32|i32|u32|vec[234][fi]|mat[234]x[234]f)$/.test(name)) return name;
1442
+ if (SCALAR_RETURN.has(name)) return "f32";
1443
+ if (VEC4_RETURN.has(name)) return "vec4f";
1444
+ if (BOOL_RETURN.has(name)) return "bool";
1445
+ const helper = ctx.fnReturns.get(name);
1446
+ if (helper) return helper;
1447
+ const open = expr.indexOf("(");
1448
+ const args = splitTopLevel(expr.slice(open + 1, expr.length - 1), ",");
1449
+ let best = null;
1450
+ for (const arg of args) {
1451
+ if (!arg.trim()) continue;
1452
+ const type = inferType(arg, ctx);
1453
+ if (!type) continue;
1454
+ const bestWidth = best ? typeWidth(best) : null;
1455
+ if (best === null || (typeWidth(type) ?? 0) > (bestWidth ?? 0)) best = type;
1456
+ }
1457
+ return best;
1458
+ }
1459
+ function hasTopLevelComparison(expr) {
1460
+ let depth = 0;
1461
+ for (let i = 0; i < expr.length; i += 1) {
1462
+ const c = expr[i];
1463
+ if (c === "(" || c === "[") depth += 1;
1464
+ else if (c === ")" || c === "]") depth -= 1;
1465
+ else if (depth === 0) {
1466
+ const pair = expr.slice(i, i + 2);
1467
+ if (pair === "&&" || pair === "||" || pair === "==" || pair === "!=") return true;
1468
+ if ((c === "<" || c === ">") && expr[i + 1] !== "<" && expr[i + 1] !== ">") return true;
1469
+ }
1470
+ }
1471
+ return false;
1472
+ }
1473
+ /** Split on top-level `+ - * /`, skipping the unary uses (leading, or right after
1474
+ * another operator or an opening delimiter). Only used to find the WIDEST operand. */
1475
+ function splitOperands(expr) {
1476
+ const parts = [];
1477
+ let depth = 0;
1478
+ let start = 0;
1479
+ for (let i = 0; i < expr.length; i += 1) {
1480
+ const c = expr[i];
1481
+ if (c === "(" || c === "[") depth += 1;
1482
+ else if (c === ")" || c === "]") depth -= 1;
1483
+ else if (depth === 0 && "+-*/%".includes(c)) {
1484
+ const before = expr.slice(start, i).trim();
1485
+ if (!before) continue;
1486
+ if (/[-+*/%<>=!&|,(]$/.test(before)) continue;
1487
+ parts.push(before);
1488
+ start = i + 1;
1489
+ }
1490
+ }
1491
+ const tail = expr.slice(start).trim();
1492
+ if (tail) parts.push(tail);
1493
+ return parts;
1494
+ }
1495
+ function mapCalls(src, name, transform) {
1496
+ const pattern = new RegExp(`(?<![\\w.])${name}\\s*\\(`, "g");
1497
+ let out = "";
1498
+ let index = 0;
1499
+ for (;;) {
1500
+ pattern.lastIndex = index;
1501
+ const match = pattern.exec(src);
1502
+ if (!match) {
1503
+ out += src.slice(index);
1504
+ return out;
1505
+ }
1506
+ const open = match.index + match[0].length - 1;
1507
+ const close = matchParen(src, open);
1508
+ const raw = src.slice(match.index, close + 1);
1509
+ const args = splitTopLevel(src.slice(open + 1, close), ",").map((arg) => mapCalls(arg, name, transform));
1510
+ out += src.slice(index, match.index);
1511
+ out += transform(args, raw);
1512
+ index = close + 1;
1513
+ }
1514
+ }
1515
+ function matchParen(src, openIndex) {
1516
+ let depth = 0;
1517
+ for (let i = openIndex; i < src.length; i += 1) if (src[i] === "(") depth += 1;
1518
+ else if (src[i] === ")") {
1519
+ depth -= 1;
1520
+ if (depth === 0) return i;
1521
+ }
1522
+ throw new UnsupportedWgslShaderError(`unbalanced parentheses in "${src.slice(openIndex, openIndex + 40)}"`);
1523
+ }
1524
+ function splitTopLevel(src, separator) {
1525
+ const parts = [];
1526
+ let depth = 0;
1527
+ let start = 0;
1528
+ for (let i = 0; i < src.length; i += 1) {
1529
+ const c = src[i];
1530
+ if (c === "(" || c === "[" || c === "{") depth += 1;
1531
+ else if (c === ")" || c === "]" || c === "}") depth -= 1;
1532
+ else if (depth === 0 && c === separator) {
1533
+ parts.push(src.slice(start, i));
1534
+ start = i + 1;
1535
+ }
1536
+ }
1537
+ parts.push(src.slice(start));
1538
+ return parts;
1539
+ }
1540
+ function indentLines(lines) {
1541
+ return lines.map((line) => line ? ` ${line}` : line);
1542
+ }
1543
+ function mergeElse(lines) {
1544
+ const out = [];
1545
+ for (const line of lines) {
1546
+ const previous = out[out.length - 1];
1547
+ if (previous !== void 0 && previous.trim() === "}" && /^\s*else\b/.test(line)) {
1548
+ out[out.length - 1] = `${previous} ${line.trim()}`;
1549
+ continue;
1550
+ }
1551
+ out.push(line);
1552
+ }
1553
+ return out;
1554
+ }
1555
+ const MOD_POLYFILL_HEADER = `// GLSL's mod() is FLOOR-signed (x - y*floor(x/y)); WGSL's % is TRUNC-signed, so they
1556
+ // disagree wherever x goes negative — a scrolling UV crossing zero would tear. WGSL has
1557
+ // no user-defined function overloading, so the component count lives in the name.`;
1558
+ const INVERSE_POLYFILL = `// WGSL has no inverse(). Cofactor / determinant, column-major like both languages.
1559
+ fn godot_inverse3(m: mat3x3f) -> mat3x3f {
1560
+ let a = m[0];
1561
+ let b = m[1];
1562
+ let c = m[2];
1563
+ let b01 = c.z * b.y - b.z * c.y;
1564
+ let b11 = b.z * c.x - c.z * b.x;
1565
+ let b21 = c.y * b.x - b.y * c.x;
1566
+ let det = a.x * b01 + a.y * b11 + a.z * b21;
1567
+ let inv = mat3x3f(
1568
+ vec3f(b01, a.z * c.y - c.z * a.y, b.z * a.y - a.z * b.y),
1569
+ vec3f(b11, c.z * a.x - a.z * c.x, a.z * b.x - b.z * a.x),
1570
+ vec3f(b21, a.y * c.x - c.y * a.x, b.y * a.x - a.y * b.x)
1571
+ );
1572
+ return inv * (1.0 / det);
1573
+ }`;
1574
+ const VS_MAIN = `struct VsOut {
1575
+ @builtin(position) pos: vec4f,
1576
+ @location(0) uv: vec2f,
1577
+ }
1578
+
1579
+ @vertex
1580
+ fn ${VERTEX_ENTRY}(@builtin(vertex_index) index: u32) -> VsOut {
1581
+ // TRIANGLE_STRIP corner order: (-1,-1) (1,-1) (-1,1) (1,1).
1582
+ var corners = array<vec2f, 4>(
1583
+ vec2f(-1.0, -1.0),
1584
+ vec2f(1.0, -1.0),
1585
+ vec2f(-1.0, 1.0),
1586
+ vec2f(1.0, 1.0)
1587
+ );
1588
+ let xy = corners[index];
1589
+ var out: VsOut;
1590
+ out.pos = vec4f(xy, 0.0, 1.0);
1591
+ // Godot's UV is node-local with a TOP-LEFT origin; clip Y is up. The flip lives HERE,
1592
+ // in the vertex stage, so the fragment prelude below is the GLSL one MINUS its
1593
+ // "1.0 - v_uv.y" — one place to get wrong instead of one per fragment.
1594
+ out.uv = vec2f(xy.x * 0.5 + 0.5, 0.5 - xy.y * 0.5);
1595
+ return out;
1596
+ }`;
1597
+ function assembleModule(input) {
1598
+ const { parsed, analysis, plan, renames, ctx } = input;
1599
+ const structText = [
1600
+ "// One uniform struct for everything the fragment reads. Members are laid out by",
1601
+ "// WGSL's own uniform address space rules IN DECLARATION ORDER, which is exactly what",
1602
+ "// `wgslStructLayout` records — so the byte offsets a writer holds and the offsets the",
1603
+ "// compiler computes are the same numbers, with no explicit padding members to drift.",
1604
+ "struct Uniforms {",
1605
+ ...plan.members.map((member) => {
1606
+ const type = member.arrayLength !== void 0 ? `array<${member.type}, ${member.arrayLength}>` : member.type;
1607
+ return ` ${member.wgslName}: ${type},`;
1608
+ }),
1609
+ "}"
1610
+ ].join("\n");
1611
+ const bindingLines = [
1612
+ `@group(0) @binding(${BINDINGS.uniform}) var<uniform> _u: Uniforms;`,
1613
+ `@group(0) @binding(${BINDINGS.texture}) var TEXTURE: texture_2d<f32>;`,
1614
+ `@group(0) @binding(${BINDINGS.textureSampler}) var TEXTURE_smp: sampler;`
1615
+ ];
1616
+ parsed.samplers.forEach((sampler, i) => {
1617
+ const name = renameOf(sampler.name, renames);
1618
+ bindingLines.push(`@group(0) @binding(${BINDINGS.userSamplersBase + i * 2}) var ${name}: texture_2d<f32>;`);
1619
+ bindingLines.push(`@group(0) @binding(${BINDINGS.userSamplersBase + i * 2 + 1}) var ${name}_smp: sampler;`);
1620
+ });
1621
+ const consts = [];
1622
+ if (analysis.usesPi) consts.push("const PI: f32 = 3.141592653589793;");
1623
+ consts.push(...input.helpers.consts);
1624
+ const polyfills = [];
1625
+ if (ctx.needMod.size > 0) {
1626
+ const widths = [...ctx.needMod].sort((a, b) => a - b);
1627
+ polyfills.push([MOD_POLYFILL_HEADER, ...widths.map((width) => {
1628
+ const type = width === 1 ? "f32" : `vec${width}f`;
1629
+ return `fn ${width === 1 ? "godot_mod" : `godot_mod${width}`}(x: ${type}, y: ${type}) -> ${type} { return x - y * floor(x / y); }`;
1630
+ })].join("\n"));
1631
+ }
1632
+ if (ctx.needInverse.mat3) polyfills.push(INVERSE_POLYFILL);
1633
+ const fragment = [];
1634
+ const bodyTogether = input.bodyLines.join("\n");
1635
+ for (const name of plan.boolUniformNames) if (hasToken(bodyTogether, name)) fragment.push(`let ${name}: bool = (_u.${name} != 0.0);`);
1636
+ fragment.push("let raw_uv = in.uv;");
1637
+ fragment.push("let GODOT_UV = _u.uv_window.xy + raw_uv * _u.uv_window.zw;", "let UV = (GODOT_UV - vec2f(0.5)) / _u.uv_fit + vec2f(0.5);");
1638
+ if (analysis.usesScreenUv) fragment.push("let SCREEN_UV = _u.screen_origin + GODOT_UV * _u.screen_size;");
1639
+ fragment.push(analysis.opaqueColor ? "var COLOR: vec4f = vec4f(textureSampleLevel(TEXTURE, TEXTURE_smp, UV, 0.0).rgb, 1.0);" : "var COLOR: vec4f = textureSampleLevel(TEXTURE, TEXTURE_smp, UV, 0.0);");
1640
+ fragment.push(...input.varyingLines);
1641
+ fragment.push(...input.bodyLines);
1642
+ if (analysis.autoModulate) fragment.push("COLOR = COLOR * _u.modulate;");
1643
+ fragment.push("// PREMULTIPLIED. A GPUCanvasContext offers only alphaMode \"opaque\" | \"premultiplied\",", "// so this is THE canvas contract: return rgb*a with a, and let the pipeline blend", "// one / one-minus-src-alpha on colour AND alpha. Returning straight alpha here halos;", "// returning premultiplied under a src-alpha blend double-multiplies. Neither errors.", "return vec4f(COLOR.rgb * COLOR.a, COLOR.a);");
1644
+ const fsMain = [
1645
+ "@fragment",
1646
+ `fn ${FRAGMENT_ENTRY}(in: VsOut) -> @location(0) vec4f {`,
1647
+ ...indentLines(fragment),
1648
+ "}"
1649
+ ].join("\n");
1650
+ return `${[
1651
+ "// Generated by transpileGodotShaderWgsl (packages/html/src/webgpu/transpile-wgsl.ts).",
1652
+ structText,
1653
+ bindingLines.join("\n"),
1654
+ consts.length > 0 ? consts.join("\n") : "",
1655
+ polyfills.join("\n\n"),
1656
+ input.helpers.fns.join("\n\n"),
1657
+ VS_MAIN,
1658
+ fsMain
1659
+ ].filter((section) => section !== "").join("\n\n")}\n`;
1660
+ }
1661
+ //#endregion
1662
+ export { UnsupportedShaderError, UnsupportedWgslShaderError, analyzeShader, expandGodotShaderIncludes, extractFunction, hasToken, matchBrace, parseShader, promoteIntLiterals, rejectUnsupported, replaceToken, sanitizeReservedIdentifiers, shaderLogic, stripComments, transpileGodotShader, transpileGodotShaderWgsl, unwrapShaderResource, wgslStructLayout };
1663
+
1664
+ //# sourceMappingURL=index.js.map