wgsl-play 0.0.39 → 0.0.40

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.
Files changed (56) hide show
  1. package/README.md +59 -2
  2. package/dist/{WgslPlay-CSjRo-5Z.d.ts → WgslPlay-ouKx64GN.d.ts} +52 -3
  3. package/dist/WgslPlay-zK9mE5rV.js +1544 -0
  4. package/dist/WgslPlay.d.ts +2 -2
  5. package/dist/WgslPlay.js +2 -2
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +2 -2
  8. package/dist/jsx-preact.d.ts +12 -0
  9. package/dist/jsx-preact.js +0 -0
  10. package/dist/wgsl-play.js +1834 -754
  11. package/package.json +12 -8
  12. package/src/CanvasSize.ts +27 -0
  13. package/src/ComputeBuild.ts +151 -0
  14. package/src/FragmentRender.ts +147 -0
  15. package/src/PlaybackControls.ts +36 -13
  16. package/src/RenderResources.ts +132 -0
  17. package/src/Renderer.ts +112 -138
  18. package/src/ResultsPanel.ts +261 -0
  19. package/src/WgslPlay.css +80 -8
  20. package/src/WgslPlay.ts +346 -103
  21. package/src/icons/refresh.svg +1 -0
  22. package/src/jsx-preact.ts +15 -0
  23. package/src/test/BundleHydrator.test.ts +8 -11
  24. package/src/test/CanvasSize.test.ts +48 -0
  25. package/src/test/E2eHelpers.ts +117 -0
  26. package/src/test/RenderFrame.e2e.ts +33 -0
  27. package/src/test/ResultsPanel.test.ts +164 -0
  28. package/src/test/WgslPlay.basics.e2e.ts +87 -0
  29. package/src/test/WgslPlay.compute.e2e.ts +147 -0
  30. package/src/test/WgslPlay.connect.e2e.ts +101 -0
  31. package/src/test/WgslPlay.loading.e2e.ts +68 -0
  32. package/src/test/WgslPlay.resize.e2e.ts +199 -0
  33. package/src/test/WgslPlay.resources.e2e.ts +67 -0
  34. package/src/test/WgslPlay.resources.e2e.ts-snapshots/buffer-zero-init-chromium-darwin.png +0 -0
  35. package/src/test/WgslPlay.resources.e2e.ts-snapshots/texture-grid-chromium-darwin.png +0 -0
  36. package/src/test/WgslPlay.resources.e2e.ts-snapshots/texture-img-green-chromium-darwin.png +0 -0
  37. package/src/test/WgslPlay.resources.e2e.ts-snapshots/texture-img-magenta-chromium-darwin.png +0 -0
  38. package/src/test/WgslPlay.resources.e2e.ts-snapshots/texture-multi-chromium-darwin.png +0 -0
  39. package/src/test/WgslPlay.uniforms.e2e.ts +57 -0
  40. package/dist/WgslPlay-LsU6XE09.js +0 -933
  41. package/src/test/WgslPlay.e2e.ts +0 -327
  42. /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.basics.e2e.ts-snapshots}/basic-shader-chromium-darwin.png +0 -0
  43. /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.basics.e2e.ts-snapshots}/conditions-after-red-chromium-darwin.png +0 -0
  44. /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.basics.e2e.ts-snapshots}/conditions-initial-green-chromium-darwin.png +0 -0
  45. /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.connect.e2e.ts-snapshots}/connect-conditions-green-chromium-darwin.png +0 -0
  46. /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.connect.e2e.ts-snapshots}/connect-conditions-red-chromium-darwin.png +0 -0
  47. /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.connect.e2e.ts-snapshots}/connect-dynamic-npm-chromium-darwin.png +0 -0
  48. /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.connect.e2e.ts-snapshots}/connect-source-external-chromium-darwin.png +0 -0
  49. /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.connect.e2e.ts-snapshots}/connect-source-multifile-chromium-darwin.png +0 -0
  50. /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.loading.e2e.ts-snapshots}/link-import-chromium-darwin.png +0 -0
  51. /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.loading.e2e.ts-snapshots}/npm-cdn-chromium-darwin.png +0 -0
  52. /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.loading.e2e.ts-snapshots}/shader-root-internal-chromium-darwin.png +0 -0
  53. /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.loading.e2e.ts-snapshots}/shader-root-src-chromium-darwin.png +0 -0
  54. /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.loading.e2e.ts-snapshots}/static-import-chromium-darwin.png +0 -0
  55. /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.uniforms.e2e.ts-snapshots}/uniforms-initial-chromium-darwin.png +0 -0
  56. /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.uniforms.e2e.ts-snapshots}/uniforms-slider-changed-chromium-darwin.png +0 -0
package/dist/wgsl-play.js CHANGED
@@ -75,338 +75,6 @@ async function hydrateBundle(path, registry, hydrated, fetcher) {
75
75
  return placeholder;
76
76
  }
77
77
  //#endregion
78
- //#region ../../node_modules/.pnpm/fflate@0.8.2/node_modules/fflate/esm/browser.js
79
- var u8 = Uint8Array, u16 = Uint16Array, i32 = Int32Array;
80
- var fleb = new u8([
81
- 0,
82
- 0,
83
- 0,
84
- 0,
85
- 0,
86
- 0,
87
- 0,
88
- 0,
89
- 1,
90
- 1,
91
- 1,
92
- 1,
93
- 2,
94
- 2,
95
- 2,
96
- 2,
97
- 3,
98
- 3,
99
- 3,
100
- 3,
101
- 4,
102
- 4,
103
- 4,
104
- 4,
105
- 5,
106
- 5,
107
- 5,
108
- 5,
109
- 0,
110
- 0,
111
- 0,
112
- 0
113
- ]);
114
- var fdeb = new u8([
115
- 0,
116
- 0,
117
- 0,
118
- 0,
119
- 1,
120
- 1,
121
- 2,
122
- 2,
123
- 3,
124
- 3,
125
- 4,
126
- 4,
127
- 5,
128
- 5,
129
- 6,
130
- 6,
131
- 7,
132
- 7,
133
- 8,
134
- 8,
135
- 9,
136
- 9,
137
- 10,
138
- 10,
139
- 11,
140
- 11,
141
- 12,
142
- 12,
143
- 13,
144
- 13,
145
- 0,
146
- 0
147
- ]);
148
- var clim = new u8([
149
- 16,
150
- 17,
151
- 18,
152
- 0,
153
- 8,
154
- 7,
155
- 9,
156
- 6,
157
- 10,
158
- 5,
159
- 11,
160
- 4,
161
- 12,
162
- 3,
163
- 13,
164
- 2,
165
- 14,
166
- 1,
167
- 15
168
- ]);
169
- var freb = function(eb, start) {
170
- var b = new u16(31);
171
- for (var i = 0; i < 31; ++i) b[i] = start += 1 << eb[i - 1];
172
- var r = new i32(b[30]);
173
- for (var i = 1; i < 30; ++i) for (var j = b[i]; j < b[i + 1]; ++j) r[j] = j - b[i] << 5 | i;
174
- return {
175
- b,
176
- r
177
- };
178
- };
179
- var _a = freb(fleb, 2), fl = _a.b, revfl = _a.r;
180
- fl[28] = 258, revfl[258] = 28;
181
- var _b = freb(fdeb, 0), fd = _b.b;
182
- _b.r;
183
- var rev = new u16(32768);
184
- for (var i = 0; i < 32768; ++i) {
185
- var x = (i & 43690) >> 1 | (i & 21845) << 1;
186
- x = (x & 52428) >> 2 | (x & 13107) << 2;
187
- x = (x & 61680) >> 4 | (x & 3855) << 4;
188
- rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1;
189
- }
190
- var hMap = (function(cd, mb, r) {
191
- var s = cd.length;
192
- var i = 0;
193
- var l = new u16(mb);
194
- for (; i < s; ++i) if (cd[i]) ++l[cd[i] - 1];
195
- var le = new u16(mb);
196
- for (i = 1; i < mb; ++i) le[i] = le[i - 1] + l[i - 1] << 1;
197
- var co;
198
- if (r) {
199
- co = new u16(1 << mb);
200
- var rvb = 15 - mb;
201
- for (i = 0; i < s; ++i) if (cd[i]) {
202
- var sv = i << 4 | cd[i];
203
- var r_1 = mb - cd[i];
204
- var v = le[cd[i] - 1]++ << r_1;
205
- for (var m = v | (1 << r_1) - 1; v <= m; ++v) co[rev[v] >> rvb] = sv;
206
- }
207
- } else {
208
- co = new u16(s);
209
- for (i = 0; i < s; ++i) if (cd[i]) co[i] = rev[le[cd[i] - 1]++] >> 15 - cd[i];
210
- }
211
- return co;
212
- });
213
- var flt = new u8(288);
214
- for (var i = 0; i < 144; ++i) flt[i] = 8;
215
- for (var i = 144; i < 256; ++i) flt[i] = 9;
216
- for (var i = 256; i < 280; ++i) flt[i] = 7;
217
- for (var i = 280; i < 288; ++i) flt[i] = 8;
218
- var fdt = new u8(32);
219
- for (var i = 0; i < 32; ++i) fdt[i] = 5;
220
- var flrm = /* @__PURE__ */ hMap(flt, 9, 1), fdrm = /* @__PURE__ */ hMap(fdt, 5, 1);
221
- var max = function(a) {
222
- var m = a[0];
223
- for (var i = 1; i < a.length; ++i) if (a[i] > m) m = a[i];
224
- return m;
225
- };
226
- var bits = function(d, p, m) {
227
- var o = p / 8 | 0;
228
- return (d[o] | d[o + 1] << 8) >> (p & 7) & m;
229
- };
230
- var bits16 = function(d, p) {
231
- var o = p / 8 | 0;
232
- return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7);
233
- };
234
- var shft = function(p) {
235
- return (p + 7) / 8 | 0;
236
- };
237
- var slc = function(v, s, e) {
238
- if (s == null || s < 0) s = 0;
239
- if (e == null || e > v.length) e = v.length;
240
- return new u8(v.subarray(s, e));
241
- };
242
- var ec = [
243
- "unexpected EOF",
244
- "invalid block type",
245
- "invalid length/literal",
246
- "invalid distance",
247
- "stream finished",
248
- "no stream handler",
249
- ,
250
- "no callback",
251
- "invalid UTF-8 data",
252
- "extra field too long",
253
- "date not in range 1980-2099",
254
- "filename too long",
255
- "stream finishing",
256
- "invalid zip data"
257
- ];
258
- var err = function(ind, msg, nt) {
259
- var e = new Error(msg || ec[ind]);
260
- e.code = ind;
261
- if (Error.captureStackTrace) Error.captureStackTrace(e, err);
262
- if (!nt) throw e;
263
- return e;
264
- };
265
- var inflt = function(dat, st, buf, dict) {
266
- var sl = dat.length, dl = dict ? dict.length : 0;
267
- if (!sl || st.f && !st.l) return buf || new u8(0);
268
- var noBuf = !buf;
269
- var resize = noBuf || st.i != 2;
270
- var noSt = st.i;
271
- if (noBuf) buf = new u8(sl * 3);
272
- var cbuf = function(l) {
273
- var bl = buf.length;
274
- if (l > bl) {
275
- var nbuf = new u8(Math.max(bl * 2, l));
276
- nbuf.set(buf);
277
- buf = nbuf;
278
- }
279
- };
280
- var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n;
281
- var tbts = sl * 8;
282
- do {
283
- if (!lm) {
284
- final = bits(dat, pos, 1);
285
- var type = bits(dat, pos + 1, 3);
286
- pos += 3;
287
- if (!type) {
288
- var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l;
289
- if (t > sl) {
290
- if (noSt) err(0);
291
- break;
292
- }
293
- if (resize) cbuf(bt + l);
294
- buf.set(dat.subarray(s, t), bt);
295
- st.b = bt += l, st.p = pos = t * 8, st.f = final;
296
- continue;
297
- } else if (type == 1) lm = flrm, dm = fdrm, lbt = 9, dbt = 5;
298
- else if (type == 2) {
299
- var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4;
300
- var tl = hLit + bits(dat, pos + 5, 31) + 1;
301
- pos += 14;
302
- var ldt = new u8(tl);
303
- var clt = new u8(19);
304
- for (var i = 0; i < hcLen; ++i) clt[clim[i]] = bits(dat, pos + i * 3, 7);
305
- pos += hcLen * 3;
306
- var clb = max(clt), clbmsk = (1 << clb) - 1;
307
- var clm = hMap(clt, clb, 1);
308
- for (var i = 0; i < tl;) {
309
- var r = clm[bits(dat, pos, clbmsk)];
310
- pos += r & 15;
311
- var s = r >> 4;
312
- if (s < 16) ldt[i++] = s;
313
- else {
314
- var c = 0, n = 0;
315
- if (s == 16) n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1];
316
- else if (s == 17) n = 3 + bits(dat, pos, 7), pos += 3;
317
- else if (s == 18) n = 11 + bits(dat, pos, 127), pos += 7;
318
- while (n--) ldt[i++] = c;
319
- }
320
- }
321
- var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit);
322
- lbt = max(lt);
323
- dbt = max(dt);
324
- lm = hMap(lt, lbt, 1);
325
- dm = hMap(dt, dbt, 1);
326
- } else err(1);
327
- if (pos > tbts) {
328
- if (noSt) err(0);
329
- break;
330
- }
331
- }
332
- if (resize) cbuf(bt + 131072);
333
- var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1;
334
- var lpos = pos;
335
- for (;; lpos = pos) {
336
- var c = lm[bits16(dat, pos) & lms], sym = c >> 4;
337
- pos += c & 15;
338
- if (pos > tbts) {
339
- if (noSt) err(0);
340
- break;
341
- }
342
- if (!c) err(2);
343
- if (sym < 256) buf[bt++] = sym;
344
- else if (sym == 256) {
345
- lpos = pos, lm = null;
346
- break;
347
- } else {
348
- var add = sym - 254;
349
- if (sym > 264) {
350
- var i = sym - 257, b = fleb[i];
351
- add = bits(dat, pos, (1 << b) - 1) + fl[i];
352
- pos += b;
353
- }
354
- var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
355
- if (!d) err(3);
356
- pos += d & 15;
357
- var dt = fd[dsym];
358
- if (dsym > 3) {
359
- var b = fdeb[dsym];
360
- dt += bits16(dat, pos) & (1 << b) - 1, pos += b;
361
- }
362
- if (pos > tbts) {
363
- if (noSt) err(0);
364
- break;
365
- }
366
- if (resize) cbuf(bt + 131072);
367
- var end = bt + add;
368
- if (bt < dt) {
369
- var shift = dl - dt, dend = Math.min(dt, end);
370
- if (shift + bt < 0) err(3);
371
- for (; bt < dend; ++bt) buf[bt] = dict[shift + bt];
372
- }
373
- for (; bt < end; ++bt) buf[bt] = buf[bt - dt];
374
- }
375
- }
376
- st.l = lm, st.p = lpos, st.b = bt, st.f = final;
377
- if (lm) final = 1, st.m = lbt, st.d = dm, st.n = dbt;
378
- } while (!final);
379
- return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt);
380
- };
381
- var et = /* @__PURE__ */ new u8(0);
382
- var gzs = function(d) {
383
- if (d[0] != 31 || d[1] != 139 || d[2] != 8) err(6, "invalid gzip data");
384
- var flg = d[3];
385
- var st = 10;
386
- if (flg & 4) st += (d[10] | d[11] << 8) + 2;
387
- for (var zs = (flg >> 3 & 1) + (flg >> 4 & 1); zs > 0; zs -= !d[st++]);
388
- return st + (flg & 2);
389
- };
390
- var gzl = function(d) {
391
- var l = d.length;
392
- return (d[l - 4] | d[l - 3] << 8 | d[l - 2] << 16 | d[l - 1] << 24) >>> 0;
393
- };
394
- /**
395
- * Expands GZIP data
396
- * @param data The data to decompress
397
- * @param opts The decompression options
398
- * @returns The decompressed version of the data
399
- */
400
- function gunzipSync(data, opts) {
401
- var st = gzs(data);
402
- if (st + 8 > data.length) err(6, "invalid gzip data");
403
- return inflt(data.subarray(st, -8), { i: 2 }, opts && opts.out || new u8(gzl(data)), opts && opts.dictionary);
404
- }
405
- var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder();
406
- try {
407
- td.decode(et, { stream: true });
408
- } catch (e) {}
409
- //#endregion
410
78
  //#region ../../node_modules/.pnpm/nanotar@0.2.0/node_modules/nanotar/dist/index.mjs
411
79
  const TAR_TYPE_FILE = 0;
412
80
  const TAR_TYPE_DIR = 5;
@@ -458,6 +126,13 @@ function parseTar(data, opts) {
458
126
  }
459
127
  return files;
460
128
  }
129
+ async function parseTarGzip(data, opts = {}) {
130
+ const stream = new ReadableStream({ start(controller) {
131
+ controller.enqueue(new Uint8Array(data));
132
+ controller.close();
133
+ } }).pipeThrough(new DecompressionStream(opts.compression ?? "gzip"));
134
+ return parseTar(await new Response(stream).arrayBuffer(), opts);
135
+ }
461
136
  function _readString(buffer, offset, size) {
462
137
  const view = new Uint8Array(buffer, offset, size);
463
138
  const i = view.indexOf(0);
@@ -521,7 +196,7 @@ function memoAsync(fn) {
521
196
  async function fetchAndExtractTgzRaw(tgzUrl) {
522
197
  const response = await fetch(tgzUrl);
523
198
  if (!response.ok) throw new Error(`Failed to fetch package: HTTP ${response.status}`);
524
- return parseTar(gunzipSync(new Uint8Array(await response.arrayBuffer())));
199
+ return parseTarGzip(new Uint8Array(await response.arrayBuffer()));
525
200
  }
526
201
  async function npmPackageToUrlRaw(packageName) {
527
202
  const metadataUrl = `https://registry.npmjs.org/${packageName}`;
@@ -771,12 +446,13 @@ const isBrowser = "document" in globalThis;
771
446
  /** Throw an error with an embedded source map so that browser users can
772
447
  * click on the error in the browser debug console and see the wesl source code. */
773
448
  function throwClickableError(params) {
774
- const { url, text, lineNumber, lineColumn, length, error } = params;
449
+ const { url, text, lineNumber, lineColumn, length, offset, error } = params;
775
450
  const weslLocation = {
776
451
  file: url,
777
452
  line: lineNumber,
778
453
  column: lineColumn,
779
- length
454
+ length,
455
+ offset
780
456
  };
781
457
  error.weslLocation = weslLocation;
782
458
  if (!isBrowser) throw error;
@@ -837,6 +513,7 @@ function failIdentElem(identElem, msg = "") {
837
513
  lineNumber,
838
514
  lineColumn,
839
515
  length: end - start,
516
+ offset: start,
840
517
  error: new Error(detailedMessage)
841
518
  });
842
519
  }
@@ -1112,7 +789,7 @@ function lowerAndEmitElem(e, ctx) {
1112
789
  case "let":
1113
790
  case "statement":
1114
791
  case "continuing":
1115
- emitStatement$1(e, ctx);
792
+ emitStatement(e, ctx);
1116
793
  return;
1117
794
  case "override":
1118
795
  case "const":
@@ -1150,7 +827,7 @@ function emitModule(e, ctx) {
1150
827
  lowerAndEmitElem(child, ctx);
1151
828
  }
1152
829
  }
1153
- function emitStatement$1(e, ctx) {
830
+ function emitStatement(e, ctx) {
1154
831
  if (!(e.contents.length > 0 && e.contents[0].kind === "attribute")) emitAttributes(e.attributes, ctx);
1155
832
  emitContents(e, ctx);
1156
833
  }
@@ -4529,20 +4206,13 @@ async function link(params) {
4529
4206
  }
4530
4207
  /** linker api for benchmarking */
4531
4208
  function _linkSync(params) {
4532
- const { weslSrc, libs = [], packageName, debugWeslRoot } = params;
4533
- const { resolver } = params;
4534
- const resolvers = [];
4535
- if (resolver) resolvers.push(resolver);
4536
- else if (weslSrc) resolvers.push(new RecordResolver(weslSrc, {
4209
+ const { weslSrc, libs = [], packageName, debugWeslRoot, resolver } = params;
4210
+ if (!resolver && !weslSrc) throw new Error("Either resolver or weslSrc must be provided");
4211
+ const allResolvers = [resolver ?? new RecordResolver(weslSrc, {
4537
4212
  packageName,
4538
4213
  debugWeslRoot
4539
- }));
4540
- else throw new Error("Either resolver or weslSrc must be provided");
4541
- if (libs.length > 0) {
4542
- const libResolvers = createLibraryResolvers(libs, debugWeslRoot);
4543
- resolvers.push(...libResolvers);
4544
- }
4545
- const finalResolver = resolvers.length === 1 ? resolvers[0] : new CompositeResolver(resolvers);
4214
+ }), ...createLibraryResolvers(libs, debugWeslRoot)];
4215
+ const finalResolver = allResolvers.length === 1 ? allResolvers[0] : new CompositeResolver(allResolvers);
4546
4216
  return linkRegistry({
4547
4217
  ...params,
4548
4218
  resolver: finalResolver
@@ -4626,6 +4296,7 @@ function setupVirtualLibs(virtualLibs, constants) {
4626
4296
  }
4627
4297
  return libs && mapValues(libs, (fn) => ({ fn }));
4628
4298
  }
4299
+ /** Run registered transform plugins over the bound AST. */
4629
4300
  function applyTransformPlugins(rootModule, globalNames, config) {
4630
4301
  const { moduleElem, srcModule } = rootModule;
4631
4302
  const startAst = {
@@ -4638,53 +4309,38 @@ function applyTransformPlugins(rootModule, globalNames, config) {
4638
4309
  }
4639
4310
  /** Assemble WGSL output from prologue statements, root module, and imported declarations. */
4640
4311
  function emitWgsl(rootModuleElem, srcModule, newDecls, newStatements, conditions = {}) {
4641
- const prologueBuilders = newStatements.map((s) => emitStatement(s, conditions));
4642
- const rootBuilder = new SrcMapBuilder({
4643
- text: srcModule.src,
4644
- path: srcModule.debugFilePath
4645
- });
4312
+ const prologueBuilders = newStatements.map((s) => emitElem(s.srcModule, s.elem, conditions, { addNl: true }));
4313
+ const rootBuilder = builderFromModule(srcModule);
4646
4314
  lowerAndEmit({
4647
4315
  srcBuilder: rootBuilder,
4648
4316
  rootElems: [rootModuleElem],
4649
4317
  conditions,
4650
4318
  extracting: false
4651
4319
  });
4652
- const declBuilders = newDecls.map((decl) => emitDecl(decl, conditions));
4320
+ const declBuilders = newDecls.map((decl) => emitElem(decl.srcModule, decl.declElem, conditions, { skipConditionalFiltering: true }));
4653
4321
  return [
4654
4322
  ...prologueBuilders,
4655
4323
  rootBuilder,
4656
4324
  ...declBuilders
4657
4325
  ];
4658
4326
  }
4659
- function emitStatement(s, conditions) {
4660
- const { elem, srcModule } = s;
4661
- const { src: text, debugFilePath: path } = srcModule;
4662
- const builder = new SrcMapBuilder({
4663
- text,
4664
- path
4665
- });
4327
+ /** Emit a single element (prologue statement or imported declaration) into a SrcMapBuilder. */
4328
+ function emitElem(srcModule, elem, conditions, opts = {}) {
4329
+ const builder = builderFromModule(srcModule);
4666
4330
  lowerAndEmit({
4667
4331
  srcBuilder: builder,
4668
4332
  rootElems: [elem],
4669
- conditions
4333
+ conditions,
4334
+ skipConditionalFiltering: opts.skipConditionalFiltering
4670
4335
  });
4671
- builder.addNl();
4336
+ if (opts.addNl) builder.addNl();
4672
4337
  return builder;
4673
4338
  }
4674
- /** Skip conditional filtering because findValidRootDecls already validated these declarations */
4675
- function emitDecl(decl, conditions) {
4676
- const { src: text, debugFilePath: path } = decl.srcModule;
4677
- const builder = new SrcMapBuilder({
4678
- text,
4679
- path
4680
- });
4681
- lowerAndEmit({
4682
- srcBuilder: builder,
4683
- rootElems: [decl.declElem],
4684
- conditions,
4685
- skipConditionalFiltering: true
4339
+ function builderFromModule(srcModule) {
4340
+ return new SrcMapBuilder({
4341
+ text: srcModule.src,
4342
+ path: srcModule.debugFilePath
4686
4343
  });
4687
- return builder;
4688
4344
  }
4689
4345
  matchOneOf(textureStorageTypes);
4690
4346
  matchOneOf(sampledTextureTypes);
@@ -4730,6 +4386,7 @@ function makeWeslDevice(device) {
4730
4386
  lineNumber: message.lineNum,
4731
4387
  lineColumn: message.linePos,
4732
4388
  length: message.length,
4389
+ offset: message.offset,
4733
4390
  error: /* @__PURE__ */ new Error(message.type + ": " + message.message)
4734
4391
  });
4735
4392
  else console.error(ev.error.message);
@@ -5158,125 +4815,32 @@ function resetConfig() {
5158
4815
  globalConfig = { ...defaultConfig };
5159
4816
  }
5160
4817
  //#endregion
5161
- //#region src/ErrorOverlay.ts
5162
- /** Manages an error overlay element within a shadow DOM. */
5163
- var ErrorOverlay = class {
5164
- el;
5165
- _message = null;
5166
- constructor(container) {
5167
- this.el = document.createElement("div");
5168
- this.el.className = "error-overlay";
5169
- container.appendChild(this.el);
5170
- }
5171
- show(message) {
5172
- this._message = message;
5173
- this.el.textContent = message;
5174
- this.el.classList.add("visible");
5175
- console.error("[wgsl-play]", message);
5176
- }
5177
- hide() {
5178
- this._message = null;
5179
- this.el.classList.remove("visible");
5180
- }
5181
- get visible() {
5182
- return this.el.classList.contains("visible");
5183
- }
5184
- get message() {
5185
- return this._message;
5186
- }
5187
- };
5188
- //#endregion
5189
- //#region src/icons/backToStart.svg?raw
5190
- var backToStart_default = "<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\"><rect x=\"4\" y=\"4\" width=\"3\" height=\"16\" fill=\"currentColor\"/><polygon points=\"20,4 9,12 20,20\" fill=\"currentColor\"/></svg>\n";
5191
- //#endregion
5192
- //#region src/icons/expand.svg?raw
5193
- var expand_default = "<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\"><path d=\"M3 3h6v2H5v4H3V3zm12 0h6v6h-2V5h-4V3zM3 15h2v4h4v2H3v-6zm16 4h-4v2h6v-6h-2v4z\" fill=\"currentColor\"/></svg>\n";
5194
- //#endregion
5195
- //#region src/icons/pause.svg?raw
5196
- var pause_default = "<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\"><rect x=\"5\" y=\"4\" width=\"4\" height=\"16\" fill=\"currentColor\"/><rect x=\"15\" y=\"4\" width=\"4\" height=\"16\" fill=\"currentColor\"/></svg>\n";
5197
- //#endregion
5198
- //#region src/icons/play.svg?raw
5199
- var play_default = "<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\"><polygon points=\"6,4 20,12 6,20\" fill=\"currentColor\"/></svg>\n";
5200
- //#endregion
5201
- //#region src/icons/shrink.svg?raw
5202
- var shrink_default = "<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\"><path d=\"M9 3v6H3v-2h4V3h2zm6 0h2v4h4v2h-6V3zM3 15h6v6H7v-4H3v-2zm12 0h6v2h-4v4h-2v-6z\" fill=\"currentColor\"/></svg>\n";
5203
- //#endregion
5204
- //#region src/PlaybackControls.ts
5205
- /** Playback controls overlay for wgsl-play. */
5206
- var PlaybackControls = class {
5207
- container;
5208
- playPauseBtn;
5209
- fullscreenBtn;
5210
- playing = true;
5211
- constructor(shadow, onPlay, onPause, onRewind, onFullscreen) {
5212
- this.container = document.createElement("div");
5213
- this.container.className = "controls";
5214
- this.fullscreenBtn = document.createElement("button");
5215
- this.fullscreenBtn.innerHTML = expand_default;
5216
- this.fullscreenBtn.addEventListener("click", onFullscreen);
5217
- const rewindBtn = document.createElement("button");
5218
- rewindBtn.innerHTML = backToStart_default;
5219
- rewindBtn.addEventListener("click", onRewind);
5220
- this.playPauseBtn = document.createElement("button");
5221
- this.playPauseBtn.innerHTML = pause_default;
5222
- this.playPauseBtn.addEventListener("click", () => this.playing ? onPause() : onPlay());
5223
- this.container.append(this.fullscreenBtn, rewindBtn, this.playPauseBtn);
5224
- shadow.appendChild(this.container);
5225
- }
5226
- setPlaying(playing) {
5227
- this.playing = playing;
5228
- this.playPauseBtn.innerHTML = playing ? pause_default : play_default;
5229
- }
5230
- setFullscreen(isFullscreen) {
5231
- this.fullscreenBtn.innerHTML = isFullscreen ? shrink_default : expand_default;
5232
- }
5233
- show() {
5234
- this.container.style.display = "";
5235
- }
5236
- hide() {
5237
- this.container.style.display = "none";
5238
- }
5239
- };
5240
- //#endregion
5241
- //#region ../wesl-gpu/src/DeviceCache.ts
4818
+ //#region ../wesl-gpu/src/ErrorScopes.ts
5242
4819
  /**
5243
- * WeakMap cache for GPUDevice-based caching
5244
- *
5245
- * GPUDevices are weakly held to avoid memory leaks.
4820
+ * Runs a function with WebGPU error scopes, automatically handling push/pop/check.
4821
+ * JS errors from `fn` propagate unchanged. GPU errors (validation, OOM, internal)
4822
+ * are rethrown as `Error` with the original `GPUError` preserved as `.cause`.
5246
4823
  */
5247
- var DeviceCache = class {
5248
- cache = /* @__PURE__ */ new WeakMap();
5249
- get(device, key) {
5250
- return this.cache.get(device)?.get(key);
5251
- }
5252
- set(device, key, value) {
5253
- let deviceCache = this.cache.get(device);
5254
- if (!deviceCache) {
5255
- deviceCache = /* @__PURE__ */ new Map();
5256
- this.cache.set(device, deviceCache);
5257
- }
5258
- deviceCache.set(key, value);
4824
+ async function withErrorScopes(device, fn) {
4825
+ device.pushErrorScope("internal");
4826
+ device.pushErrorScope("out-of-memory");
4827
+ device.pushErrorScope("validation");
4828
+ let result;
4829
+ let jsError;
4830
+ try {
4831
+ result = await fn();
4832
+ } catch (e) {
4833
+ jsError = e;
5259
4834
  }
5260
- };
5261
- new DeviceCache();
5262
- new DeviceCache();
5263
- //#endregion
5264
- //#region ../wesl-gpu/src/FullscreenVertex.ts
5265
- /** Fullscreen triangle vertex shader that covers viewport with 3 vertices, no vertex buffer needed */
5266
- const fullscreenTriangleVertex = `
5267
- @vertex
5268
- fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4f {
5269
- // Covers viewport with 3 vertices, no vertex buffer needed
5270
- var pos: vec2f;
5271
- if (idx == 0u) {
5272
- pos = vec2f(-1.0, -1.0);
5273
- } else if (idx == 1u) {
5274
- pos = vec2f(3.0, -1.0);
5275
- } else {
5276
- pos = vec2f(-1.0, 3.0);
5277
- }
5278
- return vec4f(pos, 0.0, 1.0);
5279
- }`;
4835
+ const validationError = await device.popErrorScope();
4836
+ const oomError = await device.popErrorScope();
4837
+ const internalError = await device.popErrorScope();
4838
+ if (jsError) throw jsError;
4839
+ if (validationError) throw new Error(`WebGPU validation error: ${validationError.message}`, { cause: validationError });
4840
+ if (oomError) throw new Error(`WebGPU out-of-memory error: ${oomError.message}`, { cause: oomError });
4841
+ if (internalError) throw new Error(`WebGPU internal error: ${internalError.message}`, { cause: internalError });
4842
+ return result;
4843
+ }
5280
4844
  //#endregion
5281
4845
  //#region ../wesl-reflect/src/Annotations.ts
5282
4846
  /** Find a StandardAttribute by name on an element with attributes. */
@@ -5315,7 +4879,6 @@ function originalTypeName(typeRef) {
5315
4879
  }
5316
4880
  //#endregion
5317
4881
  //#region ../wesl-reflect/src/StructLayout.ts
5318
- const roundUp = (align, n) => Math.ceil(n / align) * align;
5319
4882
  const typeTable = {
5320
4883
  f32: scalar(4),
5321
4884
  i32: scalar(4),
@@ -5354,16 +4917,69 @@ const typeTable = {
5354
4917
  };
5355
4918
  /** Compute byte offsets and buffer size for a bound WGSL struct.
5356
4919
  * Nested structs resolved via refersTo links. Conditional members filtered when conditions provided. */
5357
- function structLayout(struct, conditions) {
5358
- return membersLayout(conditions ? filterValidElements(struct.members, conditions) : struct.members, conditions);
4920
+ function structLayout(struct, conditions, structs) {
4921
+ return membersLayout(conditions ? filterValidElements(struct.members, conditions) : struct.members, conditions, structs);
4922
+ }
4923
+ /** Build a StructRegistry from all top-level struct declarations in `ast`. */
4924
+ function buildStructRegistry(ast) {
4925
+ const reg = /* @__PURE__ */ new Map();
4926
+ for (const e of ast.moduleElem.contents) if (e.kind === "struct") reg.set(e.name.ident.originalName, e);
4927
+ return reg;
4928
+ }
4929
+ /** Resolve alignment and size for any host-shareable typeRef (primitive, array, or nested struct). */
4930
+ function typeRefLayout(typeRef, conditions, structs) {
4931
+ const name = originalTypeName(typeRef);
4932
+ const primitive = typeTable[name];
4933
+ if (primitive) return primitive;
4934
+ if (name === "array") {
4935
+ const params = typeRef.templateParams;
4936
+ if (!params?.length) throw new Error("array type missing template parameters");
4937
+ const elem = elemTypeInfo(params[0], conditions, structs);
4938
+ const stride = roundUp(elem.alignment, elem.size);
4939
+ const p = params[1];
4940
+ const count = p && "value" in p ? Number(p.value) : 0;
4941
+ return {
4942
+ alignment: elem.alignment,
4943
+ size: count * stride
4944
+ };
4945
+ }
4946
+ const nested = resolveStructInfo(typeRef.name, conditions, structs, name);
4947
+ if (nested) return nested;
4948
+ throw new Error(`unsupported type for layout: '${name}'`);
4949
+ }
4950
+ /** Round `n` up to the next multiple of `align`. */
4951
+ function roundUp(align, n) {
4952
+ return Math.ceil(n / align) * align;
4953
+ }
4954
+ function scalar(size) {
4955
+ return {
4956
+ alignment: size,
4957
+ size
4958
+ };
4959
+ }
4960
+ /** vec2: align=2*S, size=2*S; vec3: align=4*S, size=3*S; vec4: align=4*S, size=4*S */
4961
+ function vec(n, scalarSize) {
4962
+ return {
4963
+ alignment: (n === 3 ? 4 : n) * scalarSize,
4964
+ size: n * scalarSize
4965
+ };
4966
+ }
4967
+ /** matCxR<T>: C columns of vecR<T>, column stride = roundUp(colAlign, colSize) */
4968
+ function mat(cols, rows, scalarSize) {
4969
+ const col = vec(rows, scalarSize);
4970
+ const colStride = roundUp(col.alignment, col.size);
4971
+ return {
4972
+ alignment: col.alignment,
4973
+ size: cols * colStride
4974
+ };
5359
4975
  }
5360
4976
  /** Compute layout from a flat member list (used internally and for nested resolution). */
5361
- function membersLayout(members, conditions) {
4977
+ function membersLayout(members, conditions, structs) {
5362
4978
  let offset = 0;
5363
4979
  let structAlign = 1;
5364
4980
  const fields = [];
5365
4981
  for (const m of members) {
5366
- let { alignment, size } = memberTypeInfo(m.typeRef, conditions);
4982
+ let { alignment, size } = typeRefLayout(m.typeRef, conditions, structs);
5367
4983
  const alignAttr = findAnnotation(m, "align");
5368
4984
  if (alignAttr) {
5369
4985
  const [n] = numericParams(alignAttr);
@@ -5389,73 +5005,38 @@ function membersLayout(members, conditions) {
5389
5005
  alignment: structAlign
5390
5006
  };
5391
5007
  }
5392
- /** Resolve alignment and size for a member's type (primitive, array, or nested struct). */
5393
- function memberTypeInfo(typeRef, conditions) {
5394
- const name = originalTypeName(typeRef);
5395
- const primitive = typeTable[name];
5396
- if (primitive) return primitive;
5397
- if (name === "array") {
5398
- const params = typeRef.templateParams;
5399
- if (!params?.length) throw new Error("array type missing template parameters");
5400
- const elem = elemTypeInfo(params[0], conditions);
5401
- const stride = roundUp(elem.alignment, elem.size);
5402
- const p = params[1];
5403
- const count = p && "value" in p ? Number(p.value) : 0;
5404
- return {
5405
- alignment: elem.alignment,
5406
- size: count * stride
5407
- };
5408
- }
5409
- const nested = resolveStructInfo(typeRef.name, conditions);
5410
- if (nested) return nested;
5411
- throw new Error(`unsupported type for layout: '${name}'`);
5412
- }
5413
5008
  /** Extract type info from an array template param (TypeRefElem or RefIdentElem). */
5414
- function elemTypeInfo(param, conditions) {
5415
- if (param.kind === "type") return memberTypeInfo(param, conditions);
5416
- const p = param;
5417
- const ident = p.ident;
5418
- const typeName = ident?.originalName ?? p.originalName;
5009
+ function elemTypeInfo(param, conditions, structs) {
5010
+ if (param.kind === "type") return typeRefLayout(param, conditions, structs);
5011
+ const ident = param.kind === "ref" ? param.ident : void 0;
5012
+ const typeName = ident?.originalName;
5419
5013
  if (!typeName) throw new Error("cannot resolve array element type");
5420
5014
  const primitive = typeTable[typeName];
5421
5015
  if (primitive) return primitive;
5422
- if (ident) {
5423
- const nested = resolveStructInfo(ident, conditions);
5424
- if (nested) return nested;
5425
- }
5016
+ const nested = resolveStructInfo(ident, conditions, structs, typeName);
5017
+ if (nested) return nested;
5426
5018
  throw new Error(`unsupported type for layout: '${typeName}'`);
5427
5019
  }
5428
- /** Follow refersTo links to resolve a RefIdent to a struct and compute its layout. */
5429
- function resolveStructInfo(ident, conditions) {
5430
- const decl = ident.refersTo;
5431
- if (decl?.kind !== "decl") return void 0;
5432
- const elem = decl.declElem;
5433
- if (elem?.kind !== "struct") return void 0;
5434
- const inner = structLayout(elem, conditions);
5020
+ /** Resolve a struct typeRef via refersTo (bound) or struct registry (unbound). */
5021
+ function resolveStructInfo(ident, conditions, structs, fallbackName) {
5022
+ const fromRefersTo = ident && structFromRefersTo(ident);
5023
+ const name = ident?.originalName ?? fallbackName;
5024
+ const fromRegistry = name ? structs?.get(name) : void 0;
5025
+ const elem = fromRefersTo ?? fromRegistry;
5026
+ if (!elem) return void 0;
5027
+ const inner = structLayout(elem, conditions, structs);
5435
5028
  return {
5436
5029
  alignment: inner.alignment,
5437
5030
  size: inner.bufferSize
5438
5031
  };
5439
5032
  }
5440
- function scalar(size) {
5441
- return {
5442
- alignment: size,
5443
- size
5444
- };
5445
- }
5446
- function vec(n, scalarSize) {
5447
- return {
5448
- alignment: (n === 3 ? 4 : n) * scalarSize,
5449
- size: n * scalarSize
5450
- };
5451
- }
5452
- function mat(cols, rows, scalarSize) {
5453
- const col = vec(rows, scalarSize);
5454
- const colStride = roundUp(col.alignment, col.size);
5455
- return {
5456
- alignment: col.alignment,
5457
- size: cols * colStride
5458
- };
5033
+ /** Walk a bound RefIdent's refersTo chain to find its StructElem, if any. */
5034
+ function structFromRefersTo(ident) {
5035
+ const decl = ident.refersTo;
5036
+ if (decl?.kind !== "decl") return void 0;
5037
+ const elem = decl.declElem;
5038
+ if (elem?.kind !== "struct") return void 0;
5039
+ return elem;
5459
5040
  }
5460
5041
  //#endregion
5461
5042
  //#region ../wesl-reflect/src/UniformAnnotations.ts
@@ -5559,11 +5140,540 @@ function classifyMember(m, base) {
5559
5140
  } };
5560
5141
  }
5561
5142
  //#endregion
5562
- //#region ../wesl-gpu/src/UniformsVirtualLib.ts
5563
- /** Default env:: module when no @uniforms struct is declared. */
5564
- const defaultEnvSource = `
5565
- struct Uniforms {
5566
- resolution: vec2f,
5143
+ //#region ../wesl-reflect/src/TypeShape.ts
5144
+ const scalarTable = {
5145
+ f32: "f32",
5146
+ i32: "i32",
5147
+ u32: "u32",
5148
+ f16: "f16",
5149
+ bool: "bool"
5150
+ };
5151
+ const vecTable = {
5152
+ vec2f: {
5153
+ n: 2,
5154
+ component: "f32"
5155
+ },
5156
+ vec3f: {
5157
+ n: 3,
5158
+ component: "f32"
5159
+ },
5160
+ vec4f: {
5161
+ n: 4,
5162
+ component: "f32"
5163
+ },
5164
+ vec2i: {
5165
+ n: 2,
5166
+ component: "i32"
5167
+ },
5168
+ vec3i: {
5169
+ n: 3,
5170
+ component: "i32"
5171
+ },
5172
+ vec4i: {
5173
+ n: 4,
5174
+ component: "i32"
5175
+ },
5176
+ vec2u: {
5177
+ n: 2,
5178
+ component: "u32"
5179
+ },
5180
+ vec3u: {
5181
+ n: 3,
5182
+ component: "u32"
5183
+ },
5184
+ vec4u: {
5185
+ n: 4,
5186
+ component: "u32"
5187
+ },
5188
+ vec2h: {
5189
+ n: 2,
5190
+ component: "f16"
5191
+ },
5192
+ vec3h: {
5193
+ n: 3,
5194
+ component: "f16"
5195
+ },
5196
+ vec4h: {
5197
+ n: 4,
5198
+ component: "f16"
5199
+ }
5200
+ };
5201
+ const matTable = {
5202
+ mat2x2f: {
5203
+ cols: 2,
5204
+ rows: 2,
5205
+ component: "f32"
5206
+ },
5207
+ mat3x2f: {
5208
+ cols: 3,
5209
+ rows: 2,
5210
+ component: "f32"
5211
+ },
5212
+ mat4x2f: {
5213
+ cols: 4,
5214
+ rows: 2,
5215
+ component: "f32"
5216
+ },
5217
+ mat2x3f: {
5218
+ cols: 2,
5219
+ rows: 3,
5220
+ component: "f32"
5221
+ },
5222
+ mat3x3f: {
5223
+ cols: 3,
5224
+ rows: 3,
5225
+ component: "f32"
5226
+ },
5227
+ mat4x3f: {
5228
+ cols: 4,
5229
+ rows: 3,
5230
+ component: "f32"
5231
+ },
5232
+ mat2x4f: {
5233
+ cols: 2,
5234
+ rows: 4,
5235
+ component: "f32"
5236
+ },
5237
+ mat3x4f: {
5238
+ cols: 3,
5239
+ rows: 4,
5240
+ component: "f32"
5241
+ },
5242
+ mat4x4f: {
5243
+ cols: 4,
5244
+ rows: 4,
5245
+ component: "f32"
5246
+ },
5247
+ mat2x2h: {
5248
+ cols: 2,
5249
+ rows: 2,
5250
+ component: "f16"
5251
+ },
5252
+ mat3x2h: {
5253
+ cols: 3,
5254
+ rows: 2,
5255
+ component: "f16"
5256
+ },
5257
+ mat4x2h: {
5258
+ cols: 4,
5259
+ rows: 2,
5260
+ component: "f16"
5261
+ },
5262
+ mat2x3h: {
5263
+ cols: 2,
5264
+ rows: 3,
5265
+ component: "f16"
5266
+ },
5267
+ mat3x3h: {
5268
+ cols: 3,
5269
+ rows: 3,
5270
+ component: "f16"
5271
+ },
5272
+ mat4x3h: {
5273
+ cols: 4,
5274
+ rows: 3,
5275
+ component: "f16"
5276
+ },
5277
+ mat2x4h: {
5278
+ cols: 2,
5279
+ rows: 4,
5280
+ component: "f16"
5281
+ },
5282
+ mat3x4h: {
5283
+ cols: 3,
5284
+ rows: 4,
5285
+ component: "f16"
5286
+ },
5287
+ mat4x4h: {
5288
+ cols: 4,
5289
+ rows: 4,
5290
+ component: "f16"
5291
+ }
5292
+ };
5293
+ const addressSpaces = [
5294
+ "function",
5295
+ "private",
5296
+ "workgroup",
5297
+ "uniform",
5298
+ "storage",
5299
+ "handle"
5300
+ ];
5301
+ /** Thrown by varReflection / typeShape when a type cannot be reflected. */
5302
+ var TypeShapeError = class extends Error {
5303
+ varName;
5304
+ constructor(message, varName) {
5305
+ super(message);
5306
+ this.name = "TypeShapeError";
5307
+ this.varName = varName;
5308
+ }
5309
+ };
5310
+ /** Reflect a top-level `var` declaration: type tree, address space, access mode, and bindings. */
5311
+ function varReflection(ast, varName) {
5312
+ const gvar = findGlobalVar(ast, varName);
5313
+ if (!gvar) throw new TypeShapeError(`var '${varName}' not found`, varName);
5314
+ const typeRef = gvar.name.typeRef;
5315
+ if (!typeRef) throw new TypeShapeError(`var '${varName}' has no type`, varName);
5316
+ const type = typeShape(typeRef, buildStructRegistry(ast), varName);
5317
+ const declText = ast.srcModule.src.slice(gvar.start, gvar.end);
5318
+ return {
5319
+ varName,
5320
+ addressSpace: parseAddressSpace(declText),
5321
+ accessMode: parseAccessMode(declText),
5322
+ group: numericAttr(gvar, "group"),
5323
+ binding: numericAttr(gvar, "binding"),
5324
+ type,
5325
+ byteSize: type.size
5326
+ };
5327
+ }
5328
+ /** Build a TypeShape from a TypeRefElem, with layout (size/alignment/offsets) populated. */
5329
+ function typeShape(typeRef, structs, varName) {
5330
+ const name = originalTypeName(typeRef);
5331
+ const primitive = primitiveShape(name);
5332
+ if (primitive) return primitive;
5333
+ if (name === "atomic") return atomicShape(typeRef, varName);
5334
+ if (name === "array") return arrayShape(typeRef, structs, varName);
5335
+ const struct = structs.get(name);
5336
+ if (struct) return structShape(struct, structs, varName);
5337
+ throw new TypeShapeError(`unsupported type '${name}'`, varName);
5338
+ }
5339
+ /** Byte size of a WGSL scalar: f16 is 2, all others (f32/i32/u32/bool) are 4. */
5340
+ function scalarSize(s) {
5341
+ return s === "f16" ? 2 : 4;
5342
+ }
5343
+ function findGlobalVar(ast, varName) {
5344
+ return ast.moduleElem.contents.find((e) => e.kind === "gvar" && e.name.decl.ident.originalName === varName);
5345
+ }
5346
+ function parseAddressSpace(declText) {
5347
+ const m = declText.match(/var\s*<\s*([a-z_]+)/);
5348
+ if (!m) return void 0;
5349
+ const space = m[1];
5350
+ return addressSpaces.includes(space) ? space : void 0;
5351
+ }
5352
+ function parseAccessMode(declText) {
5353
+ if (/\bread_write\b/.test(declText)) return "read_write";
5354
+ if (/var\s*<\s*storage\s*,\s*read\b/.test(declText)) return "read";
5355
+ }
5356
+ function numericAttr(gvar, name) {
5357
+ const attr = findAnnotation(gvar, name);
5358
+ if (!attr) return void 0;
5359
+ const [n] = numericParams(attr);
5360
+ return Number.isFinite(n) ? n : void 0;
5361
+ }
5362
+ /** Look up a scalar/vec/mat shape by WGSL primitive name. */
5363
+ function primitiveShape(name) {
5364
+ const scalar = scalarTable[name];
5365
+ if (scalar) return {
5366
+ kind: "scalar",
5367
+ type: scalar,
5368
+ ...scalarLayout(scalar)
5369
+ };
5370
+ const vec = vecTable[name];
5371
+ if (vec) return {
5372
+ kind: "vec",
5373
+ ...vec,
5374
+ ...vecLayout(vec.n, vec.component)
5375
+ };
5376
+ const mat = matTable[name];
5377
+ if (mat) return {
5378
+ kind: "mat",
5379
+ ...mat,
5380
+ ...matLayout(mat)
5381
+ };
5382
+ }
5383
+ function atomicShape(typeRef, varName) {
5384
+ const param = typeRef.templateParams?.[0];
5385
+ const inner = param && elemTypeName(param);
5386
+ if (inner !== "i32" && inner !== "u32") throw new TypeShapeError(`atomic must wrap i32 or u32`, varName);
5387
+ return {
5388
+ kind: "atomic",
5389
+ component: inner,
5390
+ ...scalarLayout(inner)
5391
+ };
5392
+ }
5393
+ function arrayShape(typeRef, structs, varName) {
5394
+ const params = typeRef.templateParams;
5395
+ if (!params?.length) throw new TypeShapeError(`array missing element type`, varName);
5396
+ const elemRef = params[0];
5397
+ const elem = elemRef.kind === "type" ? typeShape(elemRef, structs, varName) : namedTypeShape(elemTypeName(elemRef), structs, varName);
5398
+ const stride = roundUp(elem.alignment, elem.size);
5399
+ const lenExpr = params[1];
5400
+ if (lenExpr === void 0) return {
5401
+ kind: "array",
5402
+ elem,
5403
+ length: "runtime",
5404
+ stride,
5405
+ size: 0,
5406
+ alignment: elem.alignment
5407
+ };
5408
+ const length = lenExpr.value !== void 0 ? Number(lenExpr.value) : NaN;
5409
+ if (!Number.isFinite(length) || length <= 0) throw new TypeShapeError(`array length must be a positive literal`, varName);
5410
+ return {
5411
+ kind: "array",
5412
+ elem,
5413
+ length,
5414
+ stride,
5415
+ size: length * stride,
5416
+ alignment: elem.alignment
5417
+ };
5418
+ }
5419
+ function structShape(struct, structs, varName) {
5420
+ let offset = 0;
5421
+ let alignment = 1;
5422
+ const fields = [];
5423
+ for (const m of struct.members) {
5424
+ const mShape = typeShape(m.typeRef, structs, varName);
5425
+ let fieldAlign = mShape.alignment;
5426
+ let fieldSize = mShape.size;
5427
+ const alignAttr = findAnnotation(m, "align");
5428
+ if (alignAttr) {
5429
+ const [n] = numericParams(alignAttr);
5430
+ if (n) fieldAlign = Math.max(n, fieldAlign);
5431
+ }
5432
+ const sizeAttr = findAnnotation(m, "size");
5433
+ if (sizeAttr) {
5434
+ const [n] = numericParams(sizeAttr);
5435
+ if (n) fieldSize = n;
5436
+ }
5437
+ alignment = Math.max(alignment, fieldAlign);
5438
+ offset = roundUp(fieldAlign, offset);
5439
+ fields.push({
5440
+ name: m.name.name,
5441
+ offset,
5442
+ size: fieldSize,
5443
+ type: mShape
5444
+ });
5445
+ offset += fieldSize;
5446
+ }
5447
+ return {
5448
+ kind: "struct",
5449
+ name: struct.name.ident.originalName,
5450
+ fields,
5451
+ alignment,
5452
+ size: roundUp(alignment, offset)
5453
+ };
5454
+ }
5455
+ function scalarLayout(s) {
5456
+ const n = scalarSize(s);
5457
+ return {
5458
+ size: n,
5459
+ alignment: n
5460
+ };
5461
+ }
5462
+ /** vecN<T> layout: size=N*S, align=N*S (vec3 pads alignment to 4*S). */
5463
+ function vecLayout(n, component) {
5464
+ const s = scalarSize(component);
5465
+ const alignN = n === 3 ? 4 : n;
5466
+ return {
5467
+ size: n * s,
5468
+ alignment: alignN * s
5469
+ };
5470
+ }
5471
+ /** matCxR<T>: C columns of vec<R,T>, column stride = roundUp(colAlign, colSize). */
5472
+ function matLayout(m) {
5473
+ const col = vecLayout(m.rows, m.component);
5474
+ const colStride = roundUp(col.alignment, col.size);
5475
+ return {
5476
+ size: m.cols * colStride,
5477
+ alignment: col.alignment
5478
+ };
5479
+ }
5480
+ /** Pull a type name out of a template parameter (TypeRef or RefIdent). */
5481
+ function elemTypeName(param) {
5482
+ if (param.kind === "type") return originalTypeName(param);
5483
+ if (param.kind === "ref") return param.ident?.originalName;
5484
+ }
5485
+ /** Resolve an array template-param identifier (e.g. `Particle` in `array<Particle, 4>`). */
5486
+ function namedTypeShape(name, structs, varName) {
5487
+ if (!name) throw new TypeShapeError(`cannot resolve array element`, varName);
5488
+ const primitive = primitiveShape(name);
5489
+ if (primitive) return primitive;
5490
+ const struct = structs.get(name);
5491
+ if (struct) return structShape(struct, structs, varName);
5492
+ throw new TypeShapeError(`unsupported type '${name}'`, varName);
5493
+ }
5494
+ //#endregion
5495
+ //#region ../wesl-reflect/src/AnnotatedResources.ts
5496
+ /** Find all @buffer, @test_texture, @sampler annotated global vars in a parsed WESL module. */
5497
+ function findAnnotatedResources(ast) {
5498
+ const src = ast.srcModule.src;
5499
+ const structs = buildStructRegistry(ast);
5500
+ return ast.moduleElem.contents.filter((e) => e.kind === "gvar").flatMap((gvar) => discoverResource(gvar, src, structs));
5501
+ }
5502
+ /** Linker plugin that decorates @buffer/@test_texture/@sampler globals with
5503
+ * @group(0) @binding(N) so they emit as bindable WGSL vars.
5504
+ * The wgsl-test annotations themselves drop at emit (non-WGSL attributes).
5505
+ * Throws if a target var already carries user-supplied @group/@binding. */
5506
+ function annotatedResourcesPlugin(resources, startBinding = 1) {
5507
+ const bindingByName = new Map(resources.map((r, i) => [r.varName, startBinding + i]));
5508
+ return { transform: (ast) => {
5509
+ for (const elem of ast.moduleElem.contents) {
5510
+ if (elem.kind !== "gvar") continue;
5511
+ const varName = elem.name.decl.ident.originalName;
5512
+ const binding = bindingByName.get(varName);
5513
+ if (binding === void 0) continue;
5514
+ assertNoUserBinding(elem, varName);
5515
+ const anchor = annotationAnchor(elem);
5516
+ const groupAttr = makeStandardAttr("group", 0, anchor);
5517
+ const bindingAttr = makeStandardAttr("binding", binding, anchor);
5518
+ elem.attributes = [
5519
+ ...elem.attributes ?? [],
5520
+ groupAttr,
5521
+ bindingAttr
5522
+ ];
5523
+ elem.contents = [
5524
+ groupAttr,
5525
+ bindingAttr,
5526
+ ...elem.contents
5527
+ ];
5528
+ }
5529
+ return ast;
5530
+ } };
5531
+ }
5532
+ /** Dispatch on which runtime annotation the gvar carries; returns [] if none. */
5533
+ function discoverResource(gvar, src, structs) {
5534
+ if (findAnnotation(gvar, "buffer")) return [discoverBuffer(gvar, src, structs)];
5535
+ const textureAttr = findAnnotation(gvar, "test_texture");
5536
+ if (textureAttr) return [discoverTexture(gvar, textureAttr, src)];
5537
+ const userTextureAttr = findAnnotation(gvar, "texture");
5538
+ if (userTextureAttr) return [discoverUserTexture(gvar, userTextureAttr)];
5539
+ const samplerAttr = findAnnotation(gvar, "sampler");
5540
+ if (samplerAttr) return [discoverSampler(gvar, samplerAttr)];
5541
+ return [];
5542
+ }
5543
+ /** Throw if the user has already put @group or @binding on an annotated var. */
5544
+ function assertNoUserBinding(gvar, varName) {
5545
+ if (findAnnotation(gvar, "group") || findAnnotation(gvar, "binding")) throw new Error(`@buffer/@test_texture/@texture/@sampler on var '${varName}' cannot be combined with user-supplied @group/@binding — the runtime owns the binding for annotated resources.`);
5546
+ }
5547
+ /** Pick a source position to anchor synthetic attributes on, preferring the
5548
+ * runtime annotation so error maps point to something meaningful. */
5549
+ function annotationAnchor(gvar) {
5550
+ for (const name of [
5551
+ "buffer",
5552
+ "test_texture",
5553
+ "texture",
5554
+ "sampler"
5555
+ ]) {
5556
+ const attr = gvar.attributes?.find((a) => a.attribute.kind === "@attribute" && a.attribute.name === name);
5557
+ if (attr) return {
5558
+ start: attr.start,
5559
+ end: attr.end
5560
+ };
5561
+ }
5562
+ return {
5563
+ start: gvar.start,
5564
+ end: gvar.start
5565
+ };
5566
+ }
5567
+ /** Build a synthetic `@name(value)` standard attribute anchored at the given source span. */
5568
+ function makeStandardAttr(name, value, anchor) {
5569
+ const { start, end } = anchor;
5570
+ return {
5571
+ kind: "attribute",
5572
+ start,
5573
+ end,
5574
+ contents: [],
5575
+ attribute: {
5576
+ kind: "@attribute",
5577
+ name,
5578
+ params: [{
5579
+ kind: "expression",
5580
+ start,
5581
+ end,
5582
+ contents: [{
5583
+ kind: "literal",
5584
+ value: String(value),
5585
+ start,
5586
+ end
5587
+ }]
5588
+ }]
5589
+ }
5590
+ };
5591
+ }
5592
+ function discoverBuffer(gvar, src, structs) {
5593
+ const varName = gvar.name.decl.ident.originalName;
5594
+ const access = src.slice(gvar.start, gvar.end).includes("read_write") ? "read_write" : "read";
5595
+ const { typeRef } = gvar.name;
5596
+ return {
5597
+ kind: "buffer",
5598
+ varName,
5599
+ access,
5600
+ byteSize: typeRef ? typeShape(typeRef, structs, varName).size : 0
5601
+ };
5602
+ }
5603
+ function discoverTexture(gvar, attr, src) {
5604
+ const varName = gvar.name.decl.ident.originalName;
5605
+ const params = attr.params ?? [];
5606
+ return {
5607
+ kind: "test_texture",
5608
+ varName,
5609
+ source: firstRefName(params[0]) ?? "",
5610
+ params: params.slice(1).map((p) => Number.parseInt(src.slice(p.start, p.end).trim(), 10) || 0),
5611
+ typeName: gvar.name.typeRef ? originalTypeName(gvar.name.typeRef) : ""
5612
+ };
5613
+ }
5614
+ function discoverUserTexture(gvar, attr) {
5615
+ return {
5616
+ kind: "texture",
5617
+ varName: gvar.name.decl.ident.originalName,
5618
+ source: firstRefName(attr.params?.[0]) ?? "",
5619
+ typeName: gvar.name.typeRef ? originalTypeName(gvar.name.typeRef) : ""
5620
+ };
5621
+ }
5622
+ function discoverSampler(gvar, attr) {
5623
+ return {
5624
+ kind: "sampler",
5625
+ varName: gvar.name.decl.ident.originalName,
5626
+ filter: (firstRefName(attr.params?.[0]) ?? "linear") === "nearest" ? "nearest" : "linear"
5627
+ };
5628
+ }
5629
+ /** Extract the originalName of the first `ref` expression in an attribute parameter. */
5630
+ function firstRefName(param) {
5631
+ const ref = param?.contents.find((c) => c.kind === "ref");
5632
+ return ref?.kind === "ref" ? ref.ident.originalName : void 0;
5633
+ }
5634
+ //#endregion
5635
+ //#region ../wesl-reflect/src/EntryPoints.ts
5636
+ const stageNames = [
5637
+ "compute",
5638
+ "fragment",
5639
+ "vertex"
5640
+ ];
5641
+ /** Classify all functions in a parsed WESL module by entry-point stage. */
5642
+ function classifyEntryPoints(ast) {
5643
+ return ast.moduleElem.contents.filter((e) => e.kind === "fn").flatMap((fn) => entryPointFor(fn));
5644
+ }
5645
+ function entryPointFor(fn) {
5646
+ const stage = stageNames.find((s) => findAnnotation(fn, s));
5647
+ if (!stage) return [];
5648
+ const fnName = fn.name.ident.originalName;
5649
+ if (stage !== "compute") return [{
5650
+ fnName,
5651
+ stage
5652
+ }];
5653
+ return [{
5654
+ fnName,
5655
+ stage,
5656
+ workgroupSize: parseWorkgroupSize(fn)
5657
+ }];
5658
+ }
5659
+ function parseWorkgroupSize(fn) {
5660
+ const attr = findAnnotation(fn, "workgroup_size");
5661
+ if (!attr) return void 0;
5662
+ const nums = numericParams(attr);
5663
+ if (nums.length === 0) return void 0;
5664
+ const [x, y = 1, z = 1] = nums.map((n) => Number.isFinite(n) ? n : 1);
5665
+ return [
5666
+ x,
5667
+ y,
5668
+ z
5669
+ ];
5670
+ }
5671
+ //#endregion
5672
+ //#region ../wesl-gpu/src/UniformsVirtualLib.ts
5673
+ /** Default env:: module when no @uniforms struct is declared. */
5674
+ const defaultEnvSource = `
5675
+ struct Uniforms {
5676
+ resolution: vec2f,
5567
5677
  time: f32,
5568
5678
  }
5569
5679
  @group(0) @binding(0) var<uniform> u: Uniforms;
@@ -5601,34 +5711,18 @@ function findUniformsStruct(src, modulePath) {
5601
5711
  }).moduleElem.contents.filter((e) => e.kind === "struct").find(isUniformsStruct);
5602
5712
  }
5603
5713
  //#endregion
5604
- //#region ../wesl-gpu/src/FragmentPipeline.ts
5605
- /** Combined: link WESL source and create pipeline in one step. */
5606
- async function linkAndCreatePipeline(params) {
5607
- const { device, format, layout } = params;
5608
- const { module } = await linkFragmentShader(params);
5609
- return createFragmentPipeline({
5610
- device,
5611
- module,
5612
- format,
5613
- layout
5614
- });
5615
- }
5616
- /**
5617
- * Link a WESL/WGSL fragment shader to a shader module.
5618
- * Adds:
5619
- * - vertex shader that covers the viewport with a fullscreen triangle
5620
- * - a virtual module containing a std uniform buffer (for size, etc.)
5621
- */
5622
- async function linkFragmentShader(params) {
5623
- const { fragmentSource, conditions, constants, packageName, config } = params;
5714
+ //#region ../wesl-gpu/src/LinkWeslModule.ts
5715
+ /** Build the resolver chain, scan @uniforms, link, and compile the shader module.
5716
+ * Shared by `linkFragmentShader` and `linkComputeShader`. */
5717
+ async function linkWeslModule(params) {
5718
+ const { rootSource, scanSource, conditions, constants } = params;
5624
5719
  const { device, resolver, libs = [], rootModuleName = "main" } = params;
5625
- const { weslSrc, virtualLibs } = params;
5626
- const fullSource = `${fragmentSource}\n\n${fullscreenTriangleVertex}`;
5627
- const resolvers = [new RecordResolver({ [rootModuleName]: fullSource }, { packageName })];
5720
+ const { packageName, config, weslSrc, virtualLibs } = params;
5721
+ const resolvers = [new RecordResolver({ [rootModuleName]: rootSource }, { packageName })];
5628
5722
  if (weslSrc) resolvers.push(new RecordResolver(weslSrc, { packageName }));
5629
5723
  let finalResolver = resolvers.length === 1 ? resolvers[0] : new CompositeResolver(resolvers);
5630
5724
  if (resolver) finalResolver = new CompositeResolver([finalResolver, resolver]);
5631
- const scan = scanUniforms(fragmentSource, `${packageName ?? "package"}::${rootModuleName}`);
5725
+ const scan = scanUniforms(scanSource, `${packageName ?? "package"}::${rootModuleName}`);
5632
5726
  const mergedVirtualLibs = {
5633
5727
  ...scan.virtualLibs,
5634
5728
  ...virtualLibs
@@ -5647,6 +5741,156 @@ async function linkFragmentShader(params) {
5647
5741
  layout: scan.layout
5648
5742
  };
5649
5743
  }
5744
+ //#endregion
5745
+ //#region ../wesl-gpu/src/ComputePipeline.ts
5746
+ /** Link a WESL/WGSL compute shader into a GPU shader module.
5747
+ * Mirrors `linkFragmentShader` without the synthetic vertex prelude. */
5748
+ async function linkComputeShader(params) {
5749
+ const { computeSource } = params;
5750
+ return linkWeslModule({
5751
+ ...params,
5752
+ rootSource: computeSource,
5753
+ scanSource: computeSource
5754
+ });
5755
+ }
5756
+ /** Dispatch a compute shader and read back named storage buffers.
5757
+ * Defaults to a (1,1,1) dispatch; override via `dispatchWorkgroups`. */
5758
+ async function runCompute(p) {
5759
+ return withErrorScopes(p.device, () => dispatchAndReadback(p));
5760
+ }
5761
+ /** Clear each buffer to zeros so re-runs / re-tests see deterministic initial state. */
5762
+ function clearBuffers(device, buffers) {
5763
+ const list = [...buffers];
5764
+ if (list.length === 0) return;
5765
+ const encoder = device.createCommandEncoder({ label: "clearBuffers" });
5766
+ for (const buffer of list) encoder.clearBuffer(buffer);
5767
+ device.queue.submit([encoder.finish()]);
5768
+ }
5769
+ /** Build the pipeline, dispatch, copy each readBuffer to a MAP_READ staging
5770
+ * buffer, and return the mapped contents. */
5771
+ async function dispatchAndReadback(p) {
5772
+ const { device, module, entryPoint, bindGroup, pipelineLayout } = p;
5773
+ const dispatch = p.dispatchWorkgroups ?? 1;
5774
+ const pipeline = device.createComputePipeline({
5775
+ layout: pipelineLayout,
5776
+ compute: {
5777
+ module,
5778
+ entryPoint
5779
+ }
5780
+ });
5781
+ const stagingBuffers = /* @__PURE__ */ new Map();
5782
+ for (const [name, src] of p.readBuffers) stagingBuffers.set(name, createStagingBuffer(device, src.size, name));
5783
+ const encoder = device.createCommandEncoder();
5784
+ const pass = encoder.beginComputePass();
5785
+ pass.setPipeline(pipeline);
5786
+ pass.setBindGroup(0, bindGroup);
5787
+ if (typeof dispatch === "number") pass.dispatchWorkgroups(dispatch);
5788
+ else pass.dispatchWorkgroups(...dispatch);
5789
+ pass.end();
5790
+ for (const [name, src] of p.readBuffers) {
5791
+ const dst = stagingBuffers.get(name);
5792
+ encoder.copyBufferToBuffer(src, 0, dst, 0, src.size);
5793
+ }
5794
+ device.queue.submit([encoder.finish()]);
5795
+ const readbacks = /* @__PURE__ */ new Map();
5796
+ for (const [name, staging] of stagingBuffers) {
5797
+ await staging.mapAsync(GPUMapMode.READ);
5798
+ readbacks.set(name, staging.getMappedRange().slice(0));
5799
+ staging.unmap();
5800
+ staging.destroy();
5801
+ }
5802
+ return { readbacks };
5803
+ }
5804
+ /** Allocate a MAP_READ + COPY_DST buffer for compute readback. */
5805
+ function createStagingBuffer(device, size, name) {
5806
+ return device.createBuffer({
5807
+ label: `compute-readback-${name}`,
5808
+ size,
5809
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
5810
+ });
5811
+ }
5812
+ //#endregion
5813
+ //#region ../wesl-gpu/src/DeviceCache.ts
5814
+ /**
5815
+ * WeakMap cache for GPUDevice-based caching
5816
+ *
5817
+ * GPUDevices are weakly held to avoid memory leaks.
5818
+ */
5819
+ var DeviceCache = class {
5820
+ cache = /* @__PURE__ */ new WeakMap();
5821
+ get(device, key) {
5822
+ return this.cache.get(device)?.get(key);
5823
+ }
5824
+ set(device, key, value) {
5825
+ let deviceCache = this.cache.get(device);
5826
+ if (!deviceCache) {
5827
+ deviceCache = /* @__PURE__ */ new Map();
5828
+ this.cache.set(device, deviceCache);
5829
+ }
5830
+ deviceCache.set(key, value);
5831
+ }
5832
+ };
5833
+ new DeviceCache();
5834
+ const samplerCache = new DeviceCache();
5835
+ /** Create sampler. Default: linear filtering with clamp-to-edge. Internally cached. */
5836
+ function createSampler(device, options) {
5837
+ const { addressMode = "clamp-to-edge", filterMode = "linear" } = options ?? {};
5838
+ const cacheKey = `${addressMode}:${filterMode}`;
5839
+ const cached = samplerCache.get(device, cacheKey);
5840
+ if (cached) return cached;
5841
+ const sampler = device.createSampler({
5842
+ addressModeU: addressMode,
5843
+ addressModeV: addressMode,
5844
+ magFilter: filterMode,
5845
+ minFilter: filterMode
5846
+ });
5847
+ samplerCache.set(device, cacheKey, sampler);
5848
+ return sampler;
5849
+ }
5850
+ //#endregion
5851
+ //#region ../wesl-gpu/src/FullscreenVertex.ts
5852
+ /** Fullscreen triangle vertex shader that covers viewport with 3 vertices, no vertex buffer needed */
5853
+ const fullscreenTriangleVertex = `
5854
+ @vertex
5855
+ fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4f {
5856
+ // Covers viewport with 3 vertices, no vertex buffer needed
5857
+ var pos: vec2f;
5858
+ if (idx == 0u) {
5859
+ pos = vec2f(-1.0, -1.0);
5860
+ } else if (idx == 1u) {
5861
+ pos = vec2f(3.0, -1.0);
5862
+ } else {
5863
+ pos = vec2f(-1.0, 3.0);
5864
+ }
5865
+ return vec4f(pos, 0.0, 1.0);
5866
+ }`;
5867
+ //#endregion
5868
+ //#region ../wesl-gpu/src/FragmentPipeline.ts
5869
+ /** Combined: link WESL source and create pipeline in one step. */
5870
+ async function linkAndCreatePipeline(params) {
5871
+ const { device, format, layout } = params;
5872
+ const { module } = await linkFragmentShader(params);
5873
+ return createFragmentPipeline({
5874
+ device,
5875
+ module,
5876
+ format,
5877
+ layout
5878
+ });
5879
+ }
5880
+ /**
5881
+ * Link a WESL/WGSL fragment shader to a shader module.
5882
+ * Adds:
5883
+ * - vertex shader that covers the viewport with a fullscreen triangle
5884
+ * - a virtual module containing a std uniform buffer (for size, etc.)
5885
+ */
5886
+ async function linkFragmentShader(params) {
5887
+ const { fragmentSource } = params;
5888
+ return linkWeslModule({
5889
+ ...params,
5890
+ rootSource: `${fragmentSource}\n\n${fullscreenTriangleVertex}`,
5891
+ scanSource: fragmentSource
5892
+ });
5893
+ }
5650
5894
  /** Create fullscreen fragment render pipeline from a shader module. */
5651
5895
  function createFragmentPipeline(params) {
5652
5896
  const { device, module, format, layout = "auto" } = params;
@@ -5683,6 +5927,152 @@ function renderFrame(params) {
5683
5927
  pass.end();
5684
5928
  device.queue.submit([encoder.finish()]);
5685
5929
  }
5930
+ //#endregion
5931
+ //#region ../wesl-gpu/src/ResourceSetup.ts
5932
+ /** Host-side resource failure (missing texture source, unsupported texture dim,
5933
+ * test-only annotation in a non-test runtime). Distinguishes asset/configuration
5934
+ * problems from shader compile/link errors. */
5935
+ var ResourceLoadError = class extends Error {
5936
+ resourceSource;
5937
+ constructor(message, resourceSource) {
5938
+ super(message);
5939
+ this.name = "ResourceLoadError";
5940
+ this.resourceSource = resourceSource;
5941
+ }
5942
+ };
5943
+ /** Create GPU resources for @buffer/@sampler/@texture vars. */
5944
+ async function createBindResources(p) {
5945
+ const { device, resources, visibility, textureHandler, startBinding = 1 } = p;
5946
+ const { minBufferBytes = 4, prefill } = p;
5947
+ const entries = [];
5948
+ const layoutEntries = [];
5949
+ const buffers = [];
5950
+ const textures = [];
5951
+ for (const [i, resource] of resources.entries()) {
5952
+ const binding = startBinding + i;
5953
+ if (resource.kind === "buffer") {
5954
+ const made = bufferEntry(device, resource, binding, visibility, {
5955
+ minBufferBytes,
5956
+ prefill
5957
+ });
5958
+ buffers.push(made.buffer);
5959
+ entries.push(made.entry);
5960
+ layoutEntries.push(made.layout);
5961
+ } else if (resource.kind === "sampler") {
5962
+ const made = samplerEntry(device, resource, binding, visibility);
5963
+ entries.push(made.entry);
5964
+ layoutEntries.push(made.layout);
5965
+ } else {
5966
+ const t = await textureHandler(device, resource);
5967
+ const made = textureEntry(binding, visibility, t);
5968
+ textures.push(t.texture);
5969
+ entries.push(made.entry);
5970
+ layoutEntries.push(made.layout);
5971
+ }
5972
+ }
5973
+ return {
5974
+ entries,
5975
+ layoutEntries,
5976
+ buffers,
5977
+ textures
5978
+ };
5979
+ }
5980
+ /** Allocate a storage buffer for an @buffer var. */
5981
+ function bufferEntry(device, r, binding, visibility, opts) {
5982
+ const { STORAGE, COPY_SRC, COPY_DST } = GPUBufferUsage;
5983
+ const { minBufferBytes, prefill } = opts;
5984
+ const size = Math.max(r.byteSize, minBufferBytes);
5985
+ const buffer = device.createBuffer({
5986
+ label: `annotated-buffer-${r.varName}`,
5987
+ size,
5988
+ usage: STORAGE | COPY_SRC | COPY_DST,
5989
+ mappedAtCreation: prefill !== void 0
5990
+ });
5991
+ if (prefill !== void 0) {
5992
+ new Float32Array(buffer.getMappedRange()).fill(prefill);
5993
+ buffer.unmap();
5994
+ }
5995
+ return {
5996
+ buffer,
5997
+ entry: {
5998
+ binding,
5999
+ resource: { buffer }
6000
+ },
6001
+ layout: {
6002
+ binding,
6003
+ visibility,
6004
+ buffer: { type: r.access === "read_write" ? "storage" : "read-only-storage" }
6005
+ }
6006
+ };
6007
+ }
6008
+ /** Build a filtering sampler for an @sampler var. */
6009
+ function samplerEntry(device, r, binding, visibility) {
6010
+ return {
6011
+ entry: {
6012
+ binding,
6013
+ resource: createSampler(device, { filterMode: r.filter })
6014
+ },
6015
+ layout: {
6016
+ binding,
6017
+ visibility,
6018
+ sampler: { type: "filtering" }
6019
+ }
6020
+ };
6021
+ }
6022
+ /** Wrap a TextureBinding into bind group + layout entries. */
6023
+ function textureEntry(binding, visibility, t) {
6024
+ const texture = { sampleType: t.sampleType ?? "float" };
6025
+ return {
6026
+ entry: {
6027
+ binding,
6028
+ resource: t.texture.createView()
6029
+ },
6030
+ layout: {
6031
+ binding,
6032
+ visibility,
6033
+ texture
6034
+ }
6035
+ };
6036
+ }
6037
+ //#endregion
6038
+ //#region ../wesl-gpu/src/PlayResourceSetup.ts
6039
+ /** Create GPU resources for @buffer/@sampler/@texture vars in a play context. */
6040
+ async function createPlayResources(params) {
6041
+ const { resolveTexture, ...rest } = params;
6042
+ const visibility = rest.visibility ?? GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT;
6043
+ return createBindResources({
6044
+ ...rest,
6045
+ visibility,
6046
+ textureHandler
6047
+ });
6048
+ function textureHandler(dev, r) {
6049
+ return loadUserTexture(dev, r, resolveTexture);
6050
+ }
6051
+ }
6052
+ /** Resolve and upload a host image for an @texture var. */
6053
+ async function loadUserTexture(device, r, resolveTexture) {
6054
+ if (r.kind === "test_texture") throw new ResourceLoadError(`@test_texture(${r.source}) is not available at runtime — use @texture(name) with a matching host element instead (var '${r.varName}')`, r.source);
6055
+ if (r.kind !== "texture") throw new Error(`unexpected resource kind for play: ${r.kind}`);
6056
+ if (r.typeName !== "texture_2d") throw new ResourceLoadError(`@texture(${r.source}): only texture_2d<f32> is supported; got '${r.typeName}' on var '${r.varName}'`, r.source);
6057
+ const image = await resolveTexture(r.source);
6058
+ if (!image) throw new ResourceLoadError(`@texture(${r.source}): no source element found for var '${r.varName}' (expected <img data-texture="${r.source}"> or <img id="${r.source}">)`, r.source);
6059
+ const { width, height } = image;
6060
+ const { TEXTURE_BINDING, COPY_DST, RENDER_ATTACHMENT } = GPUTextureUsage;
6061
+ const texture = device.createTexture({
6062
+ label: `annotated-texture-${r.varName}`,
6063
+ format: "rgba8unorm",
6064
+ size: {
6065
+ width,
6066
+ height
6067
+ },
6068
+ usage: TEXTURE_BINDING | COPY_DST | RENDER_ATTACHMENT
6069
+ });
6070
+ device.queue.copyExternalImageToTexture({ source: image }, { texture }, {
6071
+ width,
6072
+ height
6073
+ });
6074
+ return { texture };
6075
+ }
5686
6076
  "" + memoize((nesting) => {
5687
6077
  return " ".repeat(nesting);
5688
6078
  })(23);
@@ -5932,79 +6322,256 @@ function defaultAnnotatedLayout() {
5932
6322
  };
5933
6323
  }
5934
6324
  //#endregion
5935
- //#region src/Renderer.ts
5936
- /** Initialize WebGPU for a canvas element. */
5937
- async function initWebGPU(canvas, alphaMode = "opaque") {
5938
- const adapter = await navigator.gpu.requestAdapter();
5939
- if (!adapter) throw new Error("WebGPU adapter not available");
5940
- const device = await requestWeslDevice(adapter);
5941
- const context = canvas.getContext("webgpu");
5942
- if (!context) throw new Error("WebGPU context not available");
5943
- const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
5944
- context.configure({
6325
+ //#region src/CanvasSize.ts
6326
+ /** Device-pixel size of an observed element.
6327
+ * Prefers `devicePixelContentBoxSize` (Chromium/Firefox) integer, pixel-exact.
6328
+ * Falls back to `contentBoxSize × ratio` on Safari and when a `pixel-ratio`
6329
+ * override is set (the override is CSS-pixel-based by intent). */
6330
+ function entrySize(entry, pixelRatioOverride) {
6331
+ if (pixelRatioOverride === null) {
6332
+ const dp = entry.devicePixelContentBoxSize?.[0];
6333
+ if (dp) return [dp.inlineSize, dp.blockSize];
6334
+ }
6335
+ const ratio = pixelRatioOverride !== null ? Number(pixelRatioOverride) : devicePixelRatio;
6336
+ const css = entry.contentBoxSize?.[0];
6337
+ if (css) return [css.inlineSize * ratio, css.blockSize * ratio];
6338
+ const { width, height } = entry.contentRect;
6339
+ return [width * ratio, height * ratio];
6340
+ }
6341
+ /** Floor and clamp to `[1, max]` so the canvas never exceeds the GPU's 2D limit.
6342
+ * Pass `max = undefined` to skip the upper clamp (e.g. before the GPU device
6343
+ * is available; device init will surface limit errors itself). */
6344
+ function clampCanvas(size, max) {
6345
+ const floored = Math.max(1, Math.floor(size));
6346
+ return max !== void 0 ? Math.min(floored, max) : floored;
6347
+ }
6348
+ //#endregion
6349
+ //#region src/RenderResources.ts
6350
+ /** Default allocation size (bytes) for runtime-sized `@buffer` arrays. The
6351
+ * WGSL gives no element count, so the playground picks one. 1024 bytes covers
6352
+ * 256 f32s or 64 vec4f, enough for casual playground use. */
6353
+ const defaultRuntimeArrayBytes = 1024;
6354
+ /** Allocate user-declared resources, rebuild the uniform buffer to match the
6355
+ * shader's uniform layout, and assemble the pipeline layout + bind group. */
6356
+ async function prepareResources(p) {
6357
+ const { state, resources, resolveTexture } = p;
6358
+ const { device } = state;
6359
+ const scan = scanUniforms(p.shaderSource, `${p.pkg}::${p.root}`);
6360
+ const playResources = await createPlayResources({
5945
6361
  device,
5946
- format: presentationFormat,
5947
- alphaMode
6362
+ resources,
6363
+ startBinding: 1,
6364
+ resolveTexture,
6365
+ minBufferBytes: defaultRuntimeArrayBytes
6366
+ });
6367
+ state.uniformState.buffer.destroy();
6368
+ state.uniformState = createUniformBuffer(device, scan.layout);
6369
+ const bindGroupLayout = buildBindGroupLayout(device, playResources);
6370
+ return {
6371
+ playResources,
6372
+ pipelineLayout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }),
6373
+ bindGroup: buildBindGroup(device, bindGroupLayout, state.uniformState, playResources),
6374
+ layout: scan.layout
6375
+ };
6376
+ }
6377
+ /** Destroy all textures and buffers owned by a PlayResources bundle. */
6378
+ function destroyPlayResources(r) {
6379
+ for (const t of r.textures) t.destroy();
6380
+ for (const b of r.buffers) b.destroy();
6381
+ }
6382
+ /** Dispose GPU resources owned by the current compile (textures + storage buffers). */
6383
+ function disposeResources(state) {
6384
+ for (const t of state.resourceTextures) t.destroy();
6385
+ for (const b of state.resourceBuffers) b.destroy();
6386
+ state.resourceTextures = [];
6387
+ state.resourceBuffers = [];
6388
+ }
6389
+ /** Add the @buffer/@texture/@sampler plugin to the user-supplied config. */
6390
+ function mergeResourcePlugins(userConfig, resources) {
6391
+ if (resources.length === 0) return userConfig;
6392
+ const plugins = [...userConfig?.plugins ?? [], annotatedResourcesPlugin(resources, 1)];
6393
+ return {
6394
+ ...userConfig,
6395
+ plugins
6396
+ };
6397
+ }
6398
+ function buildBindGroupLayout(device, resources) {
6399
+ const visibility = GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT;
6400
+ return device.createBindGroupLayout({ entries: [{
6401
+ binding: 0,
6402
+ visibility,
6403
+ buffer: {}
6404
+ }, ...resources.layoutEntries] });
6405
+ }
6406
+ function buildBindGroup(device, layout, uniformState, resources) {
6407
+ return device.createBindGroup({
6408
+ layout,
6409
+ entries: [{
6410
+ binding: 0,
6411
+ resource: { buffer: uniformState.buffer }
6412
+ }, ...resources.entries]
6413
+ });
6414
+ }
6415
+ //#endregion
6416
+ //#region src/ComputeBuild.ts
6417
+ /** Build the compute pipeline, dispatch once, and return initial readback. */
6418
+ async function buildCompute(p) {
6419
+ const { state, resources, playResources, ast, shaderSource } = p;
6420
+ const { device } = state;
6421
+ const compute = await linkComputeFor({
6422
+ device,
6423
+ shaderSource,
6424
+ resources,
6425
+ options: p.options
6426
+ }).catch((err) => {
6427
+ destroyPlayResources(playResources);
6428
+ throw err;
6429
+ });
6430
+ const readBuffers = mapBuffersByName(resources, playResources);
6431
+ const reflections = mapBufferReflections(ast, resources);
6432
+ disposeResources(state);
6433
+ state.pipeline = void 0;
6434
+ state.bindGroup = p.bindGroup;
6435
+ state.resourceTextures = playResources.textures;
6436
+ state.resourceBuffers = playResources.buffers;
6437
+ state.compute = {
6438
+ pipelineLayout: p.pipelineLayout,
6439
+ bindGroup: p.bindGroup,
6440
+ module: compute.module,
6441
+ entryPoint: p.entryPoint,
6442
+ readBuffers,
6443
+ reflections
6444
+ };
6445
+ const computeReadback = await dispatchComputeAndReadback(state);
6446
+ return {
6447
+ layout: p.layout,
6448
+ mode: "compute",
6449
+ computeReadback
6450
+ };
6451
+ }
6452
+ /** Re-dispatch the compute pipeline against the current uniform state and
6453
+ * return fresh BufferEntry[] for the panel. */
6454
+ async function rerunCompute(state) {
6455
+ return dispatchComputeAndReadback(state);
6456
+ }
6457
+ /** Re-zero each readback buffer, dispatch, and read back. Used for both initial
6458
+ * build and slider/refresh-driven re-runs in compute mode. */
6459
+ async function dispatchComputeAndReadback(state) {
6460
+ const compute = state.compute;
6461
+ if (!compute) return [];
6462
+ writeUniforms(state.device, state.uniformState, computeAutoValues(state));
6463
+ clearBuffers(state.device, compute.readBuffers.values());
6464
+ const { readbacks } = await runCompute({
6465
+ device: state.device,
6466
+ module: compute.module,
6467
+ entryPoint: compute.entryPoint,
6468
+ bindGroup: compute.bindGroup,
6469
+ pipelineLayout: compute.pipelineLayout,
6470
+ readBuffers: compute.readBuffers
5948
6471
  });
5949
- const bindGroupLayout = device.createBindGroupLayout({ entries: [{
5950
- binding: 0,
5951
- visibility: GPUShaderStage.FRAGMENT,
5952
- buffer: {}
5953
- }] });
5954
- const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] });
5955
- const uniformState = createUniformBuffer(device, null);
6472
+ return [...readbacks].map(([varName, data]) => ({
6473
+ reflection: compute.reflections.get(varName),
6474
+ data
6475
+ }));
6476
+ }
6477
+ async function linkComputeFor(p) {
6478
+ const config = mergeResourcePlugins(p.options?.config, p.resources);
6479
+ return withErrorScopes(p.device, () => linkComputeShader({
6480
+ device: p.device,
6481
+ computeSource: p.shaderSource,
6482
+ ...p.options,
6483
+ config
6484
+ }));
6485
+ }
6486
+ /** Map @buffer var name to its allocated GPU buffer (positional alignment). */
6487
+ function mapBuffersByName(resources, playResources) {
6488
+ const bufferVars = resources.filter((r) => r.kind === "buffer");
6489
+ return new Map(bufferVars.map((r, i) => [r.varName, playResources.buffers[i]]));
6490
+ }
6491
+ /** Map @buffer var name to its VarReflection (type tree, address space, etc.). */
6492
+ function mapBufferReflections(ast, resources) {
6493
+ return new Map(resources.filter((r) => r.kind === "buffer").map((r) => [r.varName, varReflection(ast, r.varName)]));
6494
+ }
6495
+ function computeAutoValues(state) {
5956
6496
  return {
5957
- device,
5958
- canvas,
5959
- context,
5960
- presentationFormat,
5961
- bindGroupLayout,
5962
- pipelineLayout,
5963
- uniformState,
5964
- bindGroup: createBindGroup(device, bindGroupLayout, uniformState),
5965
- mouse: {
5966
- pos: [0, 0],
5967
- delta: [0, 0],
5968
- button: 0
5969
- },
5970
- pipeline: void 0,
5971
- frameCount: 0
6497
+ resolution: [state.canvas.width, state.canvas.height],
6498
+ time: 0,
6499
+ delta_time: 0,
6500
+ frame: 0,
6501
+ mouse_pos: [0, 0],
6502
+ mouse_delta: [0, 0],
6503
+ mouse_button: 0
5972
6504
  };
5973
6505
  }
5974
- /** Compile WESL fragment shader and create render pipeline.
5975
- * Recreates the uniform buffer and bind group based on @uniforms metadata. */
5976
- async function createPipeline(state, fragmentSource, options) {
5977
- const scan = scanUniforms(fragmentSource, `${options?.packageName ?? "package"}::${options?.rootModuleName ?? "main"}`);
5978
- const { device, bindGroupLayout, presentationFormat, pipelineLayout } = state;
5979
- state.uniformState.buffer.destroy();
5980
- state.uniformState = createUniformBuffer(device, scan.layout);
5981
- state.bindGroup = createBindGroup(device, bindGroupLayout, state.uniformState);
5982
- device.pushErrorScope("validation");
5983
- let gpuError;
5984
- let jsError;
5985
- try {
5986
- state.pipeline = await linkAndCreatePipeline({
5987
- device,
5988
- fragmentSource,
5989
- format: presentationFormat,
5990
- layout: pipelineLayout,
5991
- ...options
5992
- });
5993
- } catch (e) {
5994
- jsError = e;
5995
- } finally {
5996
- gpuError = await device.popErrorScope();
6506
+ //#endregion
6507
+ //#region src/ErrorOverlay.ts
6508
+ /** Manages an error overlay element within a shadow DOM. */
6509
+ var ErrorOverlay = class {
6510
+ el;
6511
+ _message = null;
6512
+ constructor(container) {
6513
+ this.el = document.createElement("div");
6514
+ this.el.className = "error-overlay";
6515
+ container.appendChild(this.el);
6516
+ }
6517
+ show(message) {
6518
+ this._message = message;
6519
+ this.el.textContent = message;
6520
+ this.el.classList.add("visible");
6521
+ console.error("[wgsl-play]", message);
6522
+ }
6523
+ hide() {
6524
+ this._message = null;
6525
+ this.el.classList.remove("visible");
6526
+ }
6527
+ get visible() {
6528
+ return this.el.classList.contains("visible");
5997
6529
  }
5998
- if (jsError || gpuError) {
5999
- state.pipeline = void 0;
6000
- throw jsError ?? gpuError;
6530
+ get message() {
6531
+ return this._message;
6001
6532
  }
6002
- return scan.layout;
6533
+ };
6534
+ //#endregion
6535
+ //#region src/FragmentRender.ts
6536
+ /** Build the render pipeline and install it on state. */
6537
+ async function buildFragment(p) {
6538
+ const newPipeline = await buildRenderPipeline({
6539
+ device: p.state.device,
6540
+ fragmentSource: p.shaderSource,
6541
+ presentationFormat: p.state.presentationFormat,
6542
+ pipelineLayout: p.pipelineLayout,
6543
+ resources: p.resources,
6544
+ options: p.options
6545
+ }).catch((err) => {
6546
+ destroyPlayResources(p.playResources);
6547
+ p.state.pipeline = void 0;
6548
+ throw err;
6549
+ });
6550
+ disposeResources(p.state);
6551
+ p.state.pipeline = newPipeline;
6552
+ p.state.bindGroup = p.bindGroup;
6553
+ p.state.compute = void 0;
6554
+ p.state.resourceTextures = p.playResources.textures;
6555
+ p.state.resourceBuffers = p.playResources.buffers;
6556
+ return {
6557
+ layout: p.layout,
6558
+ mode: "fragment"
6559
+ };
6003
6560
  }
6004
6561
  /** Render a single frame (used when paused). */
6005
6562
  function renderOnce(state, playback) {
6006
6563
  if (!state.pipeline) return;
6007
- doRender(state, playback);
6564
+ const time = calculateTime(playback);
6565
+ const { mouse, canvas } = state;
6566
+ submitFrame(state, {
6567
+ resolution: [canvas.width, canvas.height],
6568
+ time,
6569
+ delta_time: 0,
6570
+ frame: state.frameCount,
6571
+ mouse_pos: mouse.pos,
6572
+ mouse_delta: [0, 0],
6573
+ mouse_button: mouse.button
6574
+ });
6008
6575
  }
6009
6576
  /** Start the render loop. Returns a stop function. */
6010
6577
  function startRenderLoop(state, playback) {
@@ -6018,7 +6585,7 @@ function startRenderLoop(state, playback) {
6018
6585
  const time = calculateTime(playback);
6019
6586
  const delta_time = time - lastTime;
6020
6587
  lastTime = time;
6021
- const { mouse, device, canvas, context, bindGroup } = state;
6588
+ const { mouse, canvas } = state;
6022
6589
  const auto = {
6023
6590
  resolution: [canvas.width, canvas.height],
6024
6591
  time,
@@ -6029,33 +6596,32 @@ function startRenderLoop(state, playback) {
6029
6596
  mouse_button: mouse.button
6030
6597
  };
6031
6598
  mouse.delta = [0, 0];
6032
- writeUniforms(device, state.uniformState, auto);
6033
- const targetView = context.getCurrentTexture().createView();
6034
- renderFrame({
6035
- device,
6036
- pipeline: state.pipeline,
6037
- bindGroup,
6038
- targetView
6039
- });
6040
- state.frameCount++;
6599
+ submitFrame(state, auto);
6041
6600
  animationId = requestAnimationFrame(render);
6042
6601
  }
6043
6602
  animationId = requestAnimationFrame(render);
6044
6603
  return () => cancelAnimationFrame(animationId);
6045
6604
  }
6046
- /** Update uniforms and submit one GPU frame (one-shot, e.g. when paused). */
6047
- function doRender(state, playback) {
6048
- const time = calculateTime(playback);
6049
- const { mouse, device, canvas, context, bindGroup } = state;
6050
- const auto = {
6051
- resolution: [canvas.width, canvas.height],
6052
- time,
6053
- delta_time: 0,
6054
- frame: state.frameCount,
6055
- mouse_pos: mouse.pos,
6056
- mouse_delta: [0, 0],
6057
- mouse_button: mouse.button
6058
- };
6605
+ /** Seconds elapsed since playback start, frozen while paused. */
6606
+ function calculateTime(playback) {
6607
+ return ((playback.isPlaying ? performance.now() : playback.startTime + playback.pausedDuration) - playback.startTime) / 1e3;
6608
+ }
6609
+ /** Link the shader and create the render pipeline, surfacing both JS and GPU
6610
+ * validation errors as a thrown rejection. */
6611
+ async function buildRenderPipeline(p) {
6612
+ const config = mergeResourcePlugins(p.options?.config, p.resources);
6613
+ return withErrorScopes(p.device, () => linkAndCreatePipeline({
6614
+ device: p.device,
6615
+ fragmentSource: p.fragmentSource,
6616
+ format: p.presentationFormat,
6617
+ layout: p.pipelineLayout,
6618
+ ...p.options,
6619
+ config
6620
+ }));
6621
+ }
6622
+ /** Write uniforms, render to the current swap-chain texture, and tick frameCount. */
6623
+ function submitFrame(state, auto) {
6624
+ const { device, context, bindGroup } = state;
6059
6625
  writeUniforms(device, state.uniformState, auto);
6060
6626
  const targetView = context.getCurrentTexture().createView();
6061
6627
  renderFrame({
@@ -6066,10 +6632,153 @@ function doRender(state, playback) {
6066
6632
  });
6067
6633
  state.frameCount++;
6068
6634
  }
6069
- function calculateTime(playback) {
6070
- return ((playback.isPlaying ? performance.now() : playback.startTime + playback.pausedDuration) - playback.startTime) / 1e3;
6635
+ //#endregion
6636
+ //#region src/icons/backToStart.svg?raw
6637
+ var backToStart_default = "<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\"><rect x=\"4\" y=\"4\" width=\"3\" height=\"16\" fill=\"currentColor\"/><polygon points=\"20,4 9,12 20,20\" fill=\"currentColor\"/></svg>\n";
6638
+ //#endregion
6639
+ //#region src/icons/expand.svg?raw
6640
+ var expand_default = "<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\"><path d=\"M3 3h6v2H5v4H3V3zm12 0h6v6h-2V5h-4V3zM3 15h2v4h4v2H3v-6zm16 4h-4v2h6v-6h-2v4z\" fill=\"currentColor\"/></svg>\n";
6641
+ //#endregion
6642
+ //#region src/icons/pause.svg?raw
6643
+ var pause_default = "<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\"><rect x=\"5\" y=\"4\" width=\"4\" height=\"16\" fill=\"currentColor\"/><rect x=\"15\" y=\"4\" width=\"4\" height=\"16\" fill=\"currentColor\"/></svg>\n";
6644
+ //#endregion
6645
+ //#region src/icons/play.svg?raw
6646
+ var play_default = "<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\"><polygon points=\"6,4 20,12 6,20\" fill=\"currentColor\"/></svg>\n";
6647
+ //#endregion
6648
+ //#region src/icons/refresh.svg?raw
6649
+ var refresh_default = "<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\"><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M17.66 17.66A8 8 0 1 1 19.45 9M19.95 4v5h-5\"/></svg>\n";
6650
+ //#endregion
6651
+ //#region src/icons/shrink.svg?raw
6652
+ var shrink_default = "<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\"><path d=\"M9 3v6H3v-2h4V3h2zm6 0h2v4h4v2h-6V3zM3 15h6v6H7v-4H3v-2zm12 0h6v2h-4v4h-2v-6z\" fill=\"currentColor\"/></svg>\n";
6653
+ //#endregion
6654
+ //#region src/PlaybackControls.ts
6655
+ /** Playback controls overlay for wgsl-play.
6656
+ *
6657
+ * In "render" mode (default): play/pause + rewind + fullscreen.
6658
+ * In "compute" mode: refresh (re-dispatch) + fullscreen — play/pause and
6659
+ * rewind are hidden because compute mode is not animation-driven. */
6660
+ var PlaybackControls = class {
6661
+ container;
6662
+ playPauseBtn;
6663
+ rewindBtn;
6664
+ refreshBtn;
6665
+ fullscreenBtn;
6666
+ playing = true;
6667
+ constructor(shadow, onPlay, onPause, onRewind, onFullscreen, onRefresh) {
6668
+ this.container = document.createElement("div");
6669
+ this.container.className = "controls";
6670
+ this.fullscreenBtn = makeBtn(expand_default, onFullscreen);
6671
+ this.rewindBtn = makeBtn(backToStart_default, onRewind);
6672
+ this.playPauseBtn = makeBtn(pause_default, () => this.playing ? onPause() : onPlay());
6673
+ this.refreshBtn = makeBtn(refresh_default, onRefresh);
6674
+ this.refreshBtn.hidden = true;
6675
+ this.container.append(this.fullscreenBtn, this.rewindBtn, this.playPauseBtn, this.refreshBtn);
6676
+ shadow.appendChild(this.container);
6677
+ }
6678
+ setPlaying(playing) {
6679
+ this.playing = playing;
6680
+ this.playPauseBtn.innerHTML = playing ? pause_default : play_default;
6681
+ }
6682
+ setFullscreen(isFullscreen) {
6683
+ this.fullscreenBtn.innerHTML = isFullscreen ? shrink_default : expand_default;
6684
+ }
6685
+ setMode(mode) {
6686
+ const isCompute = mode === "compute";
6687
+ this.rewindBtn.hidden = isCompute;
6688
+ this.playPauseBtn.hidden = isCompute;
6689
+ this.refreshBtn.hidden = !isCompute;
6690
+ }
6691
+ show() {
6692
+ this.container.style.display = "";
6693
+ }
6694
+ hide() {
6695
+ this.container.style.display = "none";
6696
+ }
6697
+ };
6698
+ function makeBtn(html, handler) {
6699
+ const btn = document.createElement("button");
6700
+ btn.innerHTML = html;
6701
+ btn.addEventListener("click", handler);
6702
+ return btn;
6703
+ }
6704
+ //#endregion
6705
+ //#region src/Renderer.ts
6706
+ /** Initialize WebGPU for a canvas element. */
6707
+ async function initWebGPU(canvas, alphaMode = "opaque") {
6708
+ const adapter = await navigator.gpu.requestAdapter();
6709
+ if (!adapter) throw new Error("WebGPU adapter not available");
6710
+ const device = await requestWeslDevice(adapter);
6711
+ const context = canvas.getContext("webgpu");
6712
+ if (!context) throw new Error("WebGPU context not available");
6713
+ const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
6714
+ context.configure({
6715
+ device,
6716
+ format: presentationFormat,
6717
+ alphaMode
6718
+ });
6719
+ const uniformState = createUniformBuffer(device, null);
6720
+ return {
6721
+ device,
6722
+ canvas,
6723
+ context,
6724
+ presentationFormat,
6725
+ uniformState,
6726
+ bindGroup: createUniformBindGroup(device, uniformOnlyLayout(device), uniformState),
6727
+ mouse: {
6728
+ pos: [0, 0],
6729
+ delta: [0, 0],
6730
+ button: 0
6731
+ },
6732
+ pipeline: void 0,
6733
+ frameCount: 0,
6734
+ resourceTextures: [],
6735
+ resourceBuffers: []
6736
+ };
6737
+ }
6738
+ /** Compile a WESL shader and configure pipelines + bindings.
6739
+ * Auto-detects fragment vs compute mode based on entry-point attributes. */
6740
+ async function createPipeline(state, shaderSource, resolveTexture, options) {
6741
+ const pkg = options?.packageName ?? "package";
6742
+ const root = options?.rootModuleName ?? "main";
6743
+ const ast = parseSrcModule({
6744
+ modulePath: `${pkg}::${root}`,
6745
+ debugFilePath: `./${root}.wesl`,
6746
+ src: shaderSource
6747
+ });
6748
+ const resources = findAnnotatedResources(ast);
6749
+ const entryPoints = classifyEntryPoints(ast);
6750
+ const mode = detectMode(entryPoints);
6751
+ if (mode === "compute") rejectComputeUnsupported(resources);
6752
+ const branch = {
6753
+ state,
6754
+ resources,
6755
+ shaderSource,
6756
+ options,
6757
+ ...await prepareResources({
6758
+ state,
6759
+ shaderSource,
6760
+ pkg,
6761
+ root,
6762
+ resources,
6763
+ resolveTexture
6764
+ })
6765
+ };
6766
+ if (mode === "compute") return buildCompute({
6767
+ ...branch,
6768
+ ast,
6769
+ entryPoint: entryPoints[0].fnName
6770
+ });
6771
+ return buildFragment(branch);
6772
+ }
6773
+ function uniformOnlyLayout(device) {
6774
+ const visibility = GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT;
6775
+ return device.createBindGroupLayout({ entries: [{
6776
+ binding: 0,
6777
+ visibility,
6778
+ buffer: {}
6779
+ }] });
6071
6780
  }
6072
- function createBindGroup(device, layout, uniformState) {
6781
+ function createUniformBindGroup(device, layout, uniformState) {
6073
6782
  return device.createBindGroup({
6074
6783
  layout,
6075
6784
  entries: [{
@@ -6078,6 +6787,186 @@ function createBindGroup(device, layout, uniformState) {
6078
6787
  }]
6079
6788
  });
6080
6789
  }
6790
+ /** Pick fragment vs compute mode from entry points; throws on mixed/multi-compute. */
6791
+ function detectMode(entryPoints) {
6792
+ const compute = entryPoints.filter((e) => e.stage === "compute");
6793
+ const fragment = entryPoints.filter((e) => e.stage === "fragment");
6794
+ if (compute.length > 0 && fragment.length > 0) throw new Error("mixed compute and fragment entry points are not supported (use either, not both)");
6795
+ if (compute.length > 1) throw new Error(`compute mode requires exactly one @compute entry point; found ${compute.length}`);
6796
+ return compute.length === 1 ? "compute" : "fragment";
6797
+ }
6798
+ /** @sampler/@texture in compute mode are not yet implemented; throw early. */
6799
+ function rejectComputeUnsupported(resources) {
6800
+ for (const r of resources) {
6801
+ if (r.kind === "texture" || r.kind === "test_texture") throw new Error("@texture in compute mode is not yet supported");
6802
+ if (r.kind === "sampler") throw new Error("@sampler in compute mode is not yet supported");
6803
+ }
6804
+ }
6805
+ //#endregion
6806
+ //#region src/ResultsPanel.ts
6807
+ const truncationLimit = 256;
6808
+ /** Render compute-mode @buffer readbacks as one HTML table per buffer. */
6809
+ function renderResultsPanel(p) {
6810
+ p.panel.replaceChildren(...p.entries.map(renderTable));
6811
+ }
6812
+ /** Build the structured table data for a single @buffer entry. Pure (no DOM). */
6813
+ function tableData(entry) {
6814
+ const { reflection, data } = entry;
6815
+ const { row, rowCount, stride } = rowShape(reflection, data.byteLength);
6816
+ const visibleRows = Math.min(rowCount, truncationLimit);
6817
+ const view = new DataView(data);
6818
+ const td = {
6819
+ caption: `${reflection.varName}: ${shapeLabel(reflection.type)}`,
6820
+ headers: row.kind === "struct" ? ["", ...row.fields.map((f) => f.name)] : ["", "value"],
6821
+ rows: buildRows(view, row, visibleRows, stride)
6822
+ };
6823
+ if (rowCount > truncationLimit) td.truncated = { totalRows: rowCount };
6824
+ return td;
6825
+ }
6826
+ function renderTable(entry) {
6827
+ const td = tableData(entry);
6828
+ const section = document.createElement("section");
6829
+ section.className = "result";
6830
+ section.appendChild(makeCaption(td.caption));
6831
+ const table = document.createElement("table");
6832
+ table.appendChild(makeHeader(td.headers));
6833
+ table.appendChild(makeBody(td.rows));
6834
+ if (td.truncated) table.appendChild(makeShowAllFooter(table, entry, td.truncated.totalRows));
6835
+ section.appendChild(table);
6836
+ return section;
6837
+ }
6838
+ /** Resolve the per-row shape and stride for tabular display. Runtime-sized
6839
+ * arrays derive rowCount from the readback's byteLength. Throws
6840
+ * TypeShapeError if the element type isn't supported as a table cell. */
6841
+ function rowShape(reflection, byteLength) {
6842
+ const t = reflection.type;
6843
+ if (t.kind === "array") {
6844
+ assertRenderable(t.elem, reflection.varName);
6845
+ const rowCount = t.length === "runtime" ? Math.floor(byteLength / t.stride) : t.length;
6846
+ return {
6847
+ row: t.elem,
6848
+ rowCount,
6849
+ stride: t.stride
6850
+ };
6851
+ }
6852
+ assertRenderable(t, reflection.varName);
6853
+ return {
6854
+ row: t,
6855
+ rowCount: 1,
6856
+ stride: t.size
6857
+ };
6858
+ }
6859
+ function shapeLabel(t) {
6860
+ if (t.kind === "array") {
6861
+ const len = t.length === "runtime" ? "" : `, ${t.length}`;
6862
+ return `array<${typeLabel(t.elem)}${len}>`;
6863
+ }
6864
+ return typeLabel(t);
6865
+ }
6866
+ /** Materialize `count` row-cell arrays at successive `stride` offsets. */
6867
+ function buildRows(view, row, count, stride) {
6868
+ return Array.from({ length: count }, (_, i) => rowCells(view, row, i, i * stride));
6869
+ }
6870
+ function makeCaption(text) {
6871
+ const caption = document.createElement("div");
6872
+ caption.className = "result-caption";
6873
+ caption.textContent = text;
6874
+ return caption;
6875
+ }
6876
+ function makeHeader(headers) {
6877
+ const thead = document.createElement("thead");
6878
+ const tr = document.createElement("tr");
6879
+ for (const h of headers) {
6880
+ const th = document.createElement("th");
6881
+ th.textContent = h;
6882
+ tr.appendChild(th);
6883
+ }
6884
+ thead.appendChild(tr);
6885
+ return thead;
6886
+ }
6887
+ function makeBody(rows) {
6888
+ const tbody = document.createElement("tbody");
6889
+ for (const row of rows) {
6890
+ const tr = document.createElement("tr");
6891
+ for (const cell of row) {
6892
+ const td = document.createElement("td");
6893
+ td.textContent = cell;
6894
+ tr.appendChild(td);
6895
+ }
6896
+ tbody.appendChild(tr);
6897
+ }
6898
+ return tbody;
6899
+ }
6900
+ function makeShowAllFooter(table, entry, totalRows) {
6901
+ const tfoot = document.createElement("tfoot");
6902
+ const tr = document.createElement("tr");
6903
+ const td = document.createElement("td");
6904
+ const { row, stride } = rowShape(entry.reflection, entry.data.byteLength);
6905
+ td.colSpan = row.kind === "struct" ? row.fields.length + 1 : 2;
6906
+ const button = document.createElement("button");
6907
+ button.textContent = `Show all ${totalRows} rows`;
6908
+ button.addEventListener("click", () => {
6909
+ const allRows = buildRows(new DataView(entry.data), row, totalRows, stride);
6910
+ table.replaceChild(makeBody(allRows), table.tBodies[0]);
6911
+ tfoot.remove();
6912
+ });
6913
+ td.appendChild(document.createTextNode(`Showing first ${truncationLimit} of ${totalRows} rows. `));
6914
+ td.appendChild(button);
6915
+ tr.appendChild(td);
6916
+ tfoot.appendChild(tr);
6917
+ return tfoot;
6918
+ }
6919
+ /** WgslPlay's compute-results table only renders f32/i32/u32 scalars, vectors of those, or structs of those. */
6920
+ function assertRenderable(t, varName) {
6921
+ if (t.kind === "scalar") {
6922
+ if (t.type === "f16") throw notRenderable("f16", varName);
6923
+ return;
6924
+ }
6925
+ if (t.kind === "vec") {
6926
+ if (t.component === "f16") throw notRenderable("vec<f16>", varName);
6927
+ return;
6928
+ }
6929
+ if (t.kind === "struct") {
6930
+ for (const f of t.fields) assertRenderable(f.type, varName);
6931
+ return;
6932
+ }
6933
+ if (t.kind === "mat") throw new TypeShapeError(`matrices are not supported in the compute results table; use vecN<f32> instead`, varName);
6934
+ if (t.kind === "atomic") throw new TypeShapeError(`atomic types are not supported in the compute results table`, varName);
6935
+ throw new TypeShapeError(`unsupported element type for table`, varName);
6936
+ }
6937
+ function typeLabel(t) {
6938
+ if (t.kind === "scalar") return t.type;
6939
+ if (t.kind === "vec") return `vec${t.n}<${t.component}>`;
6940
+ if (t.kind === "mat") return `mat${t.cols}x${t.rows}<${t.component}>`;
6941
+ if (t.kind === "atomic") return `atomic<${t.component}>`;
6942
+ if (t.kind === "struct") return t.name;
6943
+ const len = t.length === "runtime" ? "" : `, ${t.length}`;
6944
+ return `array<${typeLabel(t.elem)}${len}>`;
6945
+ }
6946
+ function rowCells(view, row, rowIndex, baseOffset) {
6947
+ if (row.kind === "struct") return [String(rowIndex), ...row.fields.map((f) => formatElem(view, f.type, baseOffset + f.offset))];
6948
+ return [String(rowIndex), formatElem(view, row, baseOffset)];
6949
+ }
6950
+ function notRenderable(typeName, varName) {
6951
+ return new TypeShapeError(`${typeName} is not supported in the compute results table`, varName);
6952
+ }
6953
+ function formatElem(view, elem, offset) {
6954
+ if (elem.kind === "scalar") return formatScalar(view, elem.type, offset);
6955
+ if (elem.kind === "vec") return `(${Array.from({ length: elem.n }, (_, i) => formatScalar(view, elem.component, offset + i * 4)).join(", ")})`;
6956
+ return `<${elem.kind}>`;
6957
+ }
6958
+ function formatScalar(view, type, offset) {
6959
+ if (type === "f32") return formatF32(view.getFloat32(offset, true));
6960
+ if (type === "i32") return String(view.getInt32(offset, true));
6961
+ if (type === "u32") return String(view.getUint32(offset, true));
6962
+ if (type === "bool") return view.getUint32(offset, true) !== 0 ? "true" : "false";
6963
+ return "<f16>";
6964
+ }
6965
+ function formatF32(v) {
6966
+ if (!Number.isFinite(v)) return String(v);
6967
+ if (Number.isInteger(v)) return v.toFixed(1);
6968
+ return Number.parseFloat(v.toPrecision(4)).toString();
6969
+ }
6081
6970
  //#endregion
6082
6971
  //#region src/UniformControls.ts
6083
6972
  /** Uniform controls panel for wgsl-play. Collapsed by default, toggle on hover. */
@@ -6185,12 +7074,12 @@ function hexToRgb(hex) {
6185
7074
  }
6186
7075
  //#endregion
6187
7076
  //#region src/WgslPlay.css?inline
6188
- var WgslPlay_default = ":host {\n --error-bg: rgba(220, 102, 18, 0.8);\n --error-color: white;\n --controls-bg: rgba(0, 0, 0, 0.55);\n --controls-color: rgba(255, 255, 255, 0.8);\n --controls-hover-color: white;\n --controls-hover-bg: rgba(255, 255, 255, 0.15);\n\n display: block;\n position: relative;\n overflow: hidden;\n}\n\n.resize-handle {\n display: none;\n position: absolute;\n bottom: 0;\n right: 0;\n width: 16px;\n height: 16px;\n cursor: nwse-resize;\n z-index: 2;\n touch-action: none;\n}\n\n.resize-handle::before {\n content: \"\";\n position: absolute;\n bottom: 3px;\n right: 3px;\n width: 8px;\n height: 8px;\n border-right: 2px solid var(--controls-bg);\n border-bottom: 2px solid var(--controls-bg);\n}\n\n:host([resizable]:hover) .resize-handle {\n display: block;\n}\n\n:host(.dark) {\n --error-bg: rgba(180, 60, 10, 0.85);\n --controls-bg: rgba(0, 0, 0, 0.5);\n}\n\ncanvas {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n display: block;\n}\n\n.controls {\n background: var(--controls-bg);\n position: absolute;\n bottom: 8px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n gap: 20px;\n border-radius: 6px;\n padding: 2px;\n opacity: 0;\n transition: opacity 0.2s;\n z-index: 1;\n}\n\n:host(:hover) .controls {\n opacity: 1;\n}\n\n.controls button {\n border: none;\n background: none;\n color: var(--controls-color);\n cursor: pointer;\n padding: 4px;\n border-radius: 4px;\n display: flex;\n align-items: center;\n}\n\n.controls button:hover {\n color: var(--controls-hover-color);\n background: var(--controls-hover-bg);\n}\n\n/* Settings panel */\n.settings {\n position: absolute;\n top: 8px;\n left: 8px;\n z-index: 1;\n opacity: 0;\n transition: opacity 0.2s;\n}\n\n:host(:hover) .settings {\n opacity: 1;\n}\n\n.settings.empty {\n display: none;\n}\n\n.uniform-toggle {\n border: none;\n background: var(--controls-bg);\n color: var(--controls-color);\n cursor: pointer;\n padding: 4px 6px;\n border-radius: 4px;\n display: flex;\n align-items: center;\n transition: background 0.15s;\n}\n\n.uniform-toggle:hover {\n color: var(--controls-hover-color);\n background: var(--controls-bg);\n}\n\n.uniform-toggle svg {\n transform: rotate(-90deg);\n transition: transform 0.25s linear;\n}\n\n.settings.expanded .uniform-toggle svg {\n transform: rotate(0deg);\n}\n\n.uniform-panel {\n display: grid;\n grid-template-rows: 0fr;\n margin-top: 4px;\n min-width: 180px;\n transition: grid-template-rows 0.25s linear;\n}\n\n.settings.expanded .uniform-panel {\n grid-template-rows: 1fr;\n}\n\n.uniform-panel-inner {\n overflow: hidden;\n background: var(--controls-bg);\n border-radius: 6px;\n padding: 0 10px;\n}\n\n.uniform-panel-inner > :first-child {\n margin-top: 8px;\n}\n\n.uniform-panel-inner > :last-child {\n margin-bottom: 8px;\n}\n\n.uniform-row {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 4px;\n color: var(--controls-color);\n font-family: monospace;\n font-size: 0.75rem;\n}\n\n.uniform-row label {\n min-width: 60px;\n flex-shrink: 0;\n}\n\n.uniform-row input[type=\"range\"] {\n flex: 1;\n min-width: 80px;\n}\n\n.uniform-row input[type=\"color\"] {\n width: 28px;\n height: 20px;\n border: none;\n padding: 0;\n background: none;\n cursor: pointer;\n}\n\n.uniform-row input[type=\"checkbox\"] {\n cursor: pointer;\n}\n\n.uniform-value {\n min-width: 36px;\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n.error-overlay {\n position: absolute;\n inset: 0;\n background: var(--error-bg);\n color: var(--error-color);\n padding: 1rem;\n font-family: monospace;\n font-size: 0.875rem;\n white-space: pre-wrap;\n overflow: auto;\n display: none;\n}\n\n.error-overlay.visible {\n display: block;\n}\n";
7077
+ var WgslPlay_default = ":host {\n --error-bg: rgba(220, 102, 18, 0.8);\n --error-color: white;\n --controls-bg: rgba(0, 0, 0, 0.55);\n --controls-color: rgba(255, 255, 255, 0.8);\n --controls-hover-color: white;\n --controls-hover-bg: rgba(255, 255, 255, 0.15);\n\n display: block;\n position: relative;\n overflow: hidden;\n}\n\n.resize-handle {\n display: none;\n position: absolute;\n width: 22px;\n height: 22px;\n z-index: 2;\n touch-action: none;\n opacity: 0;\n transition: opacity 0.15s;\n}\n\n:host([resizable]) .resize-handle {\n display: block;\n}\n\n.resize-handle.br {\n bottom: 0;\n right: 0;\n cursor: nwse-resize;\n}\n\n.resize-handle.bl {\n bottom: 0;\n left: 0;\n cursor: nesw-resize;\n}\n\n.resize-handle:hover,\n.resize-handle.dragging {\n opacity: 1;\n}\n\n.resize-handle::before {\n content: \"\";\n position: absolute;\n bottom: 3px;\n width: 8px;\n height: 8px;\n border-bottom: 2px solid var(--controls-bg);\n}\n\n.resize-handle.br::before {\n right: 3px;\n border-right: 2px solid var(--controls-bg);\n}\n\n.resize-handle.bl::before {\n left: 3px;\n border-left: 2px solid var(--controls-bg);\n}\n\n:host(.dark) {\n --error-bg: rgba(180, 60, 10, 0.85);\n --controls-bg: rgba(0, 0, 0, 0.5);\n}\n\ncanvas {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n display: block;\n}\n\n.controls {\n background: var(--controls-bg);\n position: absolute;\n bottom: 8px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n gap: 20px;\n border-radius: 6px;\n padding: 2px;\n opacity: 0;\n transition: opacity 0.2s;\n z-index: 1;\n}\n\n:host(:hover) .controls {\n opacity: 1;\n}\n\n.controls button {\n border: none;\n background: none;\n color: var(--controls-color);\n cursor: pointer;\n padding: 4px;\n border-radius: 4px;\n display: flex;\n align-items: center;\n}\n\n.controls button:hover {\n color: var(--controls-hover-color);\n background: var(--controls-hover-bg);\n}\n\n.controls button[hidden] {\n display: none;\n}\n\n/* Compute-mode results panel */\n.results-panel {\n font-family: var(--code-font, monospace);\n font-size: 0.85rem;\n padding: 8px;\n box-sizing: border-box;\n color: var(--results-fg, currentColor);\n background: var(--results-bg, transparent);\n}\n\n.results-panel[hidden] {\n display: none;\n}\n\n.results-panel .result {\n margin-bottom: 24px;\n}\n\n.results-panel .result-caption {\n font-weight: bold;\n padding-bottom: 8px;\n}\n\n.results-panel table {\n border-collapse: collapse;\n}\n\n.results-panel th,\n.results-panel td {\n padding: 2px 8px;\n border: 1px solid var(--results-border, rgba(128, 128, 128, 0.3));\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n.results-panel tfoot td {\n text-align: left;\n}\n\n.results-panel tfoot button {\n margin-left: 6px;\n cursor: pointer;\n}\n\n/* Settings panel */\n.settings {\n position: absolute;\n top: 8px;\n left: 8px;\n z-index: 1;\n opacity: 0;\n transition: opacity 0.2s;\n}\n\n:host(:hover) .settings {\n opacity: 1;\n}\n\n.settings.empty {\n display: none;\n}\n\n.uniform-toggle {\n border: none;\n background: var(--controls-bg);\n color: var(--controls-color);\n cursor: pointer;\n padding: 4px 6px;\n border-radius: 4px;\n display: flex;\n align-items: center;\n transition: background 0.15s;\n}\n\n.uniform-toggle:hover {\n color: var(--controls-hover-color);\n background: var(--controls-bg);\n}\n\n.uniform-toggle svg {\n transform: rotate(-90deg);\n transition: transform 0.25s linear;\n}\n\n.settings.expanded .uniform-toggle svg {\n transform: rotate(0deg);\n}\n\n.uniform-panel {\n display: grid;\n grid-template-rows: 0fr;\n margin-top: 4px;\n min-width: 180px;\n transition: grid-template-rows 0.25s linear;\n}\n\n.settings.expanded .uniform-panel {\n grid-template-rows: 1fr;\n}\n\n.uniform-panel-inner {\n overflow: hidden;\n background: var(--controls-bg);\n border-radius: 6px;\n padding: 0 10px;\n}\n\n.uniform-panel-inner > :first-child {\n margin-top: 8px;\n}\n\n.uniform-panel-inner > :last-child {\n margin-bottom: 8px;\n}\n\n.uniform-row {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 4px;\n color: var(--controls-color);\n font-family: monospace;\n font-size: 0.75rem;\n}\n\n.uniform-row label {\n min-width: 60px;\n flex-shrink: 0;\n}\n\n.uniform-row input[type=\"range\"] {\n flex: 1;\n min-width: 80px;\n}\n\n.uniform-row input[type=\"color\"] {\n width: 28px;\n height: 20px;\n border: none;\n padding: 0;\n background: none;\n cursor: pointer;\n}\n\n.uniform-row input[type=\"checkbox\"] {\n cursor: pointer;\n}\n\n.uniform-value {\n min-width: 36px;\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n.error-overlay {\n position: absolute;\n inset: 0;\n background: var(--error-bg);\n color: var(--error-color);\n padding: 1rem;\n font-family: monospace;\n font-size: 0.875rem;\n white-space: pre-wrap;\n overflow: auto;\n display: none;\n}\n\n.error-overlay.visible {\n display: block;\n}\n";
6189
7078
  //#endregion
6190
7079
  //#region src/WgslPlay.ts
6191
7080
  let styles = null;
6192
7081
  let template = null;
6193
- /** <wgsl-play> web component for rendering WESL/WGSL fragment shaders. */
7082
+ /** <wgsl-play> web component for rendering WESL/WGSL fragment shaders. */
6194
7083
  var WgslPlay = class extends HTMLElement {
6195
7084
  static observedAttributes = [
6196
7085
  "src",
@@ -6208,11 +7097,14 @@ var WgslPlay = class extends HTMLElement {
6208
7097
  "pixel-ratio"
6209
7098
  ];
6210
7099
  canvas;
7100
+ resultsPanel;
6211
7101
  errorOverlay;
6212
7102
  controls;
6213
7103
  settings;
6214
7104
  resizeObserver;
6215
7105
  stopRenderLoop;
7106
+ _currentMode = "fragment";
7107
+ _rerunPending = false;
6216
7108
  renderState;
6217
7109
  pendingUniforms = /* @__PURE__ */ new Map();
6218
7110
  playback = {
@@ -6235,7 +7127,8 @@ var WgslPlay = class extends HTMLElement {
6235
7127
  _mediaQuery = null;
6236
7128
  _onFullscreenChange = () => this.controls.setFullscreen(!!document.fullscreenElement);
6237
7129
  _pointerCleanup;
6238
- _resizeCleanup;
7130
+ _resizeCleanups = [];
7131
+ _childObserver;
6239
7132
  /** Get config overrides from element attributes. */
6240
7133
  getConfigOverrides() {
6241
7134
  const shaderRoot = this.getAttribute("shader-root");
@@ -6248,24 +7141,29 @@ var WgslPlay = class extends HTMLElement {
6248
7141
  shadow.adoptedStyleSheets = [getStyles()];
6249
7142
  shadow.appendChild(getTemplate().content.cloneNode(true));
6250
7143
  this.canvas = shadow.querySelector("canvas");
7144
+ this.resultsPanel = shadow.querySelector(".results-panel");
6251
7145
  this.errorOverlay = new ErrorOverlay(shadow);
6252
- this.settings = new UniformControls(shadow, (name, value) => this.setUniform(name, value));
6253
- this.controls = new PlaybackControls(shadow, () => this.play(), () => this.pause(), () => this.rewind(), () => this.toggleFullscreen());
7146
+ this.settings = new UniformControls(shadow, (name, value) => {
7147
+ this.setUniform(name, value);
7148
+ if (this._currentMode === "compute") this.scheduleComputeRerun();
7149
+ });
7150
+ this.controls = new PlaybackControls(shadow, () => this.play(), () => this.pause(), () => this.rewind(), () => this.toggleFullscreen(), () => this.scheduleComputeRerun());
6254
7151
  this.resizeObserver = new ResizeObserver((entries) => {
7152
+ if (!this.renderState) return;
6255
7153
  if (this.hasAttribute("width") && this.hasAttribute("height")) return;
6256
- for (const entry of entries) {
6257
- const { width, height } = entry.contentRect;
6258
- if (width > 0 && height > 0) {
6259
- const ratio = this.pixelRatio;
6260
- this.canvas.width = Math.floor(width * ratio);
6261
- this.canvas.height = Math.floor(height * ratio);
6262
- }
7154
+ const entry = entries.at(-1);
7155
+ if (!entry) return;
7156
+ const [width, height] = entrySize(entry, this.getAttribute("pixel-ratio"));
7157
+ if (width > 0 && height > 0) {
7158
+ const maxDim = this.renderState.device.limits.maxTextureDimension2D;
7159
+ this.canvas.width = clampCanvas(width, maxDim);
7160
+ this.canvas.height = clampCanvas(height, maxDim);
6263
7161
  }
6264
7162
  });
6265
- this.setupResizeHandle(shadow.querySelector(".resize-handle"));
7163
+ const handles = shadow.querySelectorAll(".resize-handle");
7164
+ for (const h of handles) this.setupResizeHandle(h, h.classList.contains("bl") ? "bl" : "br");
6266
7165
  }
6267
7166
  connectedCallback() {
6268
- this.resizeObserver.observe(this);
6269
7167
  const themeAttr = this.getAttribute("theme");
6270
7168
  if (themeAttr) this._theme = themeAttr;
6271
7169
  this._mediaQuery = matchMedia("(prefers-color-scheme: dark)");
@@ -6277,33 +7175,50 @@ var WgslPlay = class extends HTMLElement {
6277
7175
  this.controls.setPlaying(false);
6278
7176
  }
6279
7177
  this.initialize();
7178
+ this.observeLightDomChildren();
6280
7179
  upgradeProperty(this, "conditions");
6281
7180
  upgradeProperty(this, "shader");
6282
7181
  upgradeProperty(this, "project");
6283
7182
  }
7183
+ /** Start watching element size. Deferred until after `initWebGPU` so the
7184
+ * observer has a real `maxTextureDimension2D` to clamp against. */
7185
+ observeCanvasSize() {
7186
+ try {
7187
+ this.resizeObserver.observe(this, { box: "device-pixel-content-box" });
7188
+ } catch {
7189
+ this.resizeObserver.observe(this);
7190
+ }
7191
+ }
6284
7192
  disconnectedCallback() {
6285
7193
  this.resizeObserver.disconnect();
7194
+ this._childObserver?.disconnect();
7195
+ this._childObserver = void 0;
6286
7196
  this.stopRenderLoop?.();
6287
7197
  this._pointerCleanup?.();
6288
- this._resizeCleanup?.();
7198
+ for (const c of this._resizeCleanups) c();
7199
+ this._resizeCleanups = [];
6289
7200
  document.removeEventListener("fullscreenchange", this._onFullscreenChange);
6290
7201
  if (this._sourceEl && this._sourceListener) this._sourceEl.removeEventListener("change", this._sourceListener);
7202
+ if (this.renderState) disposeResources(this.renderState);
6291
7203
  }
6292
7204
  attributeChangedCallback(name, oldValue, newValue) {
6293
7205
  if (oldValue === newValue) return;
6294
7206
  switch (name) {
6295
7207
  case "no-controls":
6296
- newValue !== null ? this.controls.hide() : this.controls.show();
7208
+ if (newValue !== null) this.controls.hide();
7209
+ else this.controls.show();
6297
7210
  return;
6298
7211
  case "no-settings":
6299
- newValue !== null ? this.settings.hide() : this.settings.show();
7212
+ if (newValue !== null) this.settings.hide();
7213
+ else this.settings.show();
6300
7214
  return;
6301
7215
  case "theme":
6302
7216
  this._theme = newValue || "auto";
6303
7217
  this.updateTheme();
6304
7218
  return;
6305
7219
  case "autoplay":
6306
- newValue === "false" ? this.pause() : this.play();
7220
+ if (newValue === "false") this.pause();
7221
+ else this.play();
6307
7222
  return;
6308
7223
  case "fetch-libs":
6309
7224
  this._fetchLibs = newValue !== "false";
@@ -6316,8 +7231,6 @@ var WgslPlay = class extends HTMLElement {
6316
7231
  return;
6317
7232
  case "width":
6318
7233
  case "height":
6319
- this.updateCanvasSize();
6320
- return;
6321
7234
  case "pixel-ratio":
6322
7235
  this.updateCanvasSize();
6323
7236
  return;
@@ -6370,8 +7283,7 @@ var WgslPlay = class extends HTMLElement {
6370
7283
  }
6371
7284
  set fetchLibs(value) {
6372
7285
  this._fetchLibs = value;
6373
- if (value) this.removeAttribute("fetch-libs");
6374
- else this.setAttribute("fetch-libs", "false");
7286
+ this.setBoolAttr("fetch-libs", value);
6375
7287
  }
6376
7288
  /** Whether to fetch local .wesl source files via HTTP (default: true). */
6377
7289
  get fetchSources() {
@@ -6379,16 +7291,15 @@ var WgslPlay = class extends HTMLElement {
6379
7291
  }
6380
7292
  set fetchSources(value) {
6381
7293
  this._fetchSources = value;
6382
- if (value) this.removeAttribute("fetch-sources");
6383
- else this.setAttribute("fetch-sources", "false");
7294
+ this.setBoolAttr("fetch-sources", value);
6384
7295
  }
6385
7296
  /** Whether autoplay is enabled (default: true). Set autoplay="false" to start paused. */
6386
7297
  get autoplay() {
6387
7298
  return this.getAttribute("autoplay") !== "false";
6388
7299
  }
6389
7300
  set autoplay(value) {
6390
- if (value !== false && value !== "false") this.removeAttribute("autoplay");
6391
- else this.setAttribute("autoplay", "false");
7301
+ const enabled = value !== false && value !== "false";
7302
+ this.setBoolAttr("autoplay", enabled);
6392
7303
  }
6393
7304
  /** Scale factor from CSS pixels to canvas pixels (default: devicePixelRatio). */
6394
7305
  get pixelRatio() {
@@ -6430,9 +7341,7 @@ var WgslPlay = class extends HTMLElement {
6430
7341
  pause() {
6431
7342
  if (!this.playback.isPlaying) return;
6432
7343
  this.playback.pausedDuration = performance.now() - this.playback.startTime;
6433
- this.stopRenderLoop?.();
6434
- this.setPlaying(false);
6435
- if (this.renderState) renderOnce(this.renderState, this.playback);
7344
+ this.stopAndPresent();
6436
7345
  }
6437
7346
  setPlaying(playing) {
6438
7347
  this.playback.isPlaying = playing;
@@ -6440,10 +7349,37 @@ var WgslPlay = class extends HTMLElement {
6440
7349
  const detail = { isPlaying: playing };
6441
7350
  this.dispatchEvent(new CustomEvent("playback-change", { detail }));
6442
7351
  }
7352
+ /** Mirror a default-true setter onto its attribute: absent = on, "false" = off. */
7353
+ setBoolAttr(name, on) {
7354
+ if (on) this.removeAttribute(name);
7355
+ else this.setAttribute(name, "false");
7356
+ }
7357
+ /** Wait for any in-flight build and render one frame to the canvas, resolving
7358
+ * once it has been presented. Read the canvas via
7359
+ * `element.shadowRoot.querySelector('canvas')`. */
7360
+ async renderFrame() {
7361
+ await this.awaitIdleBuild();
7362
+ if (!this.renderState) throw new Error("renderFrame: not initialized");
7363
+ if (this.errorOverlay.visible) throw new Error("renderFrame: shader has compile error");
7364
+ if (this._currentMode === "compute") throw new Error("renderFrame: compute mode has no canvas output");
7365
+ renderOnce(this.renderState, this.playback);
7366
+ await new Promise((r) => requestAnimationFrame(() => r()));
7367
+ }
7368
+ /** Resolve once initialization and any pending build have settled. Yields via
7369
+ * setTimeout so async build steps (network fetch, GPU validation) get a
7370
+ * chance to advance between polls. */
7371
+ async awaitIdleBuild() {
7372
+ await this._initPromise;
7373
+ while (this._building || this._dirty) await new Promise((r) => setTimeout(r, 0));
7374
+ }
6443
7375
  /** Reset animation to time 0 and pause. */
6444
7376
  rewind() {
6445
7377
  this.playback.startTime = performance.now();
6446
7378
  this.playback.pausedDuration = 0;
7379
+ this.stopAndPresent();
7380
+ }
7381
+ /** Stop the RAF loop, mark paused, and re-present the current frame. */
7382
+ stopAndPresent() {
6447
7383
  this.stopRenderLoop?.();
6448
7384
  this.setPlaying(false);
6449
7385
  if (this.renderState) renderOnce(this.renderState, this.playback);
@@ -6463,16 +7399,17 @@ var WgslPlay = class extends HTMLElement {
6463
7399
  this.renderState?.uniformState.controlValues.set(name, value);
6464
7400
  if (this.renderState && !this.playback.isPlaying) renderOnce(this.renderState, this.playback);
6465
7401
  }
7402
+ /** Apply uniforms set before the pipeline existed. */
6466
7403
  flushPendingUniforms() {
6467
- const map = this.renderState?.uniformState.controlValues;
6468
- if (!map) return;
6469
- for (const [k, v] of this.pendingUniforms) map.set(k, v);
7404
+ const controlValues = this.renderState?.uniformState.controlValues;
7405
+ if (!controlValues) return;
7406
+ for (const [name, value] of this.pendingUniforms) controlValues.set(name, value);
6470
7407
  }
6471
7408
  /** Current uniform control values (readable). */
6472
7409
  get uniforms() {
6473
- const map = this.renderState?.uniformState.controlValues;
6474
- if (!map) return {};
6475
- return Object.fromEntries(map);
7410
+ const controlValues = this.renderState?.uniformState.controlValues;
7411
+ if (!controlValues) return {};
7412
+ return Object.fromEntries(controlValues);
6476
7413
  }
6477
7414
  /** Toggle fullscreen on this element. */
6478
7415
  toggleFullscreen() {
@@ -6508,52 +7445,65 @@ var WgslPlay = class extends HTMLElement {
6508
7445
  };
6509
7446
  }
6510
7447
  /** Drag-to-resize via a custom handle (works on touch + mouse). */
6511
- setupResizeHandle(handle) {
7448
+ setupResizeHandle(handle, corner) {
7449
+ const xSign = corner === "bl" ? -1 : 1;
6512
7450
  let startX = 0;
6513
7451
  let startY = 0;
6514
7452
  let startW = 0;
6515
7453
  let startH = 0;
7454
+ /** Numeric aspect ratio (w/h) sampled at drag start, or null if `auto`. */
7455
+ let ratio = null;
6516
7456
  const onMove = (e) => {
6517
- this.style.width = `${startW + e.clientX - startX}px`;
6518
- this.style.height = `${startH + e.clientY - startY}px`;
7457
+ const dx = (e.clientX - startX) * xSign;
7458
+ const dy = e.clientY - startY;
7459
+ let w = startW + dx;
7460
+ let h = startH + dy;
7461
+ if (ratio !== null) if (Math.abs(dx) >= Math.abs(dy)) h = w / ratio;
7462
+ else w = h * ratio;
7463
+ this.style.width = `${Math.max(16, w)}px`;
7464
+ this.style.height = `${Math.max(16, h)}px`;
6519
7465
  };
6520
7466
  const onUp = () => {
7467
+ handle.classList.remove("dragging");
6521
7468
  document.removeEventListener("pointermove", onMove);
6522
7469
  document.removeEventListener("pointerup", onUp);
6523
7470
  };
6524
7471
  const onDown = (e) => {
6525
7472
  e.preventDefault();
6526
7473
  handle.setPointerCapture(e.pointerId);
7474
+ handle.classList.add("dragging");
6527
7475
  const rect = this.getBoundingClientRect();
6528
7476
  startX = e.clientX;
6529
7477
  startY = e.clientY;
6530
7478
  startW = rect.width;
6531
7479
  startH = rect.height;
7480
+ ratio = parseAspectRatio(getComputedStyle(this).aspectRatio);
6532
7481
  document.addEventListener("pointermove", onMove);
6533
7482
  document.addEventListener("pointerup", onUp);
6534
7483
  };
6535
7484
  handle.addEventListener("pointerdown", onDown);
6536
- this._resizeCleanup = () => {
7485
+ this._resizeCleanups.push(() => {
6537
7486
  handle.removeEventListener("pointerdown", onDown);
6538
7487
  onUp();
6539
- };
7488
+ });
6540
7489
  }
6541
7490
  /** Recompute canvas resolution from attributes or CSS size. */
6542
7491
  updateCanvasSize() {
6543
7492
  const w = this.getAttribute("width");
6544
7493
  const h = this.getAttribute("height");
7494
+ const maxDim = this.renderState?.device.limits.maxTextureDimension2D;
6545
7495
  if (w !== null && h !== null) {
6546
- this.canvas.width = Number(w);
6547
- this.canvas.height = Number(h);
6548
- } else {
6549
- const rect = this.getBoundingClientRect();
6550
- if (rect.width > 0 && rect.height > 0) {
6551
- const ratio = this.pixelRatio;
6552
- this.canvas.width = Math.floor(rect.width * ratio);
6553
- this.canvas.height = Math.floor(rect.height * ratio);
6554
- }
7496
+ this.canvas.width = clampCanvas(Number(w), maxDim);
7497
+ this.canvas.height = clampCanvas(Number(h), maxDim);
7498
+ return;
6555
7499
  }
7500
+ const rect = this.getBoundingClientRect();
7501
+ if (rect.width <= 0 || rect.height <= 0) return;
7502
+ const ratio = this.pixelRatio;
7503
+ this.canvas.width = clampCanvas(rect.width * ratio, maxDim);
7504
+ this.canvas.height = clampCanvas(rect.height * ratio, maxDim);
6556
7505
  }
7506
+ /** Toggle the `dark` class to match `theme` attr or system preference. */
6557
7507
  updateTheme() {
6558
7508
  const isDark = this._theme === "dark" || this._theme === "auto" && matchMedia("(prefers-color-scheme: dark)").matches;
6559
7509
  this.classList.toggle("dark", isDark);
@@ -6564,10 +7514,12 @@ var WgslPlay = class extends HTMLElement {
6564
7514
  if (!this._initPromise) this._initPromise = this.doInitialize();
6565
7515
  return this._initPromise;
6566
7516
  }
7517
+ /** Init WebGPU, wire observers/listeners, and dispatch ready/init-error. */
6567
7518
  async doInitialize() {
6568
7519
  try {
6569
7520
  const alphaMode = this.hasAttribute("transparent") ? "premultiplied" : "opaque";
6570
7521
  this.renderState = await initWebGPU(this.canvas, alphaMode);
7522
+ this.observeCanvasSize();
6571
7523
  this.setupMouseTracking();
6572
7524
  this.loadInitialContent();
6573
7525
  if (this.playback.isPlaying) this.stopRenderLoop = startRenderLoop(this.renderState, this.playback);
@@ -6647,8 +7599,8 @@ var WgslPlay = class extends HTMLElement {
6647
7599
  }
6648
7600
  try {
6649
7601
  this.errorOverlay.hide();
6650
- const layout = await this.buildPipeline(mainSource);
6651
- if (!this._dirty) this.applyBuild(layout);
7602
+ const result = await this.buildPipeline(mainSource);
7603
+ if (!this._dirty) this.applyBuild(result);
6652
7604
  } catch (error) {
6653
7605
  if (!this._dirty) this.handleCompileError(error);
6654
7606
  }
@@ -6670,30 +7622,108 @@ var WgslPlay = class extends HTMLElement {
6670
7622
  };
6671
7623
  this._libs = dedupLibs(this._libs, libs);
6672
7624
  }
6673
- return createPipeline(this.renderState, mainSource, {
7625
+ return createPipeline(this.renderState, mainSource, (source) => this.resolveHostTexture(source), {
6674
7626
  ...this._linkOptions,
6675
7627
  weslSrc: this._weslSrc,
6676
7628
  libs: this._libs,
6677
7629
  rootModuleName: this._rootModuleName
6678
7630
  });
6679
7631
  }
7632
+ /** Resolve a @texture(name) to a decoded image from light-DOM children.
7633
+ * Prefers [data-texture="name"], falls back to #id. */
7634
+ resolveHostTexture = async (source) => {
7635
+ const selector = `[data-texture="${cssEscape(source)}"], #${cssEscape(source)}`;
7636
+ const el = this.querySelector(selector);
7637
+ if (!el) return null;
7638
+ if (el instanceof HTMLImageElement) return decodeImage(el, source);
7639
+ if (el instanceof HTMLCanvasElement) return el;
7640
+ throw new ResourceLoadError(`@texture(${source}): matched element is a <${el.tagName.toLowerCase()}>; expected <img> or <canvas>`, source);
7641
+ };
7642
+ /** Watch light-DOM for <img> add/remove/src changes and trigger a rebuild. */
7643
+ observeLightDomChildren() {
7644
+ if (this._childObserver) return;
7645
+ this._childObserver = new MutationObserver((records) => {
7646
+ for (const r of records) {
7647
+ if (r.type === "attributes") {
7648
+ this.requestBuild();
7649
+ return;
7650
+ }
7651
+ for (const node of [...r.addedNodes, ...r.removedNodes]) if (node instanceof HTMLImageElement) {
7652
+ this.requestBuild();
7653
+ return;
7654
+ }
7655
+ }
7656
+ });
7657
+ this._childObserver.observe(this, {
7658
+ childList: true,
7659
+ subtree: true,
7660
+ attributes: true,
7661
+ attributeFilter: [
7662
+ "src",
7663
+ "id",
7664
+ "data-texture"
7665
+ ]
7666
+ });
7667
+ }
6680
7668
  /** Apply a successful build: flush uniforms, update controls, render. */
6681
- applyBuild(layout) {
7669
+ applyBuild(result) {
6682
7670
  this.flushPendingUniforms();
6683
7671
  const controls = this.renderState.uniformState.layout.controls;
6684
7672
  this.settings.setControls(controls);
6685
- if (!this.playback.isPlaying) renderOnce(this.renderState, this.playback);
7673
+ this.applyMode(result);
6686
7674
  this.dispatchEvent(new CustomEvent("compile-success"));
6687
- this.dispatchEvent(new CustomEvent("uniforms-layout", { detail: layout }));
7675
+ const detail = result.layout;
7676
+ this.dispatchEvent(new CustomEvent("uniforms-layout", { detail }));
7677
+ }
7678
+ /** Show canvas vs results panel and (re-)render based on build mode. */
7679
+ applyMode(result) {
7680
+ this._currentMode = result.mode;
7681
+ if (result.mode === "compute") {
7682
+ this.canvas.hidden = true;
7683
+ this.resultsPanel.hidden = false;
7684
+ this.controls.setMode("compute");
7685
+ const entries = result.computeReadback ?? [];
7686
+ renderResultsPanel({
7687
+ panel: this.resultsPanel,
7688
+ entries
7689
+ });
7690
+ return;
7691
+ }
7692
+ this.canvas.hidden = false;
7693
+ this.resultsPanel.hidden = true;
7694
+ this.controls.setMode("render");
7695
+ if (!this.playback.isPlaying) renderOnce(this.renderState, this.playback);
7696
+ }
7697
+ /** Coalesce rapid uniform/refresh events into a single re-dispatch. */
7698
+ scheduleComputeRerun() {
7699
+ if (this._rerunPending) return;
7700
+ this._rerunPending = true;
7701
+ queueMicrotask(async () => {
7702
+ this._rerunPending = false;
7703
+ if (this._currentMode !== "compute" || !this.renderState) return;
7704
+ try {
7705
+ this.flushPendingUniforms();
7706
+ const entries = await rerunCompute(this.renderState);
7707
+ renderResultsPanel({
7708
+ panel: this.resultsPanel,
7709
+ entries
7710
+ });
7711
+ } catch (error) {
7712
+ this.handleCompileError(error);
7713
+ }
7714
+ });
6688
7715
  }
7716
+ /** Show the error overlay and dispatch a `compile-error` event. */
6689
7717
  handleCompileError(error) {
6690
7718
  const message = error?.message ?? String(error);
6691
7719
  this.errorOverlay.show(message);
6692
7720
  const detail = {
6693
7721
  message,
6694
7722
  source: error instanceof WeslParseError ? "wesl" : "webgpu",
7723
+ kind: error instanceof ResourceLoadError ? "resource" : "shader",
6695
7724
  locations: this.extractLocations(error)
6696
7725
  };
7726
+ if (error instanceof ResourceLoadError) detail.resourceSource = error.resourceSource;
6697
7727
  this.dispatchEvent(new CustomEvent("compile-error", { detail }));
6698
7728
  }
6699
7729
  /** Extract source locations from a WESL parse error or GPU compilation error. */
@@ -6701,37 +7731,30 @@ var WgslPlay = class extends HTMLElement {
6701
7731
  const loc = error?.weslLocation;
6702
7732
  if (loc) {
6703
7733
  const message = error?.message ?? "";
7734
+ const { file, line, column, length, offset } = loc;
6704
7735
  return [{
6705
- file: loc.file,
6706
- line: loc.line,
6707
- column: loc.column - 1,
6708
- length: loc.length,
7736
+ file,
7737
+ line,
7738
+ column: column - 1,
7739
+ length,
7740
+ offset,
6709
7741
  severity: "error",
6710
7742
  message
6711
7743
  }];
6712
7744
  }
6713
7745
  const msgs = error?.compilationInfo?.messages;
6714
- if (msgs) {
6715
- const toSeverity = (t) => t === "warning" ? "warning" : t === "info" ? "info" : "error";
6716
- return msgs.map((m) => ({
6717
- file: m.module?.url,
6718
- line: m.lineNum,
6719
- column: m.linePos - 1,
6720
- length: m.length,
6721
- severity: toSeverity(m.type),
6722
- message: m.message
6723
- }));
6724
- }
7746
+ if (msgs) return msgs.map((m) => ({
7747
+ file: m.module?.url,
7748
+ line: m.lineNum,
7749
+ column: m.linePos - 1,
7750
+ length: m.length,
7751
+ offset: m.offset,
7752
+ severity: toSeverity(m.type),
7753
+ message: m.message
7754
+ }));
6725
7755
  return [];
6726
7756
  }
6727
7757
  };
6728
- function getTemplate() {
6729
- if (!template) {
6730
- template = document.createElement("template");
6731
- template.innerHTML = `<canvas part="canvas"></canvas><div class="resize-handle"></div>`;
6732
- }
6733
- return template;
6734
- }
6735
7758
  function getStyles() {
6736
7759
  if (!styles) {
6737
7760
  styles = new CSSStyleSheet();
@@ -6739,7 +7762,15 @@ function getStyles() {
6739
7762
  }
6740
7763
  return styles;
6741
7764
  }
6742
- /** Absorb instance properties set before custom element upgrade. */
7765
+ function getTemplate() {
7766
+ if (!template) {
7767
+ template = document.createElement("template");
7768
+ template.innerHTML = `<canvas part="canvas"></canvas><div class="results-panel" part="results-panel" hidden></div><div class="resize-handle br"></div><div class="resize-handle bl"></div>`;
7769
+ }
7770
+ return template;
7771
+ }
7772
+ /** Absorb instance properties set before custom element upgrade.
7773
+ * Duplicated in WgslEdit.ts; extract to a shared package later. */
6743
7774
  function upgradeProperty(el, prop) {
6744
7775
  if (Object.hasOwn(el, prop)) {
6745
7776
  const value = el[prop];
@@ -6747,19 +7778,68 @@ function upgradeProperty(el, prop) {
6747
7778
  el[prop] = value;
6748
7779
  }
6749
7780
  }
7781
+ /** Normalize all keys in a weslSrc record to module paths. */
7782
+ function toModulePaths(weslSrc, pkg) {
7783
+ const entries = Object.entries(weslSrc).map(([key, value]) => [fileToModulePath(key, pkg, false), value]);
7784
+ return Object.fromEntries(entries);
7785
+ }
7786
+ /** Parse `getComputedStyle().aspectRatio` (`"auto"`, `"1"`, `"16 / 9"`) to w/h. */
7787
+ function parseAspectRatio(value) {
7788
+ if (!value || value === "auto") return null;
7789
+ const [w, h = "1"] = value.split("/").map((s) => s.trim());
7790
+ const wNum = Number(w);
7791
+ const hNum = Number(h);
7792
+ if (!wNum || !hNum) return null;
7793
+ return wNum / hNum;
7794
+ }
6750
7795
  /** Merge new libs, deduplicating by bundle name. */
6751
7796
  function dedupLibs(existing, newLibs) {
6752
7797
  if (!existing || newLibs.length === 0) return [...existing ?? [], ...newLibs];
6753
7798
  const names = new Set(newLibs.map((b) => b.name));
6754
7799
  return [...existing.filter((b) => !names.has(b.name)), ...newLibs];
6755
7800
  }
6756
- /** Normalize all keys in a weslSrc record to module paths. */
6757
- function toModulePaths(weslSrc, pkg) {
6758
- const entries = Object.entries(weslSrc).map(([key, value]) => [fileToModulePath(key, pkg, false), value]);
6759
- return Object.fromEntries(entries);
7801
+ /** Escape a value for safe use as an attribute-selector literal or id selector. */
7802
+ function cssEscape(value) {
7803
+ if (typeof CSS !== "undefined" && CSS.escape) return CSS.escape(value);
7804
+ return value.replace(/[^a-zA-Z0-9_-]/g, (ch) => `\\${ch}`);
7805
+ }
7806
+ /** Decode an <img> to an ImageBitmap with deterministic upload flags. */
7807
+ async function decodeImage(el, source) {
7808
+ try {
7809
+ if (!el.complete) await waitForImageLoad(el);
7810
+ if (el.decode) await el.decode();
7811
+ return await createImageBitmap(el, {
7812
+ imageOrientation: "from-image",
7813
+ premultiplyAlpha: "none",
7814
+ colorSpaceConversion: "none"
7815
+ });
7816
+ } catch (err) {
7817
+ const detail = err instanceof Error ? err.message : String(err);
7818
+ throw new ResourceLoadError(`@texture(${source}): failed to decode <img src="${el.src}"> — ${detail}`, source);
7819
+ }
7820
+ }
7821
+ /** Map a GPU compilation message `type` field to our severity enum. */
7822
+ function toSeverity(type) {
7823
+ if (type === "warning") return "warning";
7824
+ if (type === "info") return "info";
7825
+ return "error";
7826
+ }
7827
+ function waitForImageLoad(el) {
7828
+ return new Promise((resolve, reject) => {
7829
+ const onLoad = () => {
7830
+ el.removeEventListener("error", onError);
7831
+ resolve();
7832
+ };
7833
+ const onError = () => {
7834
+ el.removeEventListener("load", onLoad);
7835
+ reject(/* @__PURE__ */ new Error("image load failed"));
7836
+ };
7837
+ el.addEventListener("load", onLoad, { once: true });
7838
+ el.addEventListener("error", onError, { once: true });
7839
+ });
6760
7840
  }
6761
7841
  //#endregion
6762
7842
  //#region src/index.ts
6763
7843
  if (typeof customElements !== "undefined" && !customElements.get("wgsl-play")) customElements.define("wgsl-play", WgslPlay);
6764
7844
  //#endregion
6765
- export { FetchingResolver, WgslPlay, bundleRegistry, clearHttpCache, defaults, fetchBundleFilesFromNpm, fetchDependencies, fetchPackagesByName, fetchPackagesFromHttp, getConfig, hydrateBundleRegistry, loadBundlesFromFiles, loadBundlesFromTgz, loadBundlesWithPackageName, loadShaderFromUrl, resetConfig };
7845
+ export { FetchingResolver, ResourceLoadError, WgslPlay, bundleRegistry, clearHttpCache, defaults, fetchBundleFilesFromNpm, fetchDependencies, fetchPackagesByName, fetchPackagesFromHttp, getConfig, hydrateBundleRegistry, loadBundlesFromFiles, loadBundlesFromTgz, loadBundlesWithPackageName, loadShaderFromUrl, resetConfig };