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.
- package/README.md +59 -2
- package/dist/{WgslPlay-CSjRo-5Z.d.ts → WgslPlay-ouKx64GN.d.ts} +52 -3
- package/dist/WgslPlay-zK9mE5rV.js +1544 -0
- package/dist/WgslPlay.d.ts +2 -2
- package/dist/WgslPlay.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/jsx-preact.d.ts +12 -0
- package/dist/jsx-preact.js +0 -0
- package/dist/wgsl-play.js +1834 -754
- package/package.json +12 -8
- package/src/CanvasSize.ts +27 -0
- package/src/ComputeBuild.ts +151 -0
- package/src/FragmentRender.ts +147 -0
- package/src/PlaybackControls.ts +36 -13
- package/src/RenderResources.ts +132 -0
- package/src/Renderer.ts +112 -138
- package/src/ResultsPanel.ts +261 -0
- package/src/WgslPlay.css +80 -8
- package/src/WgslPlay.ts +346 -103
- package/src/icons/refresh.svg +1 -0
- package/src/jsx-preact.ts +15 -0
- package/src/test/BundleHydrator.test.ts +8 -11
- package/src/test/CanvasSize.test.ts +48 -0
- package/src/test/E2eHelpers.ts +117 -0
- package/src/test/RenderFrame.e2e.ts +33 -0
- package/src/test/ResultsPanel.test.ts +164 -0
- package/src/test/WgslPlay.basics.e2e.ts +87 -0
- package/src/test/WgslPlay.compute.e2e.ts +147 -0
- package/src/test/WgslPlay.connect.e2e.ts +101 -0
- package/src/test/WgslPlay.loading.e2e.ts +68 -0
- package/src/test/WgslPlay.resize.e2e.ts +199 -0
- package/src/test/WgslPlay.resources.e2e.ts +67 -0
- package/src/test/WgslPlay.resources.e2e.ts-snapshots/buffer-zero-init-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.resources.e2e.ts-snapshots/texture-grid-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.resources.e2e.ts-snapshots/texture-img-green-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.resources.e2e.ts-snapshots/texture-img-magenta-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.resources.e2e.ts-snapshots/texture-multi-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.uniforms.e2e.ts +57 -0
- package/dist/WgslPlay-LsU6XE09.js +0 -933
- package/src/test/WgslPlay.e2e.ts +0 -327
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.basics.e2e.ts-snapshots}/basic-shader-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.basics.e2e.ts-snapshots}/conditions-after-red-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.basics.e2e.ts-snapshots}/conditions-initial-green-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.connect.e2e.ts-snapshots}/connect-conditions-green-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.connect.e2e.ts-snapshots}/connect-conditions-red-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.connect.e2e.ts-snapshots}/connect-dynamic-npm-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.connect.e2e.ts-snapshots}/connect-source-external-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.connect.e2e.ts-snapshots}/connect-source-multifile-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.loading.e2e.ts-snapshots}/link-import-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.loading.e2e.ts-snapshots}/npm-cdn-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.loading.e2e.ts-snapshots}/shader-root-internal-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.loading.e2e.ts-snapshots}/shader-root-src-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.loading.e2e.ts-snapshots}/static-import-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.uniforms.e2e.ts-snapshots}/uniforms-initial-chromium-darwin.png +0 -0
- /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
|
|
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
|
|
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
|
|
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
|
-
|
|
4534
|
-
const
|
|
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
|
-
|
|
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) =>
|
|
4642
|
-
const rootBuilder =
|
|
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) =>
|
|
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
|
-
|
|
4660
|
-
|
|
4661
|
-
const
|
|
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
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
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/
|
|
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
|
-
*
|
|
5244
|
-
*
|
|
5245
|
-
*
|
|
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
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
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
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
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 } =
|
|
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
|
|
5416
|
-
const
|
|
5417
|
-
const
|
|
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
|
-
|
|
5423
|
-
|
|
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
|
-
/**
|
|
5429
|
-
function resolveStructInfo(ident, conditions) {
|
|
5430
|
-
const
|
|
5431
|
-
|
|
5432
|
-
const
|
|
5433
|
-
|
|
5434
|
-
|
|
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
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
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-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
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/
|
|
5605
|
-
/**
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
const {
|
|
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
|
|
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(
|
|
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/
|
|
5936
|
-
/**
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
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
|
-
|
|
5947
|
-
|
|
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
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
const
|
|
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
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
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
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
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
|
-
|
|
5999
|
-
|
|
6000
|
-
throw jsError ?? gpuError;
|
|
6530
|
+
get message() {
|
|
6531
|
+
return this._message;
|
|
6001
6532
|
}
|
|
6002
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
/**
|
|
6047
|
-
function
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
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
|
-
|
|
6070
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
6253
|
-
|
|
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
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
7208
|
+
if (newValue !== null) this.controls.hide();
|
|
7209
|
+
else this.controls.show();
|
|
6297
7210
|
return;
|
|
6298
7211
|
case "no-settings":
|
|
6299
|
-
newValue !== null
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6391
|
-
|
|
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.
|
|
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
|
|
6468
|
-
if (!
|
|
6469
|
-
for (const [
|
|
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
|
|
6474
|
-
if (!
|
|
6475
|
-
return Object.fromEntries(
|
|
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
|
-
|
|
6518
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
6651
|
-
if (!this._dirty) this.applyBuild(
|
|
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(
|
|
7669
|
+
applyBuild(result) {
|
|
6682
7670
|
this.flushPendingUniforms();
|
|
6683
7671
|
const controls = this.renderState.uniformState.layout.controls;
|
|
6684
7672
|
this.settings.setControls(controls);
|
|
6685
|
-
|
|
7673
|
+
this.applyMode(result);
|
|
6686
7674
|
this.dispatchEvent(new CustomEvent("compile-success"));
|
|
6687
|
-
|
|
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
|
|
6706
|
-
line
|
|
6707
|
-
column:
|
|
6708
|
-
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
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
6757
|
-
function
|
|
6758
|
-
|
|
6759
|
-
return
|
|
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 };
|