@nadrif/thread 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,834 @@
1
+ /**
2
+ * @file Built-in shader generators and the high-level DSL for GPU compute.
3
+ *
4
+ * Instead of writing raw WGSL, users describe their operation in simple
5
+ * terms and the module generates the shader automatically.
6
+ *
7
+ * **Three levels of abstraction:**
8
+ *
9
+ * ### 1. Built-in ops (zero shader code)
10
+ *
11
+ * ```js
12
+ * const gpu = new GPUCompute();
13
+ *
14
+ * // Element-wise multiply
15
+ * await gpu.run('multiply', {
16
+ * inputs: { data: new Float32Array([1, 2, 3]) },
17
+ * uniforms: { factor: new Float32Array([2.0]) },
18
+ * outputs: { result: 3 },
19
+ * });
20
+ * ```
21
+ *
22
+ * ### 2. Custom ops from a loop body
23
+ *
24
+ * ```js
25
+ * gpu.define('scaleClamp', {
26
+ * inputs: ['data'],
27
+ * outputs: ['result'],
28
+ * uniforms: ['factor', 'maxVal'],
29
+ * body: `result[i] = min(data[i] * factor, maxVal);`,
30
+ * });
31
+ *
32
+ * await gpu.run('scaleClamp', {
33
+ * inputs: { data: new Float32Array([1, 2, 3, 4, 5]) },
34
+ * uniforms: { factor: new Float32Array([2.0]), maxVal: new Float32Array([6.0]) },
35
+ * outputs: { result: 5 },
36
+ * });
37
+ * ```
38
+ *
39
+ * ### 3. Custom ops from a JS function (CPU fallback)
40
+ *
41
+ * ```js
42
+ * gpu.define('myOp', {
43
+ * inputs: ['x'],
44
+ * outputs: ['y'],
45
+ * body: `y[i] = sqrt(abs(x[i])) * 2.0;`,
46
+ * fn: (x) => Math.sqrt(Math.abs(x)) * 2, // used when GPU unavailable
47
+ * type: 'f32',
48
+ * });
49
+ * ```
50
+ *
51
+ * **Built-in ops:**
52
+ *
53
+ * | Op | Inputs | Uniforms | Description |
54
+ * |----------------|--------|----------|------------------------------------|
55
+ * | `multiply` | `a` | `b` | `result[i] = a[i] * b` |
56
+ * | `divide` | `a` | `b` | `result[i] = a[i] / b` |
57
+ * | `add` | `a` | `b` | `result[i] = a[i] + b` |
58
+ * | `subtract` | `a` | `b` | `result[i] = a[i] - b` |
59
+ * | `power` | `a` | `b` | `result[i] = pow(a[i], b)` |
60
+ * | `sqrt` | `data` | — | `result[i] = sqrt(data[i])` |
61
+ * | `abs` | `data` | — | `result[i] = abs(data[i])` |
62
+ * | `negate` | `data` | — | `result[i] = -data[i]` |
63
+ * | `sin` | `data` | — | `result[i] = sin(data[i])` |
64
+ * | `cos` | `data` | — | `result[i] = cos(data[i])` |
65
+ * | `tan` | `data` | — | `result[i] = tan(data[i])` |
66
+ * | `exp` | `data` | — | `result[i] = exp(data[i])` |
67
+ * | `log` | `data` | — | `result[i] = log(data[i])` |
68
+ * | `clamp` | `data` | `min,max`| `result[i] = clamp(data[i],mn,mx)`|
69
+ * | `lerp` | `a` | `b,t` | `result[i] = mix(a[i], b, t)` |
70
+ * | `sign` | `data` | — | `result[i] = sign(data[i])` |
71
+ * | `floor` | `data` | — | `result[i] = floor(data[i])` |
72
+ * | `ceil` | `data` | — | `result[i] = ceil(data[i])` |
73
+ * | `round` | `data` | — | `result[i] = round(data[i])` |
74
+ * | `fract` | `data` | — | `result[i] = fract(data[i])` |
75
+ * | `normalize` | `data` | — | `result[i] = data[i] / max(abs(data[i]), 1e-7)` |
76
+ * | `reciprocal` | `data` | — | `result[i] = 1.0 / data[i]` |
77
+ * | `square` | `data` | — | `result[i] = data[i] * data[i]` |
78
+ * | `copy` | `data` | — | `result[i] = data[i]` |
79
+ * | `fill` | — | `value` | `result[i] = value` |
80
+ * | `scaleOffset` | `data` | `scale,offset`| `result[i] = data[i] * scale + offset` |
81
+ * | `max` | `a` | `b` | `result[i] = max(a[i], b)` |
82
+ * | `min` | `a` | `b` | `result[i] = min(a[i], b)` |
83
+ * | `equal` | `a` | `b` | `result[i] = f32(a[i] == b)` |
84
+ * | `notEqual` | `a` | `b` | `result[i] = f32(a[i] != b)` |
85
+ * | `greaterThan` | `a` | `b` | `result[i] = f32(a[i] > b)` |
86
+ * | `lessThan` | `a` | `b` | `result[i] = f32(a[i] < b)` |
87
+ * | `select` | `a` | `b,cond` | `result[i] = select(a[i], b, cond[i])` |
88
+ *
89
+ * @module shaders
90
+ */
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // WGSL type helpers
94
+ // ---------------------------------------------------------------------------
95
+
96
+ const WGSL_TYPES = {
97
+ f32: { storage: 'f32', uniform: 'f32', array: 'array<f32>' },
98
+ i32: { storage: 'i32', uniform: 'i32', array: 'array<i32>' },
99
+ u32: { storage: 'u32', uniform: 'u32', array: 'array<u32>' },
100
+ f64: { storage: 'f32', uniform: 'f32', array: 'array<f32>' }, // WGSL has no f64
101
+ };
102
+
103
+ /**
104
+ * Resolve a type name to its WGSL representation.
105
+ *
106
+ * @param {string} type
107
+ * @returns {{ storage: string, uniform: string, array: string }}
108
+ */
109
+ function wgslType(type) {
110
+ return WGSL_TYPES[type] || WGSL_TYPES.f32;
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Shader builder
115
+ // ---------------------------------------------------------------------------
116
+
117
+ /**
118
+ * Build a complete WGSL compute shader from a declaration.
119
+ *
120
+ * This is the core engine that turns simple descriptions into valid
121
+ * WGSL. You normally don't call this directly — use `gpu.define()`
122
+ * or the built-in ops.
123
+ *
124
+ * @param {import('../types.js').ShaderDeclaration} decl
125
+ * @returns {string} Complete WGSL shader source.
126
+ *
127
+ * @example
128
+ * ```js
129
+ * const wgsl = buildShader({
130
+ * inputs: ['data'],
131
+ * outputs: ['result'],
132
+ * uniforms: ['factor'],
133
+ * body: `result[i] = data[i] * factor;`,
134
+ * type: 'f32',
135
+ * workgroupSize: 256,
136
+ * });
137
+ * // → valid WGSL string
138
+ * ```
139
+ */
140
+ export function buildShader(decl) {
141
+ const {
142
+ inputs = [],
143
+ outputs = [],
144
+ uniforms = [],
145
+ body,
146
+ type = 'f32',
147
+ workgroupSize = 256,
148
+ name = 'main',
149
+ } = decl;
150
+
151
+ if (!body || typeof body !== 'string') {
152
+ throw new TypeError('Shader declaration requires a body string');
153
+ }
154
+
155
+ const t = wgslType(type);
156
+ const lines = [];
157
+
158
+ // -- Bind group declarations --
159
+ let binding = 0;
160
+ const bindings = [];
161
+
162
+ for (const inputName of inputs) {
163
+ lines.push(`@group(0) @binding(${binding}) var<storage, read> ${inputName}: ${t.array};`);
164
+ bindings.push({ name: inputName, kind: 'input' });
165
+ binding++;
166
+ }
167
+
168
+ for (const uniformName of uniforms) {
169
+ // Multi-element uniforms use storage, single-element use uniform
170
+ lines.push(`@group(0) @binding(${binding}) var<uniform> ${uniformName}: ${t.uniform};`);
171
+ bindings.push({ name: uniformName, kind: 'uniform' });
172
+ binding++;
173
+ }
174
+
175
+ for (const outputName of outputs) {
176
+ lines.push(`@group(0) @binding(${binding}) var<storage, read_write> ${outputName}: ${t.array};`);
177
+ bindings.push({ name: outputName, kind: 'output' });
178
+ binding++;
179
+ }
180
+
181
+ lines.push('');
182
+
183
+ // -- Entry point --
184
+ lines.push(`@compute @workgroup_size(${workgroupSize})`);
185
+ lines.push(`fn ${name}(@builtin(global_invocation_id) id: vec3<u32>) {`);
186
+ lines.push(` let i = id.x;`);
187
+
188
+ // Bounds check based on the first output or input
189
+ const firstArray = outputs.length > 0 ? outputs[0] : inputs[0];
190
+ if (firstArray) {
191
+ lines.push(` if (i >= arrayLength(&${firstArray})) { return; }`);
192
+ }
193
+
194
+ lines.push('');
195
+ lines.push(` ${body.split('\n').join('\n ')}`);
196
+ lines.push('}');
197
+
198
+ return lines.join('\n');
199
+ }
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // Built-in operation definitions
203
+ // ---------------------------------------------------------------------------
204
+
205
+ /**
206
+ * Built-in operation registry. Each entry defines the inputs, uniforms,
207
+ * outputs, and WGSL body for a common GPU operation.
208
+ *
209
+ * @type {Object<string, import('../types.js').BuiltInOp>}
210
+ */
211
+ export const BUILT_IN_OPS = {
212
+ // ---- Arithmetic (binary: input + uniform scalar/vector) ----
213
+ multiply: {
214
+ inputs: ['a'], uniforms: ['b'], outputs: ['result'],
215
+ body: 'result[i] = a[i] * b;',
216
+ },
217
+ divide: {
218
+ inputs: ['a'], uniforms: ['b'], outputs: ['result'],
219
+ body: 'result[i] = a[i] / b;',
220
+ },
221
+ add: {
222
+ inputs: ['a'], uniforms: ['b'], outputs: ['result'],
223
+ body: 'result[i] = a[i] + b;',
224
+ },
225
+ subtract: {
226
+ inputs: ['a'], uniforms: ['b'], outputs: ['result'],
227
+ body: 'result[i] = a[i] - b;',
228
+ },
229
+ power: {
230
+ inputs: ['a'], uniforms: ['b'], outputs: ['result'],
231
+ body: 'result[i] = pow(a[i], b);',
232
+ },
233
+ max: {
234
+ inputs: ['a'], uniforms: ['b'], outputs: ['result'],
235
+ body: 'result[i] = max(a[i], b);',
236
+ },
237
+ min: {
238
+ inputs: ['a'], uniforms: ['b'], outputs: ['result'],
239
+ body: 'result[i] = min(a[i], b);',
240
+ },
241
+
242
+ // ---- Unary (single input, no uniforms) ----
243
+ sqrt: {
244
+ inputs: ['data'], outputs: ['result'],
245
+ body: 'result[i] = sqrt(data[i]);',
246
+ },
247
+ abs: {
248
+ inputs: ['data'], outputs: ['result'],
249
+ body: 'result[i] = abs(data[i]);',
250
+ },
251
+ negate: {
252
+ inputs: ['data'], outputs: ['result'],
253
+ body: 'result[i] = -data[i];',
254
+ },
255
+ sin: {
256
+ inputs: ['data'], outputs: ['result'],
257
+ body: 'result[i] = sin(data[i]);',
258
+ },
259
+ cos: {
260
+ inputs: ['data'], outputs: ['result'],
261
+ body: 'result[i] = cos(data[i]);',
262
+ },
263
+ tan: {
264
+ inputs: ['data'], outputs: ['result'],
265
+ body: 'result[i] = tan(data[i]);',
266
+ },
267
+ asin: {
268
+ inputs: ['data'], outputs: ['result'],
269
+ body: 'result[i] = asin(data[i]);',
270
+ },
271
+ acos: {
272
+ inputs: ['data'], outputs: ['result'],
273
+ body: 'result[i] = acos(data[i]);',
274
+ },
275
+ atan: {
276
+ inputs: ['data'], outputs: ['result'],
277
+ body: 'result[i] = atan(data[i]);',
278
+ },
279
+ exp: {
280
+ inputs: ['data'], outputs: ['result'],
281
+ body: 'result[i] = exp(data[i]);',
282
+ },
283
+ log: {
284
+ inputs: ['data'], outputs: ['result'],
285
+ body: 'result[i] = log(data[i]);',
286
+ },
287
+ sign: {
288
+ inputs: ['data'], outputs: ['result'],
289
+ body: 'result[i] = sign(data[i]);',
290
+ },
291
+ floor: {
292
+ inputs: ['data'], outputs: ['result'],
293
+ body: 'result[i] = floor(data[i]);',
294
+ },
295
+ ceil: {
296
+ inputs: ['data'], outputs: ['result'],
297
+ body: 'result[i] = ceil(data[i]);',
298
+ },
299
+ round: {
300
+ inputs: ['data'], outputs: ['result'],
301
+ body: 'result[i] = round(data[i]);',
302
+ },
303
+ fract: {
304
+ inputs: ['data'], outputs: ['result'],
305
+ body: 'result[i] = fract(data[i]);',
306
+ },
307
+ reciprocal: {
308
+ inputs: ['data'], outputs: ['result'],
309
+ body: 'result[i] = 1.0 / data[i];',
310
+ },
311
+ square: {
312
+ inputs: ['data'], outputs: ['result'],
313
+ body: 'result[i] = data[i] * data[i];',
314
+ },
315
+ copy: {
316
+ inputs: ['data'], outputs: ['result'],
317
+ body: 'result[i] = data[i];',
318
+ },
319
+ normalize: {
320
+ inputs: ['data'], outputs: ['result'],
321
+ body: 'result[i] = data[i] / max(abs(data[i]), 0.0000001);',
322
+ },
323
+
324
+ // ---- Fill (no inputs, uniform value) ----
325
+ fill: {
326
+ uniforms: ['value'], outputs: ['result'],
327
+ body: 'result[i] = value;',
328
+ },
329
+
330
+ // ---- Scale + offset ----
331
+ scaleOffset: {
332
+ inputs: ['data'], uniforms: ['scale', 'offset'], outputs: ['result'],
333
+ body: 'result[i] = data[i] * scale + offset;',
334
+ },
335
+
336
+ // ---- Clamp ----
337
+ clamp: {
338
+ inputs: ['data'], uniforms: ['minVal', 'maxVal'], outputs: ['result'],
339
+ body: 'result[i] = clamp(data[i], minVal, maxVal);',
340
+ },
341
+
342
+ // ---- Lerp / mix ----
343
+ lerp: {
344
+ inputs: ['a'], uniforms: ['b', 't'], outputs: ['result'],
345
+ body: 'result[i] = mix(a[i], b, t);',
346
+ },
347
+
348
+ // ---- Comparison (output i32 as 0.0/1.0) ----
349
+ equal: {
350
+ inputs: ['a'], uniforms: ['b'], outputs: ['result'],
351
+ body: 'result[i] = select(0.0, 1.0, a[i] == b);',
352
+ },
353
+ notEqual: {
354
+ inputs: ['a'], uniforms: ['b'], outputs: ['result'],
355
+ body: 'result[i] = select(0.0, 1.0, a[i] != b);',
356
+ },
357
+ greaterThan: {
358
+ inputs: ['a'], uniforms: ['b'], outputs: ['result'],
359
+ body: 'result[i] = select(0.0, 1.0, a[i] > b);',
360
+ },
361
+ lessThan: {
362
+ inputs: ['a'], uniforms: ['b'], outputs: ['result'],
363
+ body: 'result[i] = select(0.0, 1.0, a[i] < b);',
364
+ },
365
+ greaterThanEqual: {
366
+ inputs: ['a'], uniforms: ['b'], outputs: ['result'],
367
+ body: 'result[i] = select(0.0, 1.0, a[i] >= b);',
368
+ },
369
+ lessThanEqual: {
370
+ inputs: ['a'], uniforms: ['b'], outputs: ['result'],
371
+ body: 'result[i] = select(0.0, 1.0, a[i] <= b);',
372
+ },
373
+
374
+ // ---- Select ----
375
+ select: {
376
+ inputs: ['a', 'b', 'cond'], outputs: ['result'],
377
+ body: 'result[i] = select(a[i], b[i], cond[i] > 0.0);',
378
+ },
379
+
380
+ // ---- Clamp to range (two-input with two uniforms) ----
381
+ clampRange: {
382
+ inputs: ['data'], uniforms: ['low', 'high'], outputs: ['result'],
383
+ body: 'result[i] = clamp(data[i], low, high);',
384
+ },
385
+
386
+ // ---- Extended math (binary) ----
387
+ atan2: {
388
+ inputs: ['a'], uniforms: ['b'], outputs: ['result'],
389
+ body: 'result[i] = atan2(a[i], b);',
390
+ },
391
+ hypot: {
392
+ inputs: ['a'], uniforms: ['b'], outputs: ['result'],
393
+ body: 'result[i] = sqrt(a[i] * a[i] + b * b);',
394
+ },
395
+ mod: {
396
+ inputs: ['a'], uniforms: ['b'], outputs: ['result'],
397
+ body: 'result[i] = a[i] - b * floor(a[i] / b);',
398
+ },
399
+ diff: {
400
+ inputs: ['a', 'b'], outputs: ['result'],
401
+ body: 'result[i] = a[i] - b[i];',
402
+ },
403
+ sum: {
404
+ inputs: ['a', 'b'], outputs: ['result'],
405
+ body: 'result[i] = a[i] + b[i];',
406
+ },
407
+ product: {
408
+ inputs: ['a', 'b'], outputs: ['result'],
409
+ body: 'result[i] = a[i] * b[i];',
410
+ },
411
+
412
+ // ---- Extended math (unary) ----
413
+ cbrt: {
414
+ inputs: ['data'], outputs: ['result'],
415
+ body: 'result[i] = sign(data[i]) * pow(abs(data[i]), 1.0 / 3.0);',
416
+ },
417
+ log2: {
418
+ inputs: ['data'], outputs: ['result'],
419
+ body: 'result[i] = log2(data[i]);',
420
+ },
421
+ log10: {
422
+ inputs: ['data'], outputs: ['result'],
423
+ body: 'result[i] = log(data[i]) / log(10.0);',
424
+ },
425
+ exp2: {
426
+ inputs: ['data'], outputs: ['result'],
427
+ body: 'result[i] = exp2(data[i]);',
428
+ },
429
+ tanh: {
430
+ inputs: ['data'], outputs: ['result'],
431
+ body: 'result[i] = tanh(data[i]);',
432
+ },
433
+ sinh: {
434
+ inputs: ['data'], outputs: ['result'],
435
+ body: 'result[i] = sinh(data[i]);',
436
+ },
437
+ cosh: {
438
+ inputs: ['data'], outputs: ['result'],
439
+ body: 'result[i] = cosh(data[i]);',
440
+ },
441
+ trunc: {
442
+ inputs: ['data'], outputs: ['result'],
443
+ body: 'result[i] = trunc(data[i]);',
444
+ },
445
+ expm1: {
446
+ inputs: ['data'], outputs: ['result'],
447
+ body: 'result[i] = exp(data[i]) - 1.0;',
448
+ },
449
+ log1p: {
450
+ inputs: ['data'], outputs: ['result'],
451
+ body: 'result[i] = log(1.0 + data[i]);',
452
+ },
453
+
454
+ // ---- Interpolation ----
455
+ smoothstep: {
456
+ inputs: ['data'], uniforms: ['edge0', 'edge1'], outputs: ['result'],
457
+ body: 'result[i] = smoothstep(edge0, edge1, data[i]);',
458
+ },
459
+ step: {
460
+ inputs: ['data'], uniforms: ['edge'], outputs: ['result'],
461
+ body: 'result[i] = step(edge, data[i]);',
462
+ },
463
+
464
+ // ---- Financial ----
465
+ pctChange: {
466
+ inputs: ['a', 'b'], outputs: ['result'],
467
+ body: 'result[i] = (b[i] - a[i]) / max(abs(a[i]), 1e-10);',
468
+ },
469
+ ema: {
470
+ inputs: ['prev', 'data'], uniforms: ['alpha'], outputs: ['result'],
471
+ body: 'result[i] = alpha * data[i] + (1.0 - alpha) * prev[i];',
472
+ },
473
+
474
+ // ---- Vector-like (two-input element-wise) ----
475
+ max2: {
476
+ inputs: ['a', 'b'], outputs: ['result'],
477
+ body: 'result[i] = max(a[i], b[i]);',
478
+ },
479
+ min2: {
480
+ inputs: ['a', 'b'], outputs: ['result'],
481
+ body: 'result[i] = min(a[i], b[i]);',
482
+ },
483
+ };
484
+
485
+ // ---------------------------------------------------------------------------
486
+ // Multi-pass operations (reduction, scan, etc.)
487
+ // ---------------------------------------------------------------------------
488
+ // These ops produce a single output from many inputs. They use
489
+ // workgroup-level reduction with shared memory and return partial
490
+ // results that are combined on the CPU.
491
+
492
+ /**
493
+ * Workgroup reduction shader — produces one value per workgroup.
494
+ *
495
+ * @type {Object}
496
+ */
497
+ export const REDUCE_SHADER = {
498
+ inputs: ['data'],
499
+ uniforms: ['count'],
500
+ outputs: ['partial'],
501
+ body: `
502
+ var<workgroup> shared_data: array<f32, 256>;
503
+
504
+ let tid = local_id.x;
505
+ let i = id.x;
506
+
507
+ if (i < count) {
508
+ shared_data[tid] = data[i];
509
+ } else {
510
+ shared_data[tid] = 0.0;
511
+ }
512
+ workgroupBarrier();
513
+
514
+ for (var stride = 128u; stride > 0u; stride >>= 1u) {
515
+ if (tid < stride) {
516
+ shared_data[tid] = shared_data[tid] + shared_data[tid + stride];
517
+ }
518
+ workgroupBarrier();
519
+ }
520
+
521
+ if (tid == 0u) {
522
+ partial[wg_id.x] = shared_data[0];
523
+ }
524
+ `,
525
+ special: 'reduce_sum',
526
+ };
527
+
528
+ export const REDUCE_MIN_SHADER = {
529
+ inputs: ['data'],
530
+ uniforms: ['count'],
531
+ outputs: ['partial'],
532
+ body: `
533
+ var<workgroup> shared_data: array<f32, 256>;
534
+
535
+ let tid = local_id.x;
536
+ let i = id.x;
537
+
538
+ if (i < count) {
539
+ shared_data[tid] = data[i];
540
+ } else {
541
+ shared_data[tid] = 3.4028235e+38;
542
+ }
543
+ workgroupBarrier();
544
+
545
+ for (var stride = 128u; stride > 0u; stride >>= 1u) {
546
+ if (tid < stride) {
547
+ shared_data[tid] = min(shared_data[tid], shared_data[tid + stride]);
548
+ }
549
+ workgroupBarrier();
550
+ }
551
+
552
+ if (tid == 0u) {
553
+ partial[wg_id.x] = shared_data[0];
554
+ }
555
+ `,
556
+ special: 'reduce_min',
557
+ };
558
+
559
+ export const REDUCE_MAX_SHADER = {
560
+ inputs: ['data'],
561
+ uniforms: ['count'],
562
+ outputs: ['partial'],
563
+ body: `
564
+ var<workgroup> shared_data: array<f32, 256>;
565
+
566
+ let tid = local_id.x;
567
+ let i = id.x;
568
+
569
+ if (i < count) {
570
+ shared_data[tid] = data[i];
571
+ } else {
572
+ shared_data[tid] = -3.4028235e+38;
573
+ }
574
+ workgroupBarrier();
575
+
576
+ for (var stride = 128u; stride > 0u; stride >>= 1u) {
577
+ if (tid < stride) {
578
+ shared_data[tid] = max(shared_data[tid], shared_data[tid + stride]);
579
+ }
580
+ workgroupBarrier();
581
+ }
582
+
583
+ if (tid == 0u) {
584
+ partial[wg_id.x] = shared_data[0];
585
+ }
586
+ `,
587
+ special: 'reduce_max',
588
+ };
589
+
590
+ // ---------------------------------------------------------------------------
591
+ // Matrix multiply (tiled with shared memory)
592
+ // ---------------------------------------------------------------------------
593
+
594
+ export const MATMUL_SHADER = {
595
+ inputs: ['A', 'B'],
596
+ uniforms: ['M', 'N', 'K'],
597
+ outputs: ['C'],
598
+ body: `
599
+ let TILE = 16u;
600
+ var<workgroup> tileA: array<f32, 256>;
601
+ var<workgroup> tileB: array<f32, 256>;
602
+
603
+ let row = id.y;
604
+ let col = id.x;
605
+ let tid = local_id.y * 16u + local_id.x;
606
+
607
+ var sum: f32 = 0.0;
608
+
609
+ for (var t = 0u; t < (K + TILE - 1u) / TILE; t++) {
610
+ let aCol = t * TILE + local_id.x;
611
+ if (row < M && aCol < K) {
612
+ tileA[tid] = A[row * K + aCol];
613
+ } else {
614
+ tileA[tid] = 0.0;
615
+ }
616
+
617
+ let bRow = t * TILE + local_id.y;
618
+ if (bRow < K && col < N) {
619
+ tileB[tid] = B[bRow * N + col];
620
+ } else {
621
+ tileB[tid] = 0.0;
622
+ }
623
+
624
+ workgroupBarrier();
625
+
626
+ for (var k = 0u; k < TILE; k++) {
627
+ sum += tileA[local_id.y * TILE + k] * tileB[k * TILE + local_id.x];
628
+ }
629
+
630
+ workgroupBarrier();
631
+ }
632
+
633
+ if (row < M && col < N) {
634
+ C[row * N + col] = sum;
635
+ }
636
+ `,
637
+ special: 'matmul',
638
+ workgroupSize: 16,
639
+ dispatch: [0, 0], // [Math.ceil(N/16), Math.ceil(M/16)]
640
+ };
641
+
642
+ // ---------------------------------------------------------------------------
643
+ // Histogram — count occurrences in bins
644
+ // ---------------------------------------------------------------------------
645
+
646
+ export const HISTOGRAM_SHADER = {
647
+ inputs: ['data'],
648
+ uniforms: ['count', 'numBins'],
649
+ outputs: ['bins'],
650
+ body: `
651
+ var<workgroup> local_bins: array<u32, 256>;
652
+
653
+ let tid = local_id.x;
654
+ let i = id.x;
655
+ let n = count;
656
+ let nb = numBins;
657
+
658
+ if (tid < 256u) { local_bins[tid] = 0u; }
659
+ workgroupBarrier();
660
+
661
+ if (i < n && nb > 0u) {
662
+ let bin = clamp(u32(data[i] * f32(nb) / 1000.0), 0u, nb - 1u);
663
+ atomicAdd(&local_bins[bin], 1u);
664
+ }
665
+ workgroupBarrier();
666
+
667
+ if (tid < 256u) {
668
+ atomicAdd(&bins[tid], local_bins[tid]);
669
+ }
670
+ `,
671
+ special: 'histogram',
672
+ };
673
+
674
+ // ---------------------------------------------------------------------------
675
+ // ArgMax / ArgMin — find index of extreme value
676
+ // ---------------------------------------------------------------------------
677
+
678
+ export const ARGMAX_SHADER = {
679
+ inputs: ['data'],
680
+ uniforms: ['count'],
681
+ outputs: ['partial_val', 'partial_idx'],
682
+ body: `
683
+ var<workgroup> shared_val: array<f32, 256>;
684
+ var<workgroup> shared_idx: array<u32, 256>;
685
+
686
+ let tid = local_id.x;
687
+ let i = id.x;
688
+
689
+ if (i < count) {
690
+ shared_val[tid] = data[i];
691
+ shared_idx[tid] = u32(i);
692
+ } else {
693
+ shared_val[tid] = -3.4028235e+38;
694
+ shared_idx[tid] = 0u;
695
+ }
696
+ workgroupBarrier();
697
+
698
+ for (var stride = 128u; stride > 0u; stride >>= 1u) {
699
+ if (tid < stride) {
700
+ if (shared_val[tid + stride] > shared_val[tid]) {
701
+ shared_val[tid] = shared_val[tid + stride];
702
+ shared_idx[tid] = shared_idx[tid + stride];
703
+ }
704
+ }
705
+ workgroupBarrier();
706
+ }
707
+
708
+ if (tid == 0u) {
709
+ partial_val[wg_id.x] = shared_val[0];
710
+ partial_idx[wg_id.x] = shared_idx[0];
711
+ }
712
+ `,
713
+ special: 'argmax',
714
+ };
715
+
716
+ export const ARGMIN_SHADER = {
717
+ inputs: ['data'],
718
+ uniforms: ['count'],
719
+ outputs: ['partial_val', 'partial_idx'],
720
+ body: `
721
+ var<workgroup> shared_val: array<f32, 256>;
722
+ var<workgroup> shared_idx: array<u32, 256>;
723
+
724
+ let tid = local_id.x;
725
+ let i = id.x;
726
+
727
+ if (i < count) {
728
+ shared_val[tid] = data[i];
729
+ shared_idx[tid] = u32(i);
730
+ } else {
731
+ shared_val[tid] = 3.4028235e+38;
732
+ shared_idx[tid] = 0u;
733
+ }
734
+ workgroupBarrier();
735
+
736
+ for (var stride = 128u; stride > 0u; stride >>= 1u) {
737
+ if (tid < stride) {
738
+ if (shared_val[tid + stride] < shared_val[tid]) {
739
+ shared_val[tid] = shared_val[tid + stride];
740
+ shared_idx[tid] = shared_idx[tid + stride];
741
+ }
742
+ }
743
+ workgroupBarrier();
744
+ }
745
+
746
+ if (tid == 0u) {
747
+ partial_val[wg_id.x] = shared_val[0];
748
+ partial_idx[wg_id.x] = shared_idx[0];
749
+ }
750
+ `,
751
+ special: 'argmin',
752
+ };
753
+
754
+ // ---------------------------------------------------------------------------
755
+ // Scan — parallel prefix sum
756
+ // ---------------------------------------------------------------------------
757
+
758
+ export const SCAN_SHADER = {
759
+ inputs: ['data'],
760
+ uniforms: ['count'],
761
+ outputs: ['output'],
762
+ body: `
763
+ var<workgroup> temp: array<f32, 512>;
764
+
765
+ let tid = local_id.x;
766
+ let i = id.x;
767
+
768
+ // Load into temp (double workgroup for scan)
769
+ if (i < count) {
770
+ temp[tid] = data[i];
771
+ } else {
772
+ temp[tid] = 0.0;
773
+ }
774
+ workgroupBarrier();
775
+
776
+ // Up-sweep
777
+ for (var stride = 1u; stride < 256u; stride <<= 1u) {
778
+ let idx = (tid + 1u) * (stride * 2u) - 1u;
779
+ if (idx < 512u && idx >= stride) {
780
+ temp[idx] += temp[idx - stride];
781
+ }
782
+ workgroupBarrier();
783
+ }
784
+
785
+ // Clear last element
786
+ if (tid == 0u) {
787
+ temp[511u] = 0.0;
788
+ }
789
+ workgroupBarrier();
790
+
791
+ // Down-sweep
792
+ for (var stride = 256u; stride > 0u; stride >>= 1u) {
793
+ let idx = (tid + 1u) * (stride * 2u) - 1u;
794
+ if (idx < 512u && idx >= stride) {
795
+ let t = temp[idx - stride];
796
+ temp[idx - stride] = temp[idx];
797
+ temp[idx] += t;
798
+ }
799
+ workgroupBarrier();
800
+ }
801
+
802
+ if (i < count) {
803
+ output[i] = temp[tid + 1u];
804
+ }
805
+ `,
806
+ special: 'scan',
807
+ };
808
+
809
+ // ---------------------------------------------------------------------------
810
+ // List of all built-in op names
811
+ // ---------------------------------------------------------------------------
812
+
813
+ /**
814
+ * Array of all built-in operation names.
815
+ *
816
+ * @type {string[]}
817
+ */
818
+ export const BUILT_IN_OP_NAMES = Object.keys(BUILT_IN_OPS);
819
+
820
+ /**
821
+ * Multi-pass special operation registry.
822
+ *
823
+ * @type {Object<string, Object>}
824
+ */
825
+ export const SPECIAL_OPS = {
826
+ reduce_sum: REDUCE_SHADER,
827
+ reduce_min: REDUCE_MIN_SHADER,
828
+ reduce_max: REDUCE_MAX_SHADER,
829
+ matmul: MATMUL_SHADER,
830
+ histogram: HISTOGRAM_SHADER,
831
+ argmax: ARGMAX_SHADER,
832
+ argmin: ARGMIN_SHADER,
833
+ scan: SCAN_SHADER,
834
+ };