@woosh/meep-engine 3.11.1 → 3.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/package.json +1 -1
  2. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  3. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +294 -290
  4. package/src/engine/graphics3/ParticipatingMedia.d.ts +111 -0
  5. package/src/engine/graphics3/ParticipatingMedia.d.ts.map +1 -0
  6. package/src/engine/graphics3/ParticipatingMedia.js +203 -0
  7. package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.d.ts +34 -0
  8. package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.d.ts.map +1 -0
  9. package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.js +73 -0
  10. package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts +87 -0
  11. package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts.map +1 -0
  12. package/src/engine/graphics3/ParticipatingMediaSystem3.js +184 -0
  13. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  14. package/src/shade/playground/volumetrics_froxel/README.md +104 -1
  15. package/src/shade/playground/volumetrics_froxel/index.html +20 -1
  16. package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -1
  17. package/src/shade/playground/volumetrics_froxel/main.js +298 -0
  18. package/src/shade/playground/volumetrics_froxel/measure.d.ts +119 -0
  19. package/src/shade/playground/volumetrics_froxel/measure.d.ts.map +1 -0
  20. package/src/shade/playground/volumetrics_froxel/measure.js +559 -0
  21. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  22. package/src/shade/renderer/Renderer.js +22 -18
  23. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  24. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts +14 -13
  25. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts.map +1 -1
  26. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.js +133 -129
  27. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  28. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +480 -315
  29. package/src/shade/renderer/volumetrics/NOTES.md +649 -8
  30. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts +5 -0
  31. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts.map +1 -1
  32. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.js +21 -2
  33. package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts +26 -0
  34. package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts.map +1 -1
  35. package/src/shade/renderer/volumetrics/SceneVolumetrics.js +64 -21
  36. package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.d.ts.map +1 -1
  37. package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.js +84 -28
  38. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts +98 -0
  39. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts.map +1 -0
  40. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.js +111 -0
  41. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts +84 -0
  42. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts.map +1 -0
  43. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.js +252 -0
  44. package/src/shade/renderer/volumetrics/chunk_integrate_optical_depth.js +75 -75
  45. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts +43 -0
  46. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts.map +1 -0
  47. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.js +145 -0
  48. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts +20 -0
  49. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts.map +1 -0
  50. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.js +48 -0
  51. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts +38 -0
  52. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts.map +1 -0
  53. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.js +107 -0
  54. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +4 -1
  55. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
  56. package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +577 -471
  57. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
  58. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +64 -13
  59. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts +8 -0
  60. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts.map +1 -0
  61. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.js +174 -0
  62. package/src/shade/wgsl/emulator/CPUBitmapData.d.ts +25 -0
  63. package/src/shade/wgsl/emulator/CPUBitmapData.d.ts.map +1 -1
  64. package/src/shade/wgsl/emulator/CPUBitmapData.js +33 -0
  65. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +2221 -2215
  66. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
  67. package/src/shade/wgsl/emulator/WGSLLanguageCore.js +1297 -1117
  68. package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts +69 -0
  69. package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts.map +1 -0
  70. package/src/shade/wgsl/emulator/cpu_texture_gather.js +148 -0
  71. package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts +16 -0
  72. package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts.map +1 -1
  73. package/src/shade/wgsl/emulator/cpu_texture_sample.js +176 -172
@@ -1,1117 +1,1297 @@
1
- import { assert } from "../../../core/assert.js";
2
- import { cpu_texture_sample } from "./cpu_texture_sample.js";
3
-
4
- /**
5
- * Default sampler descriptor for `textureSample` / `textureSampleLevel`
6
- * calls when the bound sampler is `null` (common in tests). Linear filter,
7
- * clamp-to-edge on every axis - matches what most engine passes use.
8
- */
9
- const FALLBACK_LINEAR_CLAMP_SAMPLER = Object.freeze({
10
- addressModeU: "clamp-to-edge",
11
- addressModeV: "clamp-to-edge",
12
- addressModeW: "clamp-to-edge",
13
- magFilter: "linear",
14
- minFilter: "linear",
15
- mipmapFilter: "nearest",
16
- });
17
-
18
- /**
19
- * Single-element scratch view used to round a number to IEEE 754 binary16 precision.
20
- * @type {Float16Array}
21
- */
22
- const F16_SCRATCH = new Float16Array(1);
23
-
24
- /**
25
- * Shared scratch buffer for `bitcast`three views over the same 4 bytes so we can
26
- * reinterpret one type as another in O(1) without per-call allocation.
27
- */
28
- const BITCAST_BUFFER = new ArrayBuffer(4);
29
- const BITCAST_U32 = new Uint32Array(BITCAST_BUFFER);
30
- const BITCAST_I32 = new Int32Array(BITCAST_BUFFER);
31
- const BITCAST_F32 = new Float32Array(BITCAST_BUFFER);
32
-
33
- /**
34
- * Construct a WGSL vector. Mirrors WGSL `vecN<T>(...)` semantics:
35
- *
36
- * - **No args** → zero-filled.
37
- * - **One scalar** → broadcast to all `size` components (`vec3(1.0)` → `[1, 1, 1]`).
38
- * - **Exactly `size` scalars** → use as components.
39
- * - **Any mix of scalars and shorter vectors** that flattens to exactly `size` numbers (e.g.
40
- * `vec3(vec2_xy, z)` or `vec4(x, vec2_yz, w)`).
41
- *
42
- * @param {Float32ArrayConstructor | Float16ArrayConstructor | Int32ArrayConstructor | Uint32ArrayConstructor} TypedArrayCtor
43
- * @param {number} size
44
- * @param {*[]} args
45
- * @returns {*}
46
- */
47
- function makeVec(TypedArrayCtor, size, args) {
48
- if (args.length === 0) {
49
- return new TypedArrayCtor(size);
50
- }
51
- // Single scalarbroadcast.
52
- if (args.length === 1 && typeof args[0] === 'number') {
53
- const v = args[0];
54
- const out = new TypedArrayCtor(size);
55
- for (let i = 0; i < size; i++) out[i] = v;
56
- return out;
57
- }
58
- // Otherwise flatten.
59
- const out = new TypedArrayCtor(size);
60
- let i = 0;
61
- for (const a of args) {
62
- if (typeof a === 'number' || typeof a === 'boolean') {
63
- out[i++] = +a;
64
- } else if (a != null && typeof a.length === 'number') {
65
- for (let j = 0; j < a.length; j++) out[i++] = a[j];
66
- }
67
- }
68
- return out;
69
- }
70
-
71
- /**
72
- * Build a matrix constructor returning a typed array of `total_elements` entries. The returned
73
- * function accepts (a) no args → zero-filled, (b) `total_elements` scalars in column-major order,
74
- * (c) one column-vec per matrix column, or (d) any mix that flattens to `total_elements` numbers.
75
- *
76
- * @param {Float32ArrayConstructor | Float16ArrayConstructor} TypedArrayCtor
77
- * @param {number} total_elements
78
- * @returns {function(...*): Float32Array | Float16Array}
79
- */
80
- function makeMatCtor(TypedArrayCtor, total_elements) {
81
- return (...args) => {
82
- if (args.length === 0) {
83
- return new TypedArrayCtor(total_elements);
84
- }
85
- const flat = new TypedArrayCtor(total_elements);
86
- let i = 0;
87
- for (const a of args) {
88
- if (typeof a === 'number') {
89
- flat[i++] = a;
90
- } else if (a != null && typeof a.length === 'number') {
91
- for (let j = 0; j < a.length; j++) flat[i++] = a[j];
92
- }
93
- }
94
- return flat;
95
- };
96
- }
97
-
98
- /**
99
- * @param {number} a
100
- * @returns {number}
101
- */
102
- function as_i32(a) {
103
- return (a | 0);
104
- }
105
-
106
- /**
107
- * @param {number} a
108
- * @returns {number}
109
- */
110
- function as_u32(a) {
111
- return (a >>> 0);
112
- }
113
-
114
- /**
115
- * Determine if a value is a vector — TypedArray (vec2f / vec3i / etc) or
116
- * a plain JS Array (returned from comparison ops like `eq` / `ge`, since
117
- * those produce arrays of bools). Either way, a vec is a length-N
118
- * iterable of element values, distinguished from a scalar number or a
119
- * struct.
120
- * @param {*} v
121
- * @returns {boolean}
122
- */
123
- function is_vec(v) {
124
- if (v == null || typeof v !== 'object') return false;
125
- if (typeof v.BYTES_PER_ELEMENT === 'number') return true; // TypedArray (vecNf / vecNi / ...)
126
- if (Array.isArray(v)) return true; // plain JS Array — vec result from binop_cmp
127
- return false;
128
- }
129
-
130
- /**
131
- * Element-wise apply a binary op over a vector and scalar/vector counterpart.
132
- * @param {*} a
133
- * @param {*} b
134
- * @param {function(number, number): number} op
135
- * @returns {*}
136
- */
137
- function binop(a, b, op) {
138
- const a_vec = is_vec(a);
139
- const b_vec = is_vec(b);
140
-
141
- if (!a_vec && !b_vec) {
142
- return op(a, b);
143
- }
144
-
145
- if (a_vec && b_vec) {
146
- assert.equal(a.length, b.length, 'vector length mismatch');
147
- const out = new a.constructor(a.length);
148
- for (let i = 0; i < a.length; i++) {
149
- out[i] = op(a[i], b[i]);
150
- }
151
- return out;
152
- }
153
-
154
- if (a_vec) {
155
- const out = new a.constructor(a.length);
156
- for (let i = 0; i < a.length; i++) {
157
- out[i] = op(a[i], b);
158
- }
159
- return out;
160
- }
161
-
162
- // b_vec
163
- const out = new b.constructor(b.length);
164
- for (let i = 0; i < b.length; i++) {
165
- out[i] = op(a, b[i]);
166
- }
167
- return out;
168
- }
169
-
170
- /**
171
- * Element-wise apply a comparison op. For scalar operands returns a single boolean; for
172
- * vector operands returns a plain `Array<boolean>` matching the WGSL `vec<bool>` shape.
173
- * @param {*} a
174
- * @param {*} b
175
- * @param {function(number, number): boolean} op
176
- * @returns {boolean | boolean[]}
177
- */
178
- function binop_cmp(a, b, op) {
179
- const a_vec = is_vec(a);
180
- const b_vec = is_vec(b);
181
-
182
- if (!a_vec && !b_vec) {
183
- return op(a, b);
184
- }
185
- const length = a_vec ? a.length : b.length;
186
- const out = new Array(length);
187
- for (let i = 0; i < length; i++) {
188
- const ai = a_vec ? a[i] : a;
189
- const bi = b_vec ? b[i] : b;
190
- out[i] = op(ai, bi);
191
- }
192
- return out;
193
- }
194
-
195
- /**
196
- * Element-wise apply a unary op over a scalar/vector.
197
- * @param {*} a
198
- * @param {function(number): number} op
199
- * @returns {*}
200
- */
201
- function unop(a, op) {
202
- if (!is_vec(a)) {
203
- return op(a);
204
- }
205
- const out = new a.constructor(a.length);
206
- for (let i = 0; i < a.length; i++) {
207
- out[i] = op(a[i]);
208
- }
209
- return out;
210
- }
211
-
212
- /**
213
- * Element-wise apply a ternary op (e.g. mix, clamp).
214
- * @param {*} a
215
- * @param {*} b
216
- * @param {*} c
217
- * @param {function(number, number, number): number} op
218
- * @returns {*}
219
- */
220
- function ternop(a, b, c, op) {
221
- if (is_vec(a) || is_vec(b) || is_vec(c)) {
222
- const length = (is_vec(a) ? a.length : (is_vec(b) ? b.length : c.length));
223
- const ctor = (is_vec(a) ? a.constructor : (is_vec(b) ? b.constructor : c.constructor));
224
- const out = new ctor(length);
225
- for (let i = 0; i < length; i++) {
226
- const ai = is_vec(a) ? a[i] : a;
227
- const bi = is_vec(b) ? b[i] : b;
228
- const ci = is_vec(c) ? c[i] : c;
229
- out[i] = op(ai, bi, ci);
230
- }
231
- return out;
232
- }
233
- return op(a, b, c);
234
- }
235
-
236
- /**
237
- * The default WGSL language core the `wgsl` object passed into the generated JS.
238
- *
239
- * Provides:
240
- * - vector / matrix constructors (`vec3f`, `vec2u`, etc.)
241
- * - scalar constructors / casts (`f32`, `i32`, `u32`, `bool`)
242
- * - arithmetic helpers (`add`, `sub`, `mul`, `div`, `mod`, `neg`)
243
- * - WGSL builtins (`mix`, `clamp`, `min`, `max`, `abs`, `floor`, `ceil`, `pow`, ...)
244
- * - texture intrinsics (`textureLoad`, `textureSampleLevel`)
245
- * - control flow helpers (`workgroupBarrier`, `storageBarrier`)
246
- *
247
- * @type {object}
248
- */
249
- export const WGSLLanguageCore = {
250
- // ---------------------------------------------------------------------
251
- // Scalar casts
252
- // ---------------------------------------------------------------------
253
-
254
- /** Cast to i32. */
255
- i32(v) {
256
- if (typeof v === 'boolean') return v ? 1 : 0;
257
- return as_i32(v);
258
- },
259
-
260
- /** Cast to u32. */
261
- u32(v) {
262
- if (typeof v === 'boolean') return v ? 1 : 0;
263
- return as_u32(v);
264
- },
265
-
266
- /** Cast to f32 (no-op in JS, returns the number). */
267
- f32(v) {
268
- if (typeof v === 'boolean') return v ? 1 : 0;
269
- return Math.fround(+v);
270
- },
271
-
272
- /** Cast to f16. Truncates to IEEE 754 binary16 precision via Float16Array. */
273
- f16(v) {
274
- if (typeof v === 'boolean') return v ? 1 : 0;
275
- F16_SCRATCH[0] = +v;
276
- return F16_SCRATCH[0];
277
- },
278
-
279
- /** Cast to bool. */
280
- bool(v) {
281
- return !!v;
282
- },
283
-
284
- // ---------------------------------------------------------------------
285
- // Vector constructors
286
- // ---------------------------------------------------------------------
287
-
288
- vec2f: (...args) => makeVec(Float32Array, 2, args),
289
- vec3f: (...args) => makeVec(Float32Array, 3, args),
290
- vec4f: (...args) => makeVec(Float32Array, 4, args),
291
-
292
- vec2h: (...args) => makeVec(Float16Array, 2, args),
293
- vec3h: (...args) => makeVec(Float16Array, 3, args),
294
- vec4h: (...args) => makeVec(Float16Array, 4, args),
295
-
296
- vec2i: (...args) => makeVec(Int32Array, 2, args),
297
- vec3i: (...args) => makeVec(Int32Array, 3, args),
298
- vec4i: (...args) => makeVec(Int32Array, 4, args),
299
-
300
- vec2u: (...args) => makeVec(Uint32Array, 2, args),
301
- vec3u: (...args) => makeVec(Uint32Array, 3, args),
302
- vec4u: (...args) => makeVec(Uint32Array, 4, args),
303
-
304
- // ---------------------------------------------------------------------
305
- // Matrix constructors column-major flat arrays. matCxR has C columns of R rows each,
306
- // for a total of C*R entries. Args may be scalars, column vectors, or a mix.
307
- // ---------------------------------------------------------------------
308
-
309
- mat2x2f: makeMatCtor(Float32Array, 4),
310
- mat2x3f: makeMatCtor(Float32Array, 6),
311
- mat2x4f: makeMatCtor(Float32Array, 8),
312
- mat3x2f: makeMatCtor(Float32Array, 6),
313
- mat3x3f: makeMatCtor(Float32Array, 9),
314
- mat3x4f: makeMatCtor(Float32Array, 12),
315
- mat4x2f: makeMatCtor(Float32Array, 8),
316
- mat4x3f: makeMatCtor(Float32Array, 12),
317
- mat4x4f: makeMatCtor(Float32Array, 16),
318
-
319
- mat2x2h: makeMatCtor(Float16Array, 4),
320
- mat2x3h: makeMatCtor(Float16Array, 6),
321
- mat2x4h: makeMatCtor(Float16Array, 8),
322
- mat3x2h: makeMatCtor(Float16Array, 6),
323
- mat3x3h: makeMatCtor(Float16Array, 9),
324
- mat3x4h: makeMatCtor(Float16Array, 12),
325
- mat4x2h: makeMatCtor(Float16Array, 8),
326
- mat4x3h: makeMatCtor(Float16Array, 12),
327
- mat4x4h: makeMatCtor(Float16Array, 16),
328
-
329
- /**
330
- * Return column `i` of a column-major matrix as a vec view (no copy). Used by the compiler
331
- * to translate WGSL `m[i]` — which yields a vec — onto our flat-typed-array storage.
332
- * @param {Float32Array|Float16Array} m
333
- * @param {number} i
334
- * @param {number} rows
335
- */
336
- mat_col(m, i, rows) {
337
- return m.subarray(i * rows, (i + 1) * rows);
338
- },
339
-
340
- /**
341
- * Store a whole value through a pointer: WGSL's `*p = value`. Returns what the pointer variable
342
- * should now hold, which the caller assigns back.
343
- *
344
- * Pointers are modelled as the pointee itself, so `*p = value` used to compile to a plain
345
- * assignment to the parameter binding which rebinds the callee's local and leaves the
346
- * caller's object untouched. Every shader returning a result through an out-parameter then read
347
- * back whatever it had initialised the variable to, silently and with nothing raised:
348
- * `ray_triangle_compute_intersection_barycentric` reported hits at a distance of zero, which is
349
- * a hit the traversal keeps and shades.
350
- *
351
- * Vectors, matrices and structs are objects, so the store goes through the reference and the
352
- * caller sees it. **A pointer to a scalar still cannot**: a JavaScript number has no identity
353
- * to write through, so the rebind is all there is, and a caller passing `&some_f32` sees its
354
- * variable unchanged. Within the callee the value is right — `*p` reads back what `*p = …`
355
- * wrote — so the in-out scalar accumulators in `sample_discrete_wrs_*` behave, and only a
356
- * result handed *out* through a scalar pointer is lost. Widen such an out-parameter to a
357
- * vector or a struct, or return it, if a test needs to see it.
358
- *
359
- * @param {Float32Array|Float16Array|Uint32Array|Int32Array|Array|object|number|boolean} target
360
- * @param {ArrayLike<number>|object|number|boolean} value
361
- * @returns {*} the value the pointer variable should be rebound to
362
- */
363
- store_ptr(target, value) {
364
- if (ArrayBuffer.isView(target) || Array.isArray(target)) {
365
- for (let i = 0; i < target.length; i++) {
366
- target[i] = value[i];
367
- }
368
-
369
- return target;
370
- }
371
-
372
- if (target !== null && typeof target === 'object') {
373
- Object.assign(target, value);
374
-
375
- return target;
376
- }
377
-
378
- return value;
379
- },
380
-
381
- /**
382
- * Determinant of a square matrix stored as a flat column-major typed array.
383
- * Supports the WGSL square-matrix sizes: 2x2, 3x3, 4x4.
384
- * @param {Float32Array|Float16Array} m
385
- */
386
- determinant(m) {
387
- if (m.length === 4) {
388
- return m[0] * m[3] - m[1] * m[2];
389
- }
390
- if (m.length === 9) {
391
- return m[0] * (m[4] * m[8] - m[5] * m[7])
392
- - m[3] * (m[1] * m[8] - m[2] * m[7])
393
- + m[6] * (m[1] * m[5] - m[2] * m[4]);
394
- }
395
- if (m.length === 16) {
396
- // 4x4 expansion via 2x2 minors of the bottom rows.
397
- const s0 = m[0] * m[5] - m[1] * m[4];
398
- const s1 = m[0] * m[6] - m[2] * m[4];
399
- const s2 = m[0] * m[7] - m[3] * m[4];
400
- const s3 = m[1] * m[6] - m[2] * m[5];
401
- const s4 = m[1] * m[7] - m[3] * m[5];
402
- const s5 = m[2] * m[7] - m[3] * m[6];
403
- const c5 = m[10] * m[15] - m[11] * m[14];
404
- const c4 = m[ 9] * m[15] - m[11] * m[13];
405
- const c3 = m[ 9] * m[14] - m[10] * m[13];
406
- const c2 = m[ 8] * m[15] - m[11] * m[12];
407
- const c1 = m[ 8] * m[14] - m[10] * m[12];
408
- const c0 = m[ 8] * m[13] - m[ 9] * m[12];
409
- return s0 * c5 - s1 * c4 + s2 * c3 + s3 * c2 - s4 * c1 + s5 * c0;
410
- }
411
- throw new Error(`determinant: unsupported matrix length ${m.length}`);
412
- },
413
-
414
- // ---------------------------------------------------------------------
415
- // Arithmetic helpers (work on scalars and vectors element-wise)
416
- // ---------------------------------------------------------------------
417
-
418
- add: (a, b) => binop(a, b, (x, y) => x + y),
419
- sub: (a, b) => binop(a, b, (x, y) => x - y),
420
- mul: (a, b) => binop(a, b, (x, y) => x * y),
421
- div: (a, b) => binop(a, b, (x, y) => x / y),
422
- rem: (a, b) => binop(a, b, (x, y) => x % y),
423
-
424
- // Integer division truncates toward zero, matching WGSL `/` semantics on integer types.
425
- // The compiler routes `/` through `idiv` when both operands' tracked types are integer.
426
- idiv: (a, b) => binop(a, b, (x, y) => Math.trunc(x / y)),
427
-
428
- // Element-wise bitwise / shift helpers. JS's raw `&` / `|` / `^` / `<<` / `>>` work on
429
- // scalars but not typed arrays (they coerce the array to a number → NaN), so the compiler
430
- // routes vec/mat operands through these. Right-shift uses `>>>` (unsigned) to match WGSL
431
- // u32 semantics; signed sources should bitcast first.
432
- bit_and: (a, b) => binop(a, b, (x, y) => (x & y) >>> 0),
433
- bit_or: (a, b) => binop(a, b, (x, y) => (x | y) >>> 0),
434
- bit_xor: (a, b) => binop(a, b, (x, y) => (x ^ y) >>> 0),
435
- shl: (a, b) => binop(a, b, (x, y) => (x << y) >>> 0),
436
- shr: (a, b) => binop(a, b, (x, y) => x >>> y),
437
-
438
- neg: (a) => unop(a, x => -x),
439
- not: (a) => unop(a, x => (~x) >>> 0),
440
-
441
- // Bit-field extract / insert (WGSL §17.9). Component-wise over vectors; `offset` and
442
- // `count` are scalar u32 even when `e` / `newbits` are vectors, matching the WGSL
443
- // signatures. Like `shr`, these implement the UNSIGNED reading extracted bits are
444
- // zero-extended, not sign-extended so an i32 source needing sign extension should
445
- // bitcast first. Per the spec, `o = min(offset, 32)` and `c = min(count, 32 - o)`.
446
-
447
- /** `extractBits(e, offset, count)` read `count` bits at `offset`, zero-extended. */
448
- extractBits(e, offset, count) {
449
- const o = Math.min(offset >>> 0, 32);
450
- const c = Math.min(count >>> 0, 32 - o);
451
- return unop(e, (x) => {
452
- if (c === 0) {
453
- return 0;
454
- }
455
- const mask = c >= 32 ? 0xFFFFFFFF : ((1 << c) - 1);
456
- return ((x >>> o) & mask) >>> 0;
457
- });
458
- },
459
-
460
- // Bit counting (WGSL §17.9). Component-wise over vectors. All of these read their operand as
461
- // 32 bits regardless of whether the WGSL type was u32 or i32 — which is what the spec says and
462
- // what makes `countLeadingZeros` the exact counterpart of JS's `Math.clz32`.
463
-
464
- /** `countLeadingZeros(e)` number of consecutive zero bits above the most significant set bit; 32 for 0. */
465
- countLeadingZeros: (e) => unop(e, x => Math.clz32(x)),
466
-
467
- /** `countTrailingZeros(e)` number of consecutive zero bits below the least significant set bit; 32 for 0. */
468
- countTrailingZeros: (e) => unop(e, x => (x === 0 ? 32 : 31 - Math.clz32(x & -x))),
469
-
470
- /** `countOneBits(e)` population count. */
471
- countOneBits: (e) => unop(e, x => {
472
- let bits = x >>> 0;
473
- let count = 0;
474
-
475
- while (bits !== 0) {
476
- bits &= bits - 1;
477
- count += 1;
478
- }
479
-
480
- return count;
481
- }),
482
-
483
- /** `firstLeadingBit(e)` index of the most significant set bit; `0xffffffff` for 0 (u32 reading). */
484
- firstLeadingBit: (e) => unop(e, x => (x === 0 ? 0xffffffff : 31 - Math.clz32(x))),
485
-
486
- /** `firstTrailingBit(e)` index of the least significant set bit; `0xffffffff` for 0. */
487
- firstTrailingBit: (e) => unop(e, x => (x === 0 ? 0xffffffff : 31 - Math.clz32(x & -x))),
488
-
489
- /** `reverseBits(e)` — reverse the order of all 32 bits. */
490
- reverseBits: (e) => unop(e, x => {
491
- let source = x >>> 0;
492
- let reversed = 0;
493
-
494
- for (let i = 0; i < 32; i++) {
495
- reversed = ((reversed << 1) | (source & 1)) >>> 0;
496
- source >>>= 1;
497
- }
498
-
499
- return reversed;
500
- }),
501
-
502
- /** `insertBits(e, newbits, offset, count)` replace bits `[offset, offset+count)` of `e` with the low `count` bits of `newbits`. */
503
- insertBits(e, newbits, offset, count) {
504
- const o = Math.min(offset >>> 0, 32);
505
- const c = Math.min(count >>> 0, 32 - o);
506
- return binop(e, newbits, (x, ins) => {
507
- if (c === 0) {
508
- return x >>> 0;
509
- }
510
- const base_mask = c >= 32 ? 0xFFFFFFFF : ((1 << c) - 1);
511
- const field_mask = o >= 32 ? 0 : ((base_mask << o) >>> 0);
512
- return (((ins << o) & field_mask) | (x & ((~field_mask) >>> 0))) >>> 0;
513
- });
514
- },
515
-
516
- // ---------------------------------------------------------------------
517
- // WGSL math builtins
518
- // ---------------------------------------------------------------------
519
-
520
- abs: (a) => unop(a, Math.abs),
521
- sign: (a) => unop(a, Math.sign),
522
- floor: (a) => unop(a, Math.floor),
523
- ceil: (a) => unop(a, Math.ceil),
524
- round: (a) => unop(a, Math.round),
525
- trunc: (a) => unop(a, Math.trunc),
526
- fract: (a) => unop(a, x => x - Math.floor(x)),
527
- sqrt: (a) => unop(a, Math.sqrt),
528
- inverseSqrt: (a) => unop(a, x => 1 / Math.sqrt(x)),
529
- exp: (a) => unop(a, Math.exp),
530
- exp2: (a) => unop(a, x => Math.pow(2, x)),
531
- log: (a) => unop(a, Math.log),
532
- log2: (a) => unop(a, Math.log2),
533
- sin: (a) => unop(a, Math.sin),
534
- cos: (a) => unop(a, Math.cos),
535
- tan: (a) => unop(a, Math.tan),
536
- asin: (a) => unop(a, Math.asin),
537
- acos: (a) => unop(a, Math.acos),
538
- atan: (a) => unop(a, Math.atan),
539
-
540
- pow: (a, b) => binop(a, b, Math.pow),
541
- atan2: (a, b) => binop(a, b, Math.atan2),
542
- min: (a, b) => binop(a, b, Math.min),
543
- max: (a, b) => binop(a, b, Math.max),
544
- step: (edge, x) => binop(edge, x, (e, v) => v < e ? 0 : 1),
545
-
546
- clamp: (x, lo, hi) => ternop(x, lo, hi, (v, l, h) => Math.max(l, Math.min(h, v))),
547
- // Saturate is `clamp(x, 0, 1)`. WGSL builtin.
548
- saturate: (a) => unop(a, x => Math.max(0, Math.min(1, x))),
549
- mix: (a, b, t) => ternop(a, b, t, (x, y, k) => x * (1 - k) + y * k),
550
- // Fused multiply-add: `a * b + c`. JS lacks a true FMA; this is mathematically equivalent
551
- // but loses the single-rounding precision benefit of hardware FMA — fine for testing.
552
- fma: (a, b, c) => ternop(a, b, c, (x, y, z) => x * y + z),
553
- smoothstep: (edge0, edge1, x) => ternop(edge0, edge1, x, (e0, e1, v) => {
554
- const t = Math.max(0, Math.min(1, (v - e0) / (e1 - e0)));
555
- return t * t * (3 - 2 * t);
556
- }),
557
-
558
- // ---------------------------------------------------------------------
559
- // Vector operations
560
- // ---------------------------------------------------------------------
561
-
562
- dot(a, b) {
563
- assert.equal(a.length, b.length, 'dot vector length mismatch');
564
- let s = 0;
565
- for (let i = 0; i < a.length; i++) s += a[i] * b[i];
566
- return s;
567
- },
568
-
569
- length(v) {
570
- if (!is_vec(v)) return Math.abs(v);
571
- let s = 0;
572
- for (let i = 0; i < v.length; i++) s += v[i] * v[i];
573
- return Math.sqrt(s);
574
- },
575
-
576
- distance(a, b) {
577
- return WGSLLanguageCore.length(WGSLLanguageCore.sub(a, b));
578
- },
579
-
580
- normalize(v) {
581
- const len = WGSLLanguageCore.length(v);
582
- if (len === 0) {
583
- return new v.constructor(v.length);
584
- }
585
- const out = new v.constructor(v.length);
586
- for (let i = 0; i < v.length; i++) out[i] = v[i] / len;
587
- return out;
588
- },
589
-
590
- cross(a, b) {
591
- assert.equal(a.length, 3, 'cross requires vec3');
592
- assert.equal(b.length, 3, 'cross requires vec3');
593
- return new a.constructor([
594
- a[1] * b[2] - a[2] * b[1],
595
- a[2] * b[0] - a[0] * b[2],
596
- a[0] * b[1] - a[1] * b[0],
597
- ]);
598
- },
599
-
600
- reflect(i, n) {
601
- const d = WGSLLanguageCore.dot(i, n);
602
- const out = new i.constructor(i.length);
603
- for (let k = 0; k < i.length; k++) out[k] = i[k] - 2 * d * n[k];
604
- return out;
605
- },
606
-
607
- // ---------------------------------------------------------------------
608
- // Matrix × {matrix, vector} and vector × matrix products.
609
- //
610
- // WGSL matrices are column-major: `matCxR` has C columns of R rows each, stored as a flat
611
- // array of C*R elements where column `c` occupies indices `[c*R, c*R + R)`.
612
- //
613
- // Naming convention: `m{cols}x{rows}_multiply_{...}`. Only the most common shapes are
614
- // implemented. For unsupported shapes the compiler will emit a call to a missing helper —
615
- // calling it raises `TypeError: wgsl.<name> is not a function`, which clearly identifies
616
- // the shape needed; add a new entry below to support it.
617
- // ---------------------------------------------------------------------
618
-
619
- // mat × column-vector
620
-
621
- m2x2f_multiply_v2f(m, v) {
622
- const out = new Float32Array(2);
623
- out[0] = m[0]*v[0] + m[2]*v[1];
624
- out[1] = m[1]*v[0] + m[3]*v[1];
625
- return out;
626
- },
627
-
628
- m3x3f_multiply_v3f(m, v) {
629
- const out = new Float32Array(3);
630
- out[0] = m[0]*v[0] + m[3]*v[1] + m[6]*v[2];
631
- out[1] = m[1]*v[0] + m[4]*v[1] + m[7]*v[2];
632
- out[2] = m[2]*v[0] + m[5]*v[1] + m[8]*v[2];
633
- return out;
634
- },
635
-
636
- m4x4f_multiply_v4f(m, v) {
637
- const out = new Float32Array(4);
638
- out[0] = m[0]*v[0] + m[4]*v[1] + m[8] *v[2] + m[12]*v[3];
639
- out[1] = m[1]*v[0] + m[5]*v[1] + m[9] *v[2] + m[13]*v[3];
640
- out[2] = m[2]*v[0] + m[6]*v[1] + m[10]*v[2] + m[14]*v[3];
641
- out[3] = m[3]*v[0] + m[7]*v[1] + m[11]*v[2] + m[15]*v[3];
642
- return out;
643
- },
644
-
645
- // mat × mat (both square)
646
-
647
- m2x2f_multiply_m2x2f(a, b) {
648
- const out = new Float32Array(4);
649
- for (let j = 0; j < 2; j++) {
650
- const b0 = b[j*2], b1 = b[j*2 + 1];
651
- out[j*2] = a[0]*b0 + a[2]*b1;
652
- out[j*2 + 1] = a[1]*b0 + a[3]*b1;
653
- }
654
- return out;
655
- },
656
-
657
- m3x3f_multiply_m3x3f(a, b) {
658
- const out = new Float32Array(9);
659
- for (let j = 0; j < 3; j++) {
660
- const b0 = b[j*3], b1 = b[j*3 + 1], b2 = b[j*3 + 2];
661
- out[j*3] = a[0]*b0 + a[3]*b1 + a[6]*b2;
662
- out[j*3 + 1] = a[1]*b0 + a[4]*b1 + a[7]*b2;
663
- out[j*3 + 2] = a[2]*b0 + a[5]*b1 + a[8]*b2;
664
- }
665
- return out;
666
- },
667
-
668
- m4x4f_multiply_m4x4f(a, b) {
669
- const out = new Float32Array(16);
670
- for (let j = 0; j < 4; j++) {
671
- const b0 = b[j*4], b1 = b[j*4 + 1], b2 = b[j*4 + 2], b3 = b[j*4 + 3];
672
- out[j*4] = a[0]*b0 + a[4]*b1 + a[8] *b2 + a[12]*b3;
673
- out[j*4 + 1] = a[1]*b0 + a[5]*b1 + a[9] *b2 + a[13]*b3;
674
- out[j*4 + 2] = a[2]*b0 + a[6]*b1 + a[10]*b2 + a[14]*b3;
675
- out[j*4 + 3] = a[3]*b0 + a[7]*b1 + a[11]*b2 + a[15]*b3;
676
- }
677
- return out;
678
- },
679
-
680
- // row-vector × mat (square)
681
-
682
- v2f_multiply_m2x2f(v, m) {
683
- const out = new Float32Array(2);
684
- out[0] = v[0]*m[0] + v[1]*m[1];
685
- out[1] = v[0]*m[2] + v[1]*m[3];
686
- return out;
687
- },
688
-
689
- v3f_multiply_m3x3f(v, m) {
690
- const out = new Float32Array(3);
691
- out[0] = v[0]*m[0] + v[1]*m[1] + v[2]*m[2];
692
- out[1] = v[0]*m[3] + v[1]*m[4] + v[2]*m[5];
693
- out[2] = v[0]*m[6] + v[1]*m[7] + v[2]*m[8];
694
- return out;
695
- },
696
-
697
- v4f_multiply_m4x4f(v, m) {
698
- const out = new Float32Array(4);
699
- out[0] = v[0]*m[0] + v[1]*m[1] + v[2]*m[2] + v[3]*m[3];
700
- out[1] = v[0]*m[4] + v[1]*m[5] + v[2]*m[6] + v[3]*m[7];
701
- out[2] = v[0]*m[8] + v[1]*m[9] + v[2]*m[10] + v[3]*m[11];
702
- out[3] = v[0]*m[12] + v[1]*m[13] + v[2]*m[14] + v[3]*m[15];
703
- return out;
704
- },
705
-
706
- // ---------------------------------------------------------------------
707
- // Logical / bit helpers
708
- // ---------------------------------------------------------------------
709
-
710
- select(a, b, cond) {
711
- // WGSL: select(false_value, true_value, cond)
712
- // - cond as scalar bool: returns `b` if truthy, `a` otherwise (whole-vector pick).
713
- // - cond as a vec<bool>-like array: element-wise pick from `a`/`b`. Both must be
714
- // vectors of matching length.
715
- if (cond != null && typeof cond === 'object' && typeof cond.length === 'number') {
716
- const ctor = (a && typeof a.length === 'number') ? a.constructor
717
- : (b && typeof b.length === 'number') ? b.constructor
718
- : Array;
719
- const out = new ctor(cond.length);
720
- for (let i = 0; i < cond.length; i++) out[i] = cond[i] ? b[i] : a[i];
721
- return out;
722
- }
723
- return cond ? b : a;
724
- },
725
-
726
- // Element-wise comparison helpers. Scalar in → boolean out; vector in → Array<boolean> of
727
- // matching length (modelling WGSL's vec<bool>). Used by the compiler when at least one
728
- // operand of `==` / `!=` / `<` / `>` / `<=` / `>=` is a vector.
729
- eq: (a, b) => binop_cmp(a, b, (x, y) => x === y),
730
- ne: (a, b) => binop_cmp(a, b, (x, y) => x !== y),
731
- lt: (a, b) => binop_cmp(a, b, (x, y) => x < y),
732
- gt: (a, b) => binop_cmp(a, b, (x, y) => x > y),
733
- le: (a, b) => binop_cmp(a, b, (x, y) => x <= y),
734
- ge: (a, b) => binop_cmp(a, b, (x, y) => x >= y),
735
-
736
- all(v) {
737
- if (!is_vec(v)) return !!v;
738
- for (let i = 0; i < v.length; i++) if (!v[i]) return false;
739
- return true;
740
- },
741
-
742
- any(v) {
743
- if (!is_vec(v)) return !!v;
744
- for (let i = 0; i < v.length; i++) if (v[i]) return true;
745
- return false;
746
- },
747
-
748
- // ---------------------------------------------------------------------
749
- // Texture intrinsics - backed by `CPUBitmapData` (1D / 2D / 3D inferred
750
- // from the texture's `size`). Sampling routes through
751
- // `cpu_texture_sample`, which honours the bound `SamplerDescriptor`'s
752
- // address modes and filter. Mipmaps are not modelled (LOD 0 only) -
753
- // see cpu_texture_sample header for details.
754
- // ---------------------------------------------------------------------
755
-
756
- /**
757
- * `textureLoad(t, coords, level) -> vec4<T>`
758
- *
759
- * Integer-coord fetch. `coords` is i32-vector matching the texture's
760
- * dimensionality (1, 2, or 3 entries). Out-of-bounds reads return zero
761
- * across all channels (with alpha defaulting to 1.0 for textures with
762
- * fewer than 4 channels - matches WGSL semantics).
763
- *
764
- * @param {import("./CPUBitmapData.js").CPUBitmapData} t
765
- * @param {Int32Array | Uint32Array | number[] | number} coords
766
- * @param {number} [_level] - ignored, mipmaps are not modelled
767
- * @returns {Float32Array}
768
- */
769
- textureLoad(t, coords, _level_or_index = 0, _array_level = undefined) {
770
- assert.defined(t, 'texture');
771
- const out = new Float32Array(4);
772
- const dim = t.dimensionality;
773
- // textureLoad on texture_1d takes a scalar i32 in WGSL (not vec1).
774
- const x = (dim >= 1 ? (typeof coords === 'number' ? coords : coords[0]) : 0) | 0;
775
- const y = (dim >= 2 ? coords[1] : 0) | 0;
776
- /*
777
- Four arguments is the texture_2d_array form (t, coords, array_index, level) — where the
778
- third argument is a slice rather than a mip level. A 2d-array is backed by a depth>1
779
- CPUBitmapData, exactly like a 3D texture; only the call shape tells them apart.
780
- */
781
- const z = (_array_level !== undefined ? _level_or_index : (dim >= 3 ? coords[2] : 0)) | 0;
782
- const item_size = t.item_size;
783
- if (item_size < 4) out[3] = 1.0;
784
- if (x < 0 || x >= t.size[0] || y < 0 || y >= t.size[1] || z < 0 || z >= t.size[2]) {
785
- return out;
786
- }
787
- const base = t.pixel_offset(x, y, z);
788
- const n = Math.min(item_size, 4);
789
- for (let i = 0; i < n; i++) out[i] = t.data[base + i];
790
- return out;
791
- },
792
-
793
- /**
794
- * `textureSampleLevel(t, s, coords, level) -> vec4<T>`
795
- *
796
- * Filtered sample at normalised UV coords. Honours the sampler's
797
- * address modes and `magFilter`. `coords` length matches the texture
798
- * dimensionality. The `level` arg is ignored (no mipmaps).
799
- *
800
- * @param {import("./CPUBitmapData.js").CPUBitmapData} t
801
- * @param {import("../../descriptor/texture/SamplerDescriptor.js").SamplerDescriptor | null} sampler
802
- * @param {Float32Array | number[] | number} coords
803
- * @param {number} [_level]
804
- * @returns {Float32Array}
805
- */
806
- textureSampleLevel(t, sampler, coords, _level_or_index = 0, _array_level = undefined) {
807
- assert.defined(t, 'texture');
808
- const dim = t.dimensionality;
809
- // Convert UV (normalised) to texel-space coords expected by
810
- // cpu_texture_sample. coords for a 1D texture is a scalar in WGSL.
811
- const u = dim >= 1 ? (typeof coords === 'number' ? coords : coords[0]) : 0;
812
- const v = dim >= 2 ? coords[1] : 0;
813
- const tx = u * t.size[0];
814
- const ty = v * t.size[1];
815
- /**
816
- * @type {number[]}
817
- */
818
- let texel_coord;
819
- if (_array_level !== undefined) {
820
- /*
821
- Five arguments is the texture_2d_array form — (t, s, uv, array_index, level) — where the
822
- fourth argument selects a slice. A slice's texel-space centre sits at index + 0.5, which
823
- lands the linear filter's z weight on exactly that slice: filtering happens in xy and
824
- never crosses into a neighbour.
825
- */
826
- texel_coord = [tx, ty, (_level_or_index | 0) + 0.5];
827
- } else {
828
- const w = dim >= 3 ? coords[2] : 0;
829
- const tz = w * t.size[2];
830
- texel_coord = dim === 1 ? [tx] : (dim === 2 ? [tx, ty] : [tx, ty, tz]);
831
- }
832
- const result = new Float32Array(4);
833
- cpu_texture_sample(result, sampler ?? FALLBACK_LINEAR_CLAMP_SAMPLER, t, texel_coord);
834
- return result;
835
- },
836
-
837
- /**
838
- * `textureSample(t, s, coords) -> vec4<T>` and the texture_2d_array form
839
- * `textureSample(t, s, coords, array_index)`.
840
- *
841
- * Implicit-LOD sample. Without mipmaps, this is just
842
- * `textureSampleLevel` at level 0.
843
- *
844
- * @param {import("./CPUBitmapData.js").CPUBitmapData} t
845
- * @param {import("../../descriptor/texture/SamplerDescriptor.js").SamplerDescriptor | null} sampler
846
- * @param {Float32Array | number[] | number} coords
847
- * @param {number} [array_index]
848
- * @returns {Float32Array}
849
- */
850
- textureSample(t, sampler, coords, array_index = undefined) {
851
- if (array_index !== undefined) {
852
- return WGSLLanguageCore.textureSampleLevel(t, sampler, coords, array_index, 0);
853
- }
854
-
855
- return WGSLLanguageCore.textureSampleLevel(t, sampler, coords, 0);
856
- },
857
-
858
- /**
859
- * `textureSampleGrad(t, s, coords, ddx, ddy) -> vec4<T>` and the texture_2d_array form
860
- * `textureSampleGrad(t, s, coords, array_index, ddx, ddy)`.
861
- *
862
- * Explicit-gradient sample. Mipmaps are not modelled (see the header), so the gradients only
863
- * disambiguate which call shape this is: six arguments carry an array index, five do not.
864
- *
865
- * @param {import("./CPUBitmapData.js").CPUBitmapData} t
866
- * @param {import("../../descriptor/texture/SamplerDescriptor.js").SamplerDescriptor | null} sampler
867
- * @param {Float32Array | number[] | number} coords
868
- * @param {Float32Array | number[] | number} p3 gradient, or the array index
869
- * @param {Float32Array | number[]} p4 gradient
870
- * @param {Float32Array | number[]} [p5] gradient, in the array form
871
- * @returns {Float32Array}
872
- */
873
- textureSampleGrad(t, sampler, coords, p3, p4, p5 = undefined) {
874
- if (p5 !== undefined) {
875
- return WGSLLanguageCore.textureSampleLevel(t, sampler, coords, p3, 0);
876
- }
877
-
878
- return WGSLLanguageCore.textureSampleLevel(t, sampler, coords, 0);
879
- },
880
-
881
- /**
882
- * `textureStore(t, coords, value) -> ()`
883
- *
884
- * Integer-coord write into a storage texture. Out-of-bounds writes are
885
- * silently dropped (WGSL semantics). `coords` length matches the
886
- * texture dimensionality.
887
- *
888
- * @param {import("./CPUBitmapData.js").CPUBitmapData} t
889
- * @param {Int32Array | Uint32Array | number[] | number} coords
890
- * @param {Float32Array | Int32Array | Uint32Array | number[]} value
891
- */
892
- textureStore(t, coords, value) {
893
- assert.defined(t, 'texture');
894
- const dim = t.dimensionality;
895
- const x = (dim >= 1 ? (typeof coords === 'number' ? coords : coords[0]) : 0) | 0;
896
- const y = (dim >= 2 ? coords[1] : 0) | 0;
897
- const z = (dim >= 3 ? coords[2] : 0) | 0;
898
- t.write(x, y, z, value);
899
- },
900
-
901
- /**
902
- * `textureDimensions(t, level) -> u32 | vec2<u32> | vec3<u32>`
903
- *
904
- * Returns the texture's size as a vector matching its dimensionality.
905
- * 1D textures return a scalar `u32` in WGSL; we still hand back a
906
- * length-1 Uint32Array so the calling shader can swizzle/index it
907
- * uniformly with the multi-axis case.
908
- *
909
- * @param {import("./CPUBitmapData.js").CPUBitmapData} t
910
- * @param {number} [_level]
911
- * @returns {Uint32Array}
912
- */
913
- textureDimensions(t, _level = 0) {
914
- assert.defined(t, 'texture');
915
- const dim = t.dimensionality;
916
- if (dim === 1) return new Uint32Array([t.size[0]]);
917
- if (dim === 2) return new Uint32Array([t.size[0], t.size[1]]);
918
- return new Uint32Array([t.size[0], t.size[1], t.size[2]]);
919
- },
920
-
921
- // ---------------------------------------------------------------------
922
- // Atomic ops on `atomic<T>` (modeled as { value: number } objects)
923
- // ---------------------------------------------------------------------
924
-
925
- atomicLoad(p) { return p.value; },
926
- atomicStore(p, v) { p.value = v; },
927
- atomicAdd(p, v) { const old = p.value; p.value = (p.value + v) | 0; return old; },
928
- atomicSub(p, v) { const old = p.value; p.value = (p.value - v) | 0; return old; },
929
- atomicMin(p, v) { const old = p.value; p.value = Math.min(p.value, v); return old; },
930
- atomicMax(p, v) { const old = p.value; p.value = Math.max(p.value, v); return old; },
931
- atomicAnd(p, v) { const old = p.value; p.value = p.value & v; return old; },
932
- atomicOr(p, v) { const old = p.value; p.value = p.value | v; return old; },
933
- atomicXor(p, v) { const old = p.value; p.value = p.value ^ v; return old; },
934
- atomicExchange(p, v) { const old = p.value; p.value = v; return old; },
935
- atomicCompareExchangeWeak(p, expected, v) {
936
- const old = p.value;
937
- if (old === expected) {
938
- p.value = v;
939
- return { old_value: old, exchanged: true };
940
- }
941
- return { old_value: old, exchanged: false };
942
- },
943
-
944
- // ---------------------------------------------------------------------
945
- // Barriers no-ops when running pointwise (one thread).
946
- // ---------------------------------------------------------------------
947
-
948
- workgroupBarrier() { /* no-op */ },
949
- storageBarrier() { /* no-op */ },
950
- textureBarrier() { /* no-op */ },
951
-
952
- // A barrier plus a load of the pointed-at workgroup variable, which the compiler has already
953
- // reduced to the variable itself — `&x` and `*x` both compile to `x`, since pointers are
954
- // modelled as the values they name. The barrier half is the no-op above.
955
- workgroupUniformLoad(value) { return value; },
956
-
957
- // ---------------------------------------------------------------------
958
- // Fragment-stage derivatives unsupported because we run a single pixel.
959
- // ---------------------------------------------------------------------
960
- //
961
- // dpdx/dpdy/fwidth and their fine/coarse variants are implicit-derivative builtins that the
962
- // GPU computes from a 2x2 quad of neighbouring fragment threads. The emulator dispatches one
963
- // pixel at a time, so there's no neighbouring data to take a finite difference against —
964
- // any sensible value would be a fabrication. We throw to surface this clearly in tests.
965
-
966
- dpdx() { throw new Error('dpdx is not available in the fragment-shader emulator: derivatives require a 2x2 quad, but the emulator runs a single pixel.'); },
967
- dpdy() { throw new Error('dpdy is not available in the fragment-shader emulator: derivatives require a 2x2 quad, but the emulator runs a single pixel.'); },
968
- fwidth() { throw new Error('fwidth is not available in the fragment-shader emulator: derivatives require a 2x2 quad, but the emulator runs a single pixel.'); },
969
- dpdxFine() { throw new Error('dpdxFine is not available in the fragment-shader emulator: derivatives require a 2x2 quad, but the emulator runs a single pixel.'); },
970
- dpdyFine() { throw new Error('dpdyFine is not available in the fragment-shader emulator: derivatives require a 2x2 quad, but the emulator runs a single pixel.'); },
971
- fwidthFine() { throw new Error('fwidthFine is not available in the fragment-shader emulator: derivatives require a 2x2 quad, but the emulator runs a single pixel.'); },
972
- dpdxCoarse() { throw new Error('dpdxCoarse is not available in the fragment-shader emulator: derivatives require a 2x2 quad, but the emulator runs a single pixel.'); },
973
- dpdyCoarse() { throw new Error('dpdyCoarse is not available in the fragment-shader emulator: derivatives require a 2x2 quad, but the emulator runs a single pixel.'); },
974
- fwidthCoarse() { throw new Error('fwidthCoarse is not available in the fragment-shader emulator: derivatives require a 2x2 quad, but the emulator runs a single pixel.'); },
975
-
976
- // ---------------------------------------------------------------------
977
- // Pack / unpack — fixed-point and float bit-level packers from the WGSL spec.
978
- // ---------------------------------------------------------------------
979
-
980
- /** vec2<f32>(low_u16, high_u16) / 65535.0 — components in [0, 1]. */
981
- unpack2x16unorm(e) {
982
- const u = e >>> 0;
983
- return new Float32Array([(u & 0xFFFF) / 65535.0, ((u >>> 16) & 0xFFFF) / 65535.0]);
984
- },
985
-
986
- /** vec2<f32>(low_i16, high_i16) / 32767.0 — components in [-1, 1]. */
987
- unpack2x16snorm(e) {
988
- const u = e >>> 0;
989
- const sign16 = (h) => (h & 0x8000) !== 0 ? h - 0x10000 : h;
990
- const x = sign16(u & 0xFFFF) / 32767.0;
991
- const y = sign16((u >>> 16) & 0xFFFF) / 32767.0;
992
- return new Float32Array([Math.max(x, -1), Math.max(y, -1)]);
993
- },
994
-
995
- /** vec4<f32>(byte0, byte1, byte2, byte3) / 255.0 — components in [0, 1]. */
996
- unpack4x8unorm(e) {
997
- const u = e >>> 0;
998
- return new Float32Array([
999
- (u & 0xFF) / 255.0,
1000
- ((u >>> 8) & 0xFF) / 255.0,
1001
- ((u >>> 16) & 0xFF) / 255.0,
1002
- ((u >>> 24) & 0xFF) / 255.0,
1003
- ]);
1004
- },
1005
-
1006
- /** vec4<f32>(i8 byte0..3) / 127.0 — components in [-1, 1]. */
1007
- unpack4x8snorm(e) {
1008
- const u = e >>> 0;
1009
- const sign8 = (b) => (b & 0x80) !== 0 ? b - 0x100 : b;
1010
- const out = new Float32Array(4);
1011
- for (let i = 0; i < 4; i++) {
1012
- out[i] = Math.max(sign8((u >>> (i * 8)) & 0xFF) / 127.0, -1);
1013
- }
1014
- return out;
1015
- },
1016
-
1017
- /** Inverse of unpack2x16unorm clamp to [0, 1] and round to 16-bit integer. */
1018
- pack2x16unorm(v) {
1019
- const x = Math.round(Math.min(Math.max(v[0], 0), 1) * 65535.0);
1020
- const y = Math.round(Math.min(Math.max(v[1], 0), 1) * 65535.0);
1021
- return ((y << 16) | x) >>> 0;
1022
- },
1023
-
1024
- /** Inverse of unpack2x16snorm clamp to [-1, 1] and round to 16-bit signed integer. */
1025
- pack2x16snorm(v) {
1026
- const enc = (f) => Math.round(Math.min(Math.max(f, -1), 1) * 32767.0) & 0xFFFF;
1027
- return ((enc(v[1]) << 16) | enc(v[0])) >>> 0;
1028
- },
1029
-
1030
- /** Inverse of unpack4x8unorm — clamp to [0, 1] and round to 8-bit integer per channel. */
1031
- pack4x8unorm(v) {
1032
- let r = 0;
1033
- for (let i = 0; i < 4; i++) {
1034
- const b = Math.round(Math.min(Math.max(v[i], 0), 1) * 255.0) & 0xFF;
1035
- r |= b << (i * 8);
1036
- }
1037
- return r >>> 0;
1038
- },
1039
-
1040
- /** Inverse of unpack4x8snorm — clamp to [-1, 1] and round to 8-bit signed integer per channel. */
1041
- pack4x8snorm(v) {
1042
- let r = 0;
1043
- for (let i = 0; i < 4; i++) {
1044
- const b = Math.round(Math.min(Math.max(v[i], -1), 1) * 127.0) & 0xFF;
1045
- r |= b << (i * 8);
1046
- }
1047
- return r >>> 0;
1048
- },
1049
- };
1050
-
1051
- /**
1052
- * Bit-cast (WGSL `bitcast`). Reinterprets a value's bit pattern as a different type.
1053
- *
1054
- * JS has no separate u32/i32/f32 scalar types — they're all `number` — so the source type
1055
- * cannot be detected from the value at runtime. The compiler passes it explicitly when known;
1056
- * otherwise we fall back to a heuristic: integer-targeted casts assume f32 source, float-targeted
1057
- * casts assume u32 source (the common "interpret bit pattern as float" idiom).
1058
- *
1059
- * For typed-array (vector) inputs the source type is auto-detected from the array constructor.
1060
- *
1061
- * @param {*} v
1062
- * @param {string} target — destination type name, e.g. `'f32'` / `'u32'` / `'i32'`
1063
- * @param {string} [source] — source type name; required for unambiguous scalar casts
1064
- * @returns {number | TypedArray}
1065
- */
1066
- WGSLLanguageCore.bitcast = function (v, target, source) {
1067
- // Vector targets (`vec3<u32>` etc.) reduce to their scalar type — the runtime stores vectors
1068
- // as flat typed arrays, so the per-element bitcast is the same as for a scalar of that type.
1069
- const vec_match = typeof target === 'string' ? target.match(/^vec\d<([^>]+)>$/) : null;
1070
- if (vec_match !== null) {
1071
- target = vec_match[1];
1072
- }
1073
-
1074
- function convert_scalar(x, t_from, t_to) {
1075
- if (t_to === 'f32') {
1076
- if (t_from === 'u32') { BITCAST_U32[0] = x >>> 0; return BITCAST_F32[0]; }
1077
- if (t_from === 'i32') { BITCAST_I32[0] = x | 0; return BITCAST_F32[0]; }
1078
- } else if (t_to === 'u32') {
1079
- if (t_from === 'f32') { BITCAST_F32[0] = x; return BITCAST_U32[0]; }
1080
- if (t_from === 'i32') { return (x | 0) >>> 0; }
1081
- } else if (t_to === 'i32') {
1082
- if (t_from === 'f32') { BITCAST_F32[0] = x; return BITCAST_I32[0]; }
1083
- if (t_from === 'u32') { return (x >>> 0) | 0; }
1084
- }
1085
- return x;
1086
- }
1087
-
1088
- if (is_vec(v)) {
1089
- let from = source;
1090
- if (from === undefined) {
1091
- if (v instanceof Float32Array) from = 'f32';
1092
- else if (v instanceof Float16Array) from = 'f16';
1093
- else if (v instanceof Uint32Array) from = 'u32';
1094
- else if (v instanceof Int32Array) from = 'i32';
1095
- else from = 'f32';
1096
- }
1097
-
1098
- let outCtor;
1099
- if (target === 'f32') outCtor = Float32Array;
1100
- else if (target === 'f16') outCtor = Float16Array;
1101
- else if (target === 'u32') outCtor = Uint32Array;
1102
- else if (target === 'i32') outCtor = Int32Array;
1103
- else outCtor = v.constructor;
1104
-
1105
- const out = new outCtor(v.length);
1106
- for (let i = 0; i < v.length; i++) {
1107
- out[i] = convert_scalar(v[i], from, target);
1108
- }
1109
- return out;
1110
- }
1111
-
1112
- let from = source;
1113
- if (from === undefined) {
1114
- from = (target === 'f32' || target === 'f16') ? 'u32' : 'f32';
1115
- }
1116
- return convert_scalar(v, from, target);
1117
- };
1
+ import { assert } from "../../../core/assert.js";
2
+ import { cpu_texture_gather, evaluate_gpu_compare_function } from "./cpu_texture_gather.js";
3
+ import { cpu_texture_sample } from "./cpu_texture_sample.js";
4
+
5
+ /**
6
+ * Default sampler descriptor for `textureSample` / `textureSampleLevel`
7
+ * calls when the bound sampler is `null` (common in tests). Linear filter,
8
+ * clamp-to-edge on every axis - matches what most engine passes use.
9
+ */
10
+ const FALLBACK_LINEAR_CLAMP_SAMPLER = Object.freeze({
11
+ addressModeU: "clamp-to-edge",
12
+ addressModeV: "clamp-to-edge",
13
+ addressModeW: "clamp-to-edge",
14
+ magFilter: "linear",
15
+ minFilter: "linear",
16
+ mipmapFilter: "nearest",
17
+ });
18
+
19
+ /**
20
+ * Normalised UV -> texel space, for the gather intrinsics. {@link cpu_texture_gather} takes
21
+ * texel-space coordinates, the same convention {@link cpu_texture_sample} documents, so the
22
+ * shader-facing side does the conversion — `textureGather` has no 3D or 1D overload in WGSL,
23
+ * so only the two axes are ever needed.
24
+ *
25
+ * @param {Float32Array | number[]} coordsnormalised uv
26
+ * @param {import("./CPUBitmapData.js").CPUBitmapData} texture
27
+ * @returns {number[]}
28
+ */
29
+ function gather_texel_coord(coords, texture) {
30
+ return [coords[0] * texture.size[0], coords[1] * texture.size[1]];
31
+ }
32
+
33
+ /**
34
+ * Single-element scratch view used to round a number to IEEE 754 binary16 precision.
35
+ * @type {Float16Array}
36
+ */
37
+ const F16_SCRATCH = new Float16Array(1);
38
+
39
+ /**
40
+ * Shared scratch buffer for `bitcast` three views over the same 4 bytes so we can
41
+ * reinterpret one type as another in O(1) without per-call allocation.
42
+ */
43
+ const BITCAST_BUFFER = new ArrayBuffer(4);
44
+ const BITCAST_U32 = new Uint32Array(BITCAST_BUFFER);
45
+ const BITCAST_I32 = new Int32Array(BITCAST_BUFFER);
46
+ const BITCAST_F32 = new Float32Array(BITCAST_BUFFER);
47
+
48
+ /**
49
+ * Construct a WGSL vector. Mirrors WGSL `vecN<T>(...)` semantics:
50
+ *
51
+ * - **No args** zero-filled.
52
+ * - **One scalar** → broadcast to all `size` components (`vec3(1.0)` `[1, 1, 1]`).
53
+ * - **Exactly `size` scalars** → use as components.
54
+ * - **Any mix of scalars and shorter vectors** that flattens to exactly `size` numbers (e.g.
55
+ * `vec3(vec2_xy, z)` or `vec4(x, vec2_yz, w)`).
56
+ *
57
+ * @param {Float32ArrayConstructor | Float16ArrayConstructor | Int32ArrayConstructor | Uint32ArrayConstructor} TypedArrayCtor
58
+ * @param {number} size
59
+ * @param {*[]} args
60
+ * @returns {*}
61
+ */
62
+ function makeVec(TypedArrayCtor, size, args) {
63
+ if (args.length === 0) {
64
+ return new TypedArrayCtor(size);
65
+ }
66
+ // Single scalar → broadcast.
67
+ if (args.length === 1 && typeof args[0] === 'number') {
68
+ const v = args[0];
69
+ const out = new TypedArrayCtor(size);
70
+ for (let i = 0; i < size; i++) out[i] = v;
71
+ return out;
72
+ }
73
+ // Otherwise flatten.
74
+ const out = new TypedArrayCtor(size);
75
+ let i = 0;
76
+ for (const a of args) {
77
+ if (typeof a === 'number' || typeof a === 'boolean') {
78
+ out[i++] = +a;
79
+ } else if (a != null && typeof a.length === 'number') {
80
+ for (let j = 0; j < a.length; j++) out[i++] = a[j];
81
+ }
82
+ }
83
+ return out;
84
+ }
85
+
86
+ /**
87
+ * Build a matrix constructor returning a typed array of `total_elements` entries. The returned
88
+ * function accepts (a) no args → zero-filled, (b) `total_elements` scalars in column-major order,
89
+ * (c) one column-vec per matrix column, or (d) any mix that flattens to `total_elements` numbers.
90
+ *
91
+ * @param {Float32ArrayConstructor | Float16ArrayConstructor} TypedArrayCtor
92
+ * @param {number} total_elements
93
+ * @returns {function(...*): Float32Array | Float16Array}
94
+ */
95
+ function makeMatCtor(TypedArrayCtor, total_elements) {
96
+ return (...args) => {
97
+ if (args.length === 0) {
98
+ return new TypedArrayCtor(total_elements);
99
+ }
100
+ const flat = new TypedArrayCtor(total_elements);
101
+ let i = 0;
102
+ for (const a of args) {
103
+ if (typeof a === 'number') {
104
+ flat[i++] = a;
105
+ } else if (a != null && typeof a.length === 'number') {
106
+ for (let j = 0; j < a.length; j++) flat[i++] = a[j];
107
+ }
108
+ }
109
+ return flat;
110
+ };
111
+ }
112
+
113
+ /**
114
+ * @param {number} a
115
+ * @returns {number}
116
+ */
117
+ function as_i32(a) {
118
+ return (a | 0);
119
+ }
120
+
121
+ /**
122
+ * @param {number} a
123
+ * @returns {number}
124
+ */
125
+ function as_u32(a) {
126
+ return (a >>> 0);
127
+ }
128
+
129
+ /**
130
+ * Determine if a value is a vector — TypedArray (vec2f / vec3i / etc) or
131
+ * a plain JS Array (returned from comparison ops like `eq` / `ge`, since
132
+ * those produce arrays of bools). Either way, a vec is a length-N
133
+ * iterable of element values, distinguished from a scalar number or a
134
+ * struct.
135
+ * @param {*} v
136
+ * @returns {boolean}
137
+ */
138
+ function is_vec(v) {
139
+ if (v == null || typeof v !== 'object') return false;
140
+ if (typeof v.BYTES_PER_ELEMENT === 'number') return true; // TypedArray (vecNf / vecNi / ...)
141
+ if (Array.isArray(v)) return true; // plain JS Array — vec result from binop_cmp
142
+ return false;
143
+ }
144
+
145
+ /**
146
+ * Element-wise apply a binary op over a vector and scalar/vector counterpart.
147
+ * @param {*} a
148
+ * @param {*} b
149
+ * @param {function(number, number): number} op
150
+ * @returns {*}
151
+ */
152
+ function binop(a, b, op) {
153
+ const a_vec = is_vec(a);
154
+ const b_vec = is_vec(b);
155
+
156
+ if (!a_vec && !b_vec) {
157
+ return op(a, b);
158
+ }
159
+
160
+ if (a_vec && b_vec) {
161
+ assert.equal(a.length, b.length, 'vector length mismatch');
162
+ const out = new a.constructor(a.length);
163
+ for (let i = 0; i < a.length; i++) {
164
+ out[i] = op(a[i], b[i]);
165
+ }
166
+ return out;
167
+ }
168
+
169
+ if (a_vec) {
170
+ const out = new a.constructor(a.length);
171
+ for (let i = 0; i < a.length; i++) {
172
+ out[i] = op(a[i], b);
173
+ }
174
+ return out;
175
+ }
176
+
177
+ // b_vec
178
+ const out = new b.constructor(b.length);
179
+ for (let i = 0; i < b.length; i++) {
180
+ out[i] = op(a, b[i]);
181
+ }
182
+ return out;
183
+ }
184
+
185
+ /**
186
+ * Element-wise apply a comparison op. For scalar operands returns a single boolean; for
187
+ * vector operands returns a plain `Array<boolean>` matching the WGSL `vec<bool>` shape.
188
+ * @param {*} a
189
+ * @param {*} b
190
+ * @param {function(number, number): boolean} op
191
+ * @returns {boolean | boolean[]}
192
+ */
193
+ function binop_cmp(a, b, op) {
194
+ const a_vec = is_vec(a);
195
+ const b_vec = is_vec(b);
196
+
197
+ if (!a_vec && !b_vec) {
198
+ return op(a, b);
199
+ }
200
+ const length = a_vec ? a.length : b.length;
201
+ const out = new Array(length);
202
+ for (let i = 0; i < length; i++) {
203
+ const ai = a_vec ? a[i] : a;
204
+ const bi = b_vec ? b[i] : b;
205
+ out[i] = op(ai, bi);
206
+ }
207
+ return out;
208
+ }
209
+
210
+ /**
211
+ * Element-wise apply a unary op over a scalar/vector.
212
+ * @param {*} a
213
+ * @param {function(number): number} op
214
+ * @returns {*}
215
+ */
216
+ function unop(a, op) {
217
+ if (!is_vec(a)) {
218
+ return op(a);
219
+ }
220
+ const out = new a.constructor(a.length);
221
+ for (let i = 0; i < a.length; i++) {
222
+ out[i] = op(a[i]);
223
+ }
224
+ return out;
225
+ }
226
+
227
+ /**
228
+ * Element-wise apply a ternary op (e.g. mix, clamp).
229
+ * @param {*} a
230
+ * @param {*} b
231
+ * @param {*} c
232
+ * @param {function(number, number, number): number} op
233
+ * @returns {*}
234
+ */
235
+ function ternop(a, b, c, op) {
236
+ if (is_vec(a) || is_vec(b) || is_vec(c)) {
237
+ const length = (is_vec(a) ? a.length : (is_vec(b) ? b.length : c.length));
238
+ const ctor = (is_vec(a) ? a.constructor : (is_vec(b) ? b.constructor : c.constructor));
239
+ const out = new ctor(length);
240
+ for (let i = 0; i < length; i++) {
241
+ const ai = is_vec(a) ? a[i] : a;
242
+ const bi = is_vec(b) ? b[i] : b;
243
+ const ci = is_vec(c) ? c[i] : c;
244
+ out[i] = op(ai, bi, ci);
245
+ }
246
+ return out;
247
+ }
248
+ return op(a, b, c);
249
+ }
250
+
251
+ /**
252
+ * The default WGSL language core — the `wgsl` object passed into the generated JS.
253
+ *
254
+ * Provides:
255
+ * - vector / matrix constructors (`vec3f`, `vec2u`, etc.)
256
+ * - scalar constructors / casts (`f32`, `i32`, `u32`, `bool`)
257
+ * - arithmetic helpers (`add`, `sub`, `mul`, `div`, `mod`, `neg`)
258
+ * - WGSL builtins (`mix`, `clamp`, `min`, `max`, `abs`, `floor`, `ceil`, `pow`, ...)
259
+ * - texture intrinsics (`textureLoad`, `textureSampleLevel`)
260
+ * - control flow helpers (`workgroupBarrier`, `storageBarrier`)
261
+ *
262
+ * @type {object}
263
+ */
264
+ export const WGSLLanguageCore = {
265
+ // ---------------------------------------------------------------------
266
+ // Scalar casts
267
+ // ---------------------------------------------------------------------
268
+
269
+ /** Cast to i32. */
270
+ i32(v) {
271
+ if (typeof v === 'boolean') return v ? 1 : 0;
272
+ return as_i32(v);
273
+ },
274
+
275
+ /** Cast to u32. */
276
+ u32(v) {
277
+ if (typeof v === 'boolean') return v ? 1 : 0;
278
+ return as_u32(v);
279
+ },
280
+
281
+ /** Cast to f32 (no-op in JS, returns the number). */
282
+ f32(v) {
283
+ if (typeof v === 'boolean') return v ? 1 : 0;
284
+ return Math.fround(+v);
285
+ },
286
+
287
+ /** Cast to f16. Truncates to IEEE 754 binary16 precision via Float16Array. */
288
+ f16(v) {
289
+ if (typeof v === 'boolean') return v ? 1 : 0;
290
+ F16_SCRATCH[0] = +v;
291
+ return F16_SCRATCH[0];
292
+ },
293
+
294
+ /** Cast to bool. */
295
+ bool(v) {
296
+ return !!v;
297
+ },
298
+
299
+ // ---------------------------------------------------------------------
300
+ // Vector constructors
301
+ // ---------------------------------------------------------------------
302
+
303
+ vec2f: (...args) => makeVec(Float32Array, 2, args),
304
+ vec3f: (...args) => makeVec(Float32Array, 3, args),
305
+ vec4f: (...args) => makeVec(Float32Array, 4, args),
306
+
307
+ vec2h: (...args) => makeVec(Float16Array, 2, args),
308
+ vec3h: (...args) => makeVec(Float16Array, 3, args),
309
+ vec4h: (...args) => makeVec(Float16Array, 4, args),
310
+
311
+ vec2i: (...args) => makeVec(Int32Array, 2, args),
312
+ vec3i: (...args) => makeVec(Int32Array, 3, args),
313
+ vec4i: (...args) => makeVec(Int32Array, 4, args),
314
+
315
+ vec2u: (...args) => makeVec(Uint32Array, 2, args),
316
+ vec3u: (...args) => makeVec(Uint32Array, 3, args),
317
+ vec4u: (...args) => makeVec(Uint32Array, 4, args),
318
+
319
+ // ---------------------------------------------------------------------
320
+ // Matrix constructors — column-major flat arrays. matCxR has C columns of R rows each,
321
+ // for a total of C*R entries. Args may be scalars, column vectors, or a mix.
322
+ // ---------------------------------------------------------------------
323
+
324
+ mat2x2f: makeMatCtor(Float32Array, 4),
325
+ mat2x3f: makeMatCtor(Float32Array, 6),
326
+ mat2x4f: makeMatCtor(Float32Array, 8),
327
+ mat3x2f: makeMatCtor(Float32Array, 6),
328
+ mat3x3f: makeMatCtor(Float32Array, 9),
329
+ mat3x4f: makeMatCtor(Float32Array, 12),
330
+ mat4x2f: makeMatCtor(Float32Array, 8),
331
+ mat4x3f: makeMatCtor(Float32Array, 12),
332
+ mat4x4f: makeMatCtor(Float32Array, 16),
333
+
334
+ mat2x2h: makeMatCtor(Float16Array, 4),
335
+ mat2x3h: makeMatCtor(Float16Array, 6),
336
+ mat2x4h: makeMatCtor(Float16Array, 8),
337
+ mat3x2h: makeMatCtor(Float16Array, 6),
338
+ mat3x3h: makeMatCtor(Float16Array, 9),
339
+ mat3x4h: makeMatCtor(Float16Array, 12),
340
+ mat4x2h: makeMatCtor(Float16Array, 8),
341
+ mat4x3h: makeMatCtor(Float16Array, 12),
342
+ mat4x4h: makeMatCtor(Float16Array, 16),
343
+
344
+ /**
345
+ * Return column `i` of a column-major matrix as a vec view (no copy). Used by the compiler
346
+ * to translate WGSL `m[i]` which yields a vec onto our flat-typed-array storage.
347
+ * @param {Float32Array|Float16Array} m
348
+ * @param {number} i
349
+ * @param {number} rows
350
+ */
351
+ mat_col(m, i, rows) {
352
+ return m.subarray(i * rows, (i + 1) * rows);
353
+ },
354
+
355
+ /**
356
+ * Store a whole value through a pointer: WGSL's `*p = value`. Returns what the pointer variable
357
+ * should now hold, which the caller assigns back.
358
+ *
359
+ * Pointers are modelled as the pointee itself, so `*p = value` used to compile to a plain
360
+ * assignment to the parameter binding — which rebinds the callee's local and leaves the
361
+ * caller's object untouched. Every shader returning a result through an out-parameter then read
362
+ * back whatever it had initialised the variable to, silently and with nothing raised:
363
+ * `ray_triangle_compute_intersection_barycentric` reported hits at a distance of zero, which is
364
+ * a hit the traversal keeps and shades.
365
+ *
366
+ * Vectors, matrices and structs are objects, so the store goes through the reference and the
367
+ * caller sees it. **A pointer to a scalar still cannot**: a JavaScript number has no identity
368
+ * to write through, so the rebind is all there is, and a caller passing `&some_f32` sees its
369
+ * variable unchanged. Within the callee the value is right — `*p` reads back what `*p = …`
370
+ * wrote — so the in-out scalar accumulators in `sample_discrete_wrs_*` behave, and only a
371
+ * result handed *out* through a scalar pointer is lost. Widen such an out-parameter to a
372
+ * vector or a struct, or return it, if a test needs to see it.
373
+ *
374
+ * @param {Float32Array|Float16Array|Uint32Array|Int32Array|Array|object|number|boolean} target
375
+ * @param {ArrayLike<number>|object|number|boolean} value
376
+ * @returns {*} the value the pointer variable should be rebound to
377
+ */
378
+ store_ptr(target, value) {
379
+ if (ArrayBuffer.isView(target) || Array.isArray(target)) {
380
+ for (let i = 0; i < target.length; i++) {
381
+ target[i] = value[i];
382
+ }
383
+
384
+ return target;
385
+ }
386
+
387
+ if (target !== null && typeof target === 'object') {
388
+ Object.assign(target, value);
389
+
390
+ return target;
391
+ }
392
+
393
+ return value;
394
+ },
395
+
396
+ /**
397
+ * Determinant of a square matrix stored as a flat column-major typed array.
398
+ * Supports the WGSL square-matrix sizes: 2x2, 3x3, 4x4.
399
+ * @param {Float32Array|Float16Array} m
400
+ */
401
+ determinant(m) {
402
+ if (m.length === 4) {
403
+ return m[0] * m[3] - m[1] * m[2];
404
+ }
405
+ if (m.length === 9) {
406
+ return m[0] * (m[4] * m[8] - m[5] * m[7])
407
+ - m[3] * (m[1] * m[8] - m[2] * m[7])
408
+ + m[6] * (m[1] * m[5] - m[2] * m[4]);
409
+ }
410
+ if (m.length === 16) {
411
+ // 4x4 expansion via 2x2 minors of the bottom rows.
412
+ const s0 = m[0] * m[5] - m[1] * m[4];
413
+ const s1 = m[0] * m[6] - m[2] * m[4];
414
+ const s2 = m[0] * m[7] - m[3] * m[4];
415
+ const s3 = m[1] * m[6] - m[2] * m[5];
416
+ const s4 = m[1] * m[7] - m[3] * m[5];
417
+ const s5 = m[2] * m[7] - m[3] * m[6];
418
+ const c5 = m[10] * m[15] - m[11] * m[14];
419
+ const c4 = m[ 9] * m[15] - m[11] * m[13];
420
+ const c3 = m[ 9] * m[14] - m[10] * m[13];
421
+ const c2 = m[ 8] * m[15] - m[11] * m[12];
422
+ const c1 = m[ 8] * m[14] - m[10] * m[12];
423
+ const c0 = m[ 8] * m[13] - m[ 9] * m[12];
424
+ return s0 * c5 - s1 * c4 + s2 * c3 + s3 * c2 - s4 * c1 + s5 * c0;
425
+ }
426
+ throw new Error(`determinant: unsupported matrix length ${m.length}`);
427
+ },
428
+
429
+ // ---------------------------------------------------------------------
430
+ // Arithmetic helpers (work on scalars and vectors element-wise)
431
+ // ---------------------------------------------------------------------
432
+
433
+ add: (a, b) => binop(a, b, (x, y) => x + y),
434
+ sub: (a, b) => binop(a, b, (x, y) => x - y),
435
+ mul: (a, b) => binop(a, b, (x, y) => x * y),
436
+ div: (a, b) => binop(a, b, (x, y) => x / y),
437
+ rem: (a, b) => binop(a, b, (x, y) => x % y),
438
+
439
+ // Integer division — truncates toward zero, matching WGSL `/` semantics on integer types.
440
+ // The compiler routes `/` through `idiv` when both operands' tracked types are integer.
441
+ idiv: (a, b) => binop(a, b, (x, y) => Math.trunc(x / y)),
442
+
443
+ // Element-wise bitwise / shift helpers. JS's raw `&` / `|` / `^` / `<<` / `>>` work on
444
+ // scalars but not typed arrays (they coerce the array to a number → NaN), so the compiler
445
+ // routes vec/mat operands through these. Right-shift uses `>>>` (unsigned) to match WGSL
446
+ // u32 semantics; signed sources should bitcast first.
447
+ bit_and: (a, b) => binop(a, b, (x, y) => (x & y) >>> 0),
448
+ bit_or: (a, b) => binop(a, b, (x, y) => (x | y) >>> 0),
449
+ bit_xor: (a, b) => binop(a, b, (x, y) => (x ^ y) >>> 0),
450
+ shl: (a, b) => binop(a, b, (x, y) => (x << y) >>> 0),
451
+ shr: (a, b) => binop(a, b, (x, y) => x >>> y),
452
+
453
+ neg: (a) => unop(a, x => -x),
454
+ not: (a) => unop(a, x => (~x) >>> 0),
455
+
456
+ // Bit-field extract / insert (WGSL §17.9). Component-wise over vectors; `offset` and
457
+ // `count` are scalar u32 even when `e` / `newbits` are vectors, matching the WGSL
458
+ // signatures. Like `shr`, these implement the UNSIGNED reading — extracted bits are
459
+ // zero-extended, not sign-extended — so an i32 source needing sign extension should
460
+ // bitcast first. Per the spec, `o = min(offset, 32)` and `c = min(count, 32 - o)`.
461
+
462
+ /** `extractBits(e, offset, count)` read `count` bits at `offset`, zero-extended. */
463
+ extractBits(e, offset, count) {
464
+ const o = Math.min(offset >>> 0, 32);
465
+ const c = Math.min(count >>> 0, 32 - o);
466
+ return unop(e, (x) => {
467
+ if (c === 0) {
468
+ return 0;
469
+ }
470
+ const mask = c >= 32 ? 0xFFFFFFFF : ((1 << c) - 1);
471
+ return ((x >>> o) & mask) >>> 0;
472
+ });
473
+ },
474
+
475
+ // Bit counting (WGSL §17.9). Component-wise over vectors. All of these read their operand as
476
+ // 32 bits regardless of whether the WGSL type was u32 or i32 — which is what the spec says and
477
+ // what makes `countLeadingZeros` the exact counterpart of JS's `Math.clz32`.
478
+
479
+ /** `countLeadingZeros(e)` — number of consecutive zero bits above the most significant set bit; 32 for 0. */
480
+ countLeadingZeros: (e) => unop(e, x => Math.clz32(x)),
481
+
482
+ /** `countTrailingZeros(e)` — number of consecutive zero bits below the least significant set bit; 32 for 0. */
483
+ countTrailingZeros: (e) => unop(e, x => (x === 0 ? 32 : 31 - Math.clz32(x & -x))),
484
+
485
+ /** `countOneBits(e)` — population count. */
486
+ countOneBits: (e) => unop(e, x => {
487
+ let bits = x >>> 0;
488
+ let count = 0;
489
+
490
+ while (bits !== 0) {
491
+ bits &= bits - 1;
492
+ count += 1;
493
+ }
494
+
495
+ return count;
496
+ }),
497
+
498
+ /** `firstLeadingBit(e)` — index of the most significant set bit; `0xffffffff` for 0 (u32 reading). */
499
+ firstLeadingBit: (e) => unop(e, x => (x === 0 ? 0xffffffff : 31 - Math.clz32(x))),
500
+
501
+ /** `firstTrailingBit(e)` — index of the least significant set bit; `0xffffffff` for 0. */
502
+ firstTrailingBit: (e) => unop(e, x => (x === 0 ? 0xffffffff : 31 - Math.clz32(x & -x))),
503
+
504
+ /** `reverseBits(e)` reverse the order of all 32 bits. */
505
+ reverseBits: (e) => unop(e, x => {
506
+ let source = x >>> 0;
507
+ let reversed = 0;
508
+
509
+ for (let i = 0; i < 32; i++) {
510
+ reversed = ((reversed << 1) | (source & 1)) >>> 0;
511
+ source >>>= 1;
512
+ }
513
+
514
+ return reversed;
515
+ }),
516
+
517
+ /** `insertBits(e, newbits, offset, count)` — replace bits `[offset, offset+count)` of `e` with the low `count` bits of `newbits`. */
518
+ insertBits(e, newbits, offset, count) {
519
+ const o = Math.min(offset >>> 0, 32);
520
+ const c = Math.min(count >>> 0, 32 - o);
521
+ return binop(e, newbits, (x, ins) => {
522
+ if (c === 0) {
523
+ return x >>> 0;
524
+ }
525
+ const base_mask = c >= 32 ? 0xFFFFFFFF : ((1 << c) - 1);
526
+ const field_mask = o >= 32 ? 0 : ((base_mask << o) >>> 0);
527
+ return (((ins << o) & field_mask) | (x & ((~field_mask) >>> 0))) >>> 0;
528
+ });
529
+ },
530
+
531
+ // ---------------------------------------------------------------------
532
+ // WGSL math builtins
533
+ // ---------------------------------------------------------------------
534
+
535
+ abs: (a) => unop(a, Math.abs),
536
+ sign: (a) => unop(a, Math.sign),
537
+ floor: (a) => unop(a, Math.floor),
538
+ ceil: (a) => unop(a, Math.ceil),
539
+ round: (a) => unop(a, Math.round),
540
+ trunc: (a) => unop(a, Math.trunc),
541
+ fract: (a) => unop(a, x => x - Math.floor(x)),
542
+ sqrt: (a) => unop(a, Math.sqrt),
543
+ inverseSqrt: (a) => unop(a, x => 1 / Math.sqrt(x)),
544
+ exp: (a) => unop(a, Math.exp),
545
+ exp2: (a) => unop(a, x => Math.pow(2, x)),
546
+ log: (a) => unop(a, Math.log),
547
+ log2: (a) => unop(a, Math.log2),
548
+ sin: (a) => unop(a, Math.sin),
549
+ cos: (a) => unop(a, Math.cos),
550
+ tan: (a) => unop(a, Math.tan),
551
+ asin: (a) => unop(a, Math.asin),
552
+ acos: (a) => unop(a, Math.acos),
553
+ atan: (a) => unop(a, Math.atan),
554
+
555
+ pow: (a, b) => binop(a, b, Math.pow),
556
+ atan2: (a, b) => binop(a, b, Math.atan2),
557
+ min: (a, b) => binop(a, b, Math.min),
558
+ max: (a, b) => binop(a, b, Math.max),
559
+ step: (edge, x) => binop(edge, x, (e, v) => v < e ? 0 : 1),
560
+
561
+ clamp: (x, lo, hi) => ternop(x, lo, hi, (v, l, h) => Math.max(l, Math.min(h, v))),
562
+ // Saturate is `clamp(x, 0, 1)`. WGSL builtin.
563
+ saturate: (a) => unop(a, x => Math.max(0, Math.min(1, x))),
564
+ mix: (a, b, t) => ternop(a, b, t, (x, y, k) => x * (1 - k) + y * k),
565
+ // Fused multiply-add: `a * b + c`. JS lacks a true FMA; this is mathematically equivalent
566
+ // but loses the single-rounding precision benefit of hardware FMA — fine for testing.
567
+ fma: (a, b, c) => ternop(a, b, c, (x, y, z) => x * y + z),
568
+ smoothstep: (edge0, edge1, x) => ternop(edge0, edge1, x, (e0, e1, v) => {
569
+ const t = Math.max(0, Math.min(1, (v - e0) / (e1 - e0)));
570
+ return t * t * (3 - 2 * t);
571
+ }),
572
+
573
+ // ---------------------------------------------------------------------
574
+ // Vector operations
575
+ // ---------------------------------------------------------------------
576
+
577
+ dot(a, b) {
578
+ assert.equal(a.length, b.length, 'dot vector length mismatch');
579
+ let s = 0;
580
+ for (let i = 0; i < a.length; i++) s += a[i] * b[i];
581
+ return s;
582
+ },
583
+
584
+ length(v) {
585
+ if (!is_vec(v)) return Math.abs(v);
586
+ let s = 0;
587
+ for (let i = 0; i < v.length; i++) s += v[i] * v[i];
588
+ return Math.sqrt(s);
589
+ },
590
+
591
+ distance(a, b) {
592
+ return WGSLLanguageCore.length(WGSLLanguageCore.sub(a, b));
593
+ },
594
+
595
+ normalize(v) {
596
+ const len = WGSLLanguageCore.length(v);
597
+ if (len === 0) {
598
+ return new v.constructor(v.length);
599
+ }
600
+ const out = new v.constructor(v.length);
601
+ for (let i = 0; i < v.length; i++) out[i] = v[i] / len;
602
+ return out;
603
+ },
604
+
605
+ cross(a, b) {
606
+ assert.equal(a.length, 3, 'cross requires vec3');
607
+ assert.equal(b.length, 3, 'cross requires vec3');
608
+ return new a.constructor([
609
+ a[1] * b[2] - a[2] * b[1],
610
+ a[2] * b[0] - a[0] * b[2],
611
+ a[0] * b[1] - a[1] * b[0],
612
+ ]);
613
+ },
614
+
615
+ reflect(i, n) {
616
+ const d = WGSLLanguageCore.dot(i, n);
617
+ const out = new i.constructor(i.length);
618
+ for (let k = 0; k < i.length; k++) out[k] = i[k] - 2 * d * n[k];
619
+ return out;
620
+ },
621
+
622
+ // ---------------------------------------------------------------------
623
+ // Matrix × {matrix, vector} and vector × matrix products.
624
+ //
625
+ // WGSL matrices are column-major: `matCxR` has C columns of R rows each, stored as a flat
626
+ // array of C*R elements where column `c` occupies indices `[c*R, c*R + R)`.
627
+ //
628
+ // Naming convention: `m{cols}x{rows}_multiply_{...}`. Only the most common shapes are
629
+ // implemented. For unsupported shapes the compiler will emit a call to a missing helper —
630
+ // calling it raises `TypeError: wgsl.<name> is not a function`, which clearly identifies
631
+ // the shape needed; add a new entry below to support it.
632
+ // ---------------------------------------------------------------------
633
+
634
+ // mat × column-vector
635
+
636
+ m2x2f_multiply_v2f(m, v) {
637
+ const out = new Float32Array(2);
638
+ out[0] = m[0]*v[0] + m[2]*v[1];
639
+ out[1] = m[1]*v[0] + m[3]*v[1];
640
+ return out;
641
+ },
642
+
643
+ m3x3f_multiply_v3f(m, v) {
644
+ const out = new Float32Array(3);
645
+ out[0] = m[0]*v[0] + m[3]*v[1] + m[6]*v[2];
646
+ out[1] = m[1]*v[0] + m[4]*v[1] + m[7]*v[2];
647
+ out[2] = m[2]*v[0] + m[5]*v[1] + m[8]*v[2];
648
+ return out;
649
+ },
650
+
651
+ m4x4f_multiply_v4f(m, v) {
652
+ const out = new Float32Array(4);
653
+ out[0] = m[0]*v[0] + m[4]*v[1] + m[8] *v[2] + m[12]*v[3];
654
+ out[1] = m[1]*v[0] + m[5]*v[1] + m[9] *v[2] + m[13]*v[3];
655
+ out[2] = m[2]*v[0] + m[6]*v[1] + m[10]*v[2] + m[14]*v[3];
656
+ out[3] = m[3]*v[0] + m[7]*v[1] + m[11]*v[2] + m[15]*v[3];
657
+ return out;
658
+ },
659
+
660
+ // mat × mat (both square)
661
+
662
+ m2x2f_multiply_m2x2f(a, b) {
663
+ const out = new Float32Array(4);
664
+ for (let j = 0; j < 2; j++) {
665
+ const b0 = b[j*2], b1 = b[j*2 + 1];
666
+ out[j*2] = a[0]*b0 + a[2]*b1;
667
+ out[j*2 + 1] = a[1]*b0 + a[3]*b1;
668
+ }
669
+ return out;
670
+ },
671
+
672
+ m3x3f_multiply_m3x3f(a, b) {
673
+ const out = new Float32Array(9);
674
+ for (let j = 0; j < 3; j++) {
675
+ const b0 = b[j*3], b1 = b[j*3 + 1], b2 = b[j*3 + 2];
676
+ out[j*3] = a[0]*b0 + a[3]*b1 + a[6]*b2;
677
+ out[j*3 + 1] = a[1]*b0 + a[4]*b1 + a[7]*b2;
678
+ out[j*3 + 2] = a[2]*b0 + a[5]*b1 + a[8]*b2;
679
+ }
680
+ return out;
681
+ },
682
+
683
+ m4x4f_multiply_m4x4f(a, b) {
684
+ const out = new Float32Array(16);
685
+ for (let j = 0; j < 4; j++) {
686
+ const b0 = b[j*4], b1 = b[j*4 + 1], b2 = b[j*4 + 2], b3 = b[j*4 + 3];
687
+ out[j*4] = a[0]*b0 + a[4]*b1 + a[8] *b2 + a[12]*b3;
688
+ out[j*4 + 1] = a[1]*b0 + a[5]*b1 + a[9] *b2 + a[13]*b3;
689
+ out[j*4 + 2] = a[2]*b0 + a[6]*b1 + a[10]*b2 + a[14]*b3;
690
+ out[j*4 + 3] = a[3]*b0 + a[7]*b1 + a[11]*b2 + a[15]*b3;
691
+ }
692
+ return out;
693
+ },
694
+
695
+ // row-vector × mat (square)
696
+
697
+ v2f_multiply_m2x2f(v, m) {
698
+ const out = new Float32Array(2);
699
+ out[0] = v[0]*m[0] + v[1]*m[1];
700
+ out[1] = v[0]*m[2] + v[1]*m[3];
701
+ return out;
702
+ },
703
+
704
+ v3f_multiply_m3x3f(v, m) {
705
+ const out = new Float32Array(3);
706
+ out[0] = v[0]*m[0] + v[1]*m[1] + v[2]*m[2];
707
+ out[1] = v[0]*m[3] + v[1]*m[4] + v[2]*m[5];
708
+ out[2] = v[0]*m[6] + v[1]*m[7] + v[2]*m[8];
709
+ return out;
710
+ },
711
+
712
+ v4f_multiply_m4x4f(v, m) {
713
+ const out = new Float32Array(4);
714
+ out[0] = v[0]*m[0] + v[1]*m[1] + v[2]*m[2] + v[3]*m[3];
715
+ out[1] = v[0]*m[4] + v[1]*m[5] + v[2]*m[6] + v[3]*m[7];
716
+ out[2] = v[0]*m[8] + v[1]*m[9] + v[2]*m[10] + v[3]*m[11];
717
+ out[3] = v[0]*m[12] + v[1]*m[13] + v[2]*m[14] + v[3]*m[15];
718
+ return out;
719
+ },
720
+
721
+ // ---------------------------------------------------------------------
722
+ // Logical / bit helpers
723
+ // ---------------------------------------------------------------------
724
+
725
+ select(a, b, cond) {
726
+ // WGSL: select(false_value, true_value, cond)
727
+ // - cond as scalar bool: returns `b` if truthy, `a` otherwise (whole-vector pick).
728
+ // - cond as a vec<bool>-like array: element-wise pick from `a`/`b`. Both must be
729
+ // vectors of matching length.
730
+ if (cond != null && typeof cond === 'object' && typeof cond.length === 'number') {
731
+ const ctor = (a && typeof a.length === 'number') ? a.constructor
732
+ : (b && typeof b.length === 'number') ? b.constructor
733
+ : Array;
734
+ const out = new ctor(cond.length);
735
+ for (let i = 0; i < cond.length; i++) out[i] = cond[i] ? b[i] : a[i];
736
+ return out;
737
+ }
738
+ return cond ? b : a;
739
+ },
740
+
741
+ // Element-wise comparison helpers. Scalar in → boolean out; vector in → Array<boolean> of
742
+ // matching length (modelling WGSL's vec<bool>). Used by the compiler when at least one
743
+ // operand of `==` / `!=` / `<` / `>` / `<=` / `>=` is a vector.
744
+ eq: (a, b) => binop_cmp(a, b, (x, y) => x === y),
745
+ ne: (a, b) => binop_cmp(a, b, (x, y) => x !== y),
746
+ lt: (a, b) => binop_cmp(a, b, (x, y) => x < y),
747
+ gt: (a, b) => binop_cmp(a, b, (x, y) => x > y),
748
+ le: (a, b) => binop_cmp(a, b, (x, y) => x <= y),
749
+ ge: (a, b) => binop_cmp(a, b, (x, y) => x >= y),
750
+
751
+ all(v) {
752
+ if (!is_vec(v)) return !!v;
753
+ for (let i = 0; i < v.length; i++) if (!v[i]) return false;
754
+ return true;
755
+ },
756
+
757
+ any(v) {
758
+ if (!is_vec(v)) return !!v;
759
+ for (let i = 0; i < v.length; i++) if (v[i]) return true;
760
+ return false;
761
+ },
762
+
763
+ // ---------------------------------------------------------------------
764
+ // Texture intrinsics - backed by `CPUBitmapData` (1D / 2D / 3D inferred
765
+ // from the texture's `size`). Sampling routes through
766
+ // `cpu_texture_sample`, which honours the bound `SamplerDescriptor`'s
767
+ // address modes and filter. Mipmaps are not modelled (LOD 0 only) -
768
+ // see cpu_texture_sample header for details.
769
+ // ---------------------------------------------------------------------
770
+
771
+ /**
772
+ * `textureLoad(t, coords, level) -> vec4<T>`
773
+ *
774
+ * Integer-coord fetch. `coords` is i32-vector matching the texture's
775
+ * dimensionality (1, 2, or 3 entries). Out-of-bounds reads return zero
776
+ * across all channels (with alpha defaulting to 1.0 for textures with
777
+ * fewer than 4 channels - matches WGSL semantics).
778
+ *
779
+ * A texture flagged `is_depth` returns a bare `f32` instead, matching WGSL: `textureLoad` on a
780
+ * `texture_depth_*` is declared to yield a scalar, and shader code written against one uses it
781
+ * as such. Handing that code a vec4 would turn every use into a component-wise broadcast that
782
+ * neither throws nor matches the device.
783
+ *
784
+ * @param {import("./CPUBitmapData.js").CPUBitmapData} t
785
+ * @param {Int32Array | Uint32Array | number[] | number} coords
786
+ * @param {number} [_level] - ignored, mipmaps are not modelled
787
+ * @returns {Float32Array | number}
788
+ */
789
+ textureLoad(t, coords, _level_or_index = 0, _array_level = undefined) {
790
+ assert.defined(t, 'texture');
791
+ const out = new Float32Array(4);
792
+ const dim = t.dimensionality;
793
+ // textureLoad on texture_1d takes a scalar i32 in WGSL (not vec1).
794
+ const x = (dim >= 1 ? (typeof coords === 'number' ? coords : coords[0]) : 0) | 0;
795
+ const y = (dim >= 2 ? coords[1] : 0) | 0;
796
+ /*
797
+ Four arguments is the texture_2d_array form — (t, coords, array_index, level) — where the
798
+ third argument is a slice rather than a mip level. A 2d-array is backed by a depth>1
799
+ CPUBitmapData, exactly like a 3D texture; only the call shape tells them apart.
800
+ */
801
+ const z = (_array_level !== undefined ? _level_or_index : (dim >= 3 ? coords[2] : 0)) | 0;
802
+ const item_size = t.item_size;
803
+ const out_of_bounds = x < 0 || x >= t.size[0] || y < 0 || y >= t.size[1] || z < 0 || z >= t.size[2];
804
+
805
+ if (t.is_depth === true) {
806
+ return out_of_bounds ? 0.0 : t.data[t.pixel_offset(x, y, z)];
807
+ }
808
+
809
+ if (item_size < 4) out[3] = 1.0;
810
+ if (out_of_bounds) {
811
+ return out;
812
+ }
813
+ const base = t.pixel_offset(x, y, z);
814
+ const n = Math.min(item_size, 4);
815
+ for (let i = 0; i < n; i++) out[i] = t.data[base + i];
816
+ return out;
817
+ },
818
+
819
+ /**
820
+ * `textureSampleLevel(t, s, coords, level) -> vec4<T>`
821
+ *
822
+ * Filtered sample at normalised UV coords. Honours the sampler's
823
+ * address modes and `magFilter`. `coords` length matches the texture
824
+ * dimensionality. The `level` arg is ignored (no mipmaps).
825
+ *
826
+ * @param {import("./CPUBitmapData.js").CPUBitmapData} t
827
+ * @param {import("../../descriptor/texture/SamplerDescriptor.js").SamplerDescriptor | null} sampler
828
+ * @param {Float32Array | number[] | number} coords
829
+ * @param {number} [_level]
830
+ * @returns {Float32Array}
831
+ */
832
+ textureSampleLevel(t, sampler, coords, _level_or_index = 0, _array_level = undefined) {
833
+ assert.defined(t, 'texture');
834
+ const dim = t.dimensionality;
835
+ // Convert UV (normalised) to texel-space coords expected by
836
+ // cpu_texture_sample. coords for a 1D texture is a scalar in WGSL.
837
+ const u = dim >= 1 ? (typeof coords === 'number' ? coords : coords[0]) : 0;
838
+ const v = dim >= 2 ? coords[1] : 0;
839
+ const tx = u * t.size[0];
840
+ const ty = v * t.size[1];
841
+ /**
842
+ * @type {number[]}
843
+ */
844
+ let texel_coord;
845
+ if (_array_level !== undefined) {
846
+ /*
847
+ Five arguments is the texture_2d_array form — (t, s, uv, array_index, level) — where the
848
+ fourth argument selects a slice. A slice's texel-space centre sits at index + 0.5, which
849
+ lands the linear filter's z weight on exactly that slice: filtering happens in xy and
850
+ never crosses into a neighbour.
851
+ */
852
+ texel_coord = [tx, ty, (_level_or_index | 0) + 0.5];
853
+ } else {
854
+ const w = dim >= 3 ? coords[2] : 0;
855
+ const tz = w * t.size[2];
856
+ texel_coord = dim === 1 ? [tx] : (dim === 2 ? [tx, ty] : [tx, ty, tz]);
857
+ }
858
+ const result = new Float32Array(4);
859
+ cpu_texture_sample(result, sampler ?? FALLBACK_LINEAR_CLAMP_SAMPLER, t, texel_coord);
860
+ return result;
861
+ },
862
+
863
+ /**
864
+ * `textureSample(t, s, coords) -> vec4<T>` and the texture_2d_array form
865
+ * `textureSample(t, s, coords, array_index)`.
866
+ *
867
+ * Implicit-LOD sample. Without mipmaps, this is just
868
+ * `textureSampleLevel` at level 0.
869
+ *
870
+ * @param {import("./CPUBitmapData.js").CPUBitmapData} t
871
+ * @param {import("../../descriptor/texture/SamplerDescriptor.js").SamplerDescriptor | null} sampler
872
+ * @param {Float32Array | number[] | number} coords
873
+ * @param {number} [array_index]
874
+ * @returns {Float32Array}
875
+ */
876
+ textureSample(t, sampler, coords, array_index = undefined) {
877
+ if (array_index !== undefined) {
878
+ return WGSLLanguageCore.textureSampleLevel(t, sampler, coords, array_index, 0);
879
+ }
880
+
881
+ return WGSLLanguageCore.textureSampleLevel(t, sampler, coords, 0);
882
+ },
883
+
884
+ /**
885
+ * `textureSampleGrad(t, s, coords, ddx, ddy) -> vec4<T>` and the texture_2d_array form
886
+ * `textureSampleGrad(t, s, coords, array_index, ddx, ddy)`.
887
+ *
888
+ * Explicit-gradient sample. Mipmaps are not modelled (see the header), so the gradients only
889
+ * disambiguate which call shape this is: six arguments carry an array index, five do not.
890
+ *
891
+ * @param {import("./CPUBitmapData.js").CPUBitmapData} t
892
+ * @param {import("../../descriptor/texture/SamplerDescriptor.js").SamplerDescriptor | null} sampler
893
+ * @param {Float32Array | number[] | number} coords
894
+ * @param {Float32Array | number[] | number} p3 gradient, or the array index
895
+ * @param {Float32Array | number[]} p4 gradient
896
+ * @param {Float32Array | number[]} [p5] gradient, in the array form
897
+ * @returns {Float32Array}
898
+ */
899
+ textureSampleGrad(t, sampler, coords, p3, p4, p5 = undefined) {
900
+ if (p5 !== undefined) {
901
+ return WGSLLanguageCore.textureSampleLevel(t, sampler, coords, p3, 0);
902
+ }
903
+
904
+ return WGSLLanguageCore.textureSampleLevel(t, sampler, coords, 0);
905
+ },
906
+
907
+ /**
908
+ * `textureGather(component, t, s, coords[, array_index][, offset]) -> vec4<f32>` and the
909
+ * depth-texture overloads `textureGather(t, s, coords[, array_index][, offset])`, which carry
910
+ * no `component` argument because a `texture_depth_*` has a single channel.
911
+ *
912
+ * Returns the four texels of the bilinear footprint unblended, in WGSL's order —
913
+ * counter-clockwise from the lower-left, so `.wzxy` is row-major. See
914
+ * {@link cpu_texture_gather} for the footprint derivation, the component order in full, and
915
+ * why `magFilter` is ignored here (a gather never filters).
916
+ *
917
+ * Overloads are told apart by argument SHAPE, the way `textureLoad` and `textureSampleLevel`
918
+ * already tell their array forms apart: a leading number is the `component` of a colour
919
+ * gather, a leading texture means the depth overload; among the trailing arguments a number
920
+ * is an `array_index` and a vector is the const `offset`.
921
+ *
922
+ * **Cube textures are not supported** and throw rather than returning something. The emulator
923
+ * models no cube layout at all — {@link CPUBitmapData} is a flat 1D/2D/3D grid — so there is
924
+ * no face selection to gather across. A cube gather is recognisable by its `vec3` direction
925
+ * coordinate (`texture_3d` has no gather overload in WGSL at all).
926
+ *
927
+ * @param {number | import("./CPUBitmapData.js").CPUBitmapData} p0 `component`, or the
928
+ * texture in the depth overloads
929
+ * @param {*} p1
930
+ * @param {*} p2
931
+ * @param {*} p3
932
+ * @param {*} [p4]
933
+ * @param {*} [p5]
934
+ * @returns {Float32Array}
935
+ */
936
+ textureGather(p0, p1, p2, p3, p4 = undefined, p5 = undefined) {
937
+ const has_component = typeof p0 === 'number';
938
+
939
+ const t = has_component ? p1 : p0;
940
+ const sampler = has_component ? p2 : p1;
941
+ const coords = has_component ? p3 : p2;
942
+
943
+ assert.defined(t, 'texture');
944
+
945
+ if (coords !== null && coords !== undefined && coords.length >= 3) {
946
+ throw new Error("textureGather: cube textures are not supported — the emulator has no cube layout to gather a face out of (CPUBitmapData is a flat 1D/2D/3D grid). Remap to an octahedral or array atlas, as the engine's shadow path does, and gather from that.");
947
+ }
948
+
949
+ const component = has_component ? (p0 | 0) : 0;
950
+
951
+ let array_index = 0;
952
+ let offset_x = 0;
953
+ let offset_y = 0;
954
+
955
+ const extra_a = has_component ? p4 : p3;
956
+ const extra_b = has_component ? p5 : p4;
957
+
958
+ if (typeof extra_a === 'number') array_index = extra_a;
959
+ else if (extra_a !== undefined) { offset_x = extra_a[0]; offset_y = extra_a[1]; }
960
+
961
+ if (extra_b !== undefined) { offset_x = extra_b[0]; offset_y = extra_b[1]; }
962
+
963
+ const result = new Float32Array(4);
964
+
965
+ cpu_texture_gather(
966
+ result,
967
+ sampler ?? FALLBACK_LINEAR_CLAMP_SAMPLER,
968
+ t,
969
+ gather_texel_coord(coords, t),
970
+ component,
971
+ offset_x,
972
+ offset_y,
973
+ array_index
974
+ );
975
+
976
+ return result;
977
+ },
978
+
979
+ /**
980
+ * `textureGatherCompare(t, s, coords[, array_index], depth_ref[, offset]) -> vec4<f32>`
981
+ *
982
+ * The comparison form: each of the four gathered texels is tested against `depth_ref` and
983
+ * yields 1.0 or 0.0 unfiltered, per texel. The blend is the shader's job, which is exactly
984
+ * what `shadowmap_sample_filtered` does with the result (`.wzxy` into
985
+ * `interpolate_bilinear_f32`).
986
+ *
987
+ * The comparison itself comes from the bound sampler's `compare`, evaluated as
988
+ * `compare(depth_ref, texel)` see {@link evaluate_gpu_compare_function} for why that order
989
+ * is load-bearing. A sampler with no `compare` set is an error rather than a defaulted one:
990
+ * WGSL requires a `sampler_comparison` here, and picking a direction on the caller's behalf
991
+ * would invert shadows silently.
992
+ *
993
+ * Overload disambiguation is by shape, as in {@link textureGather}: a trailing vector is the
994
+ * const `offset`, and a fourth argument that is followed by another number is the
995
+ * `array_index` rather than the reference depth.
996
+ *
997
+ * @param {import("./CPUBitmapData.js").CPUBitmapData} t
998
+ * @param {import("../../descriptor/texture/SamplerDescriptor.js").SamplerDescriptor} sampler
999
+ * @param {Float32Array | number[]} coords
1000
+ * @param {*} p3 `depth_ref`, or the `array_index` in the array forms
1001
+ * @param {*} [p4]
1002
+ * @param {*} [p5]
1003
+ * @returns {Float32Array}
1004
+ */
1005
+ textureGatherCompare(t, sampler, coords, p3, p4 = undefined, p5 = undefined) {
1006
+ assert.defined(t, 'texture');
1007
+
1008
+ if (coords !== null && coords !== undefined && coords.length >= 3) {
1009
+ throw new Error("textureGatherCompare: cube textures are not supported — the emulator has no cube layout to gather a face out of (CPUBitmapData is a flat 1D/2D/3D grid). Remap to an octahedral or array atlas, as the engine's shadow path does, and gather from that.");
1010
+ }
1011
+
1012
+ let array_index = 0;
1013
+ let depth_ref;
1014
+ let offset = undefined;
1015
+
1016
+ if (p5 !== undefined) {
1017
+ // (t, s, coords, array_index, depth_ref, offset)
1018
+ array_index = p3;
1019
+ depth_ref = p4;
1020
+ offset = p5;
1021
+ } else if (p4 === undefined) {
1022
+ // (t, s, coords, depth_ref)
1023
+ depth_ref = p3;
1024
+ } else if (typeof p4 === 'number') {
1025
+ // (t, s, coords, array_index, depth_ref) — a scalar in the 5th slot is the reference
1026
+ array_index = p3;
1027
+ depth_ref = p4;
1028
+ } else {
1029
+ // (t, s, coords, depth_ref, offset) — a vector in the 5th slot is the offset
1030
+ depth_ref = p3;
1031
+ offset = p4;
1032
+ }
1033
+
1034
+ const effective_sampler = sampler ?? FALLBACK_LINEAR_CLAMP_SAMPLER;
1035
+ const compare = effective_sampler.compare;
1036
+
1037
+ if (compare === undefined || compare === null) {
1038
+ throw new Error("textureGatherCompare: the bound sampler has no 'compare' function — WGSL requires a sampler_comparison here. Bind a SamplerDescriptor with `compare` set (the engine's shadow path uses SHADOWMAP_COMPARE_SAMPLER_DESCRIPTOR, `compare: \"greater\"` for reverse-Z).");
1039
+ }
1040
+
1041
+ const result = new Float32Array(4);
1042
+
1043
+ cpu_texture_gather(
1044
+ result,
1045
+ effective_sampler,
1046
+ t,
1047
+ gather_texel_coord(coords, t),
1048
+ 0,
1049
+ offset !== undefined ? offset[0] : 0,
1050
+ offset !== undefined ? offset[1] : 0,
1051
+ array_index
1052
+ );
1053
+
1054
+ for (let i = 0; i < 4; i++) {
1055
+ result[i] = evaluate_gpu_compare_function(compare, depth_ref, result[i]);
1056
+ }
1057
+
1058
+ return result;
1059
+ },
1060
+
1061
+ /**
1062
+ * `textureStore(t, coords, value) -> ()`
1063
+ *
1064
+ * Integer-coord write into a storage texture. Out-of-bounds writes are
1065
+ * silently dropped (WGSL semantics). `coords` length matches the
1066
+ * texture dimensionality.
1067
+ *
1068
+ * @param {import("./CPUBitmapData.js").CPUBitmapData} t
1069
+ * @param {Int32Array | Uint32Array | number[] | number} coords
1070
+ * @param {Float32Array | Int32Array | Uint32Array | number[]} value
1071
+ */
1072
+ textureStore(t, coords, value) {
1073
+ assert.defined(t, 'texture');
1074
+ const dim = t.dimensionality;
1075
+ const x = (dim >= 1 ? (typeof coords === 'number' ? coords : coords[0]) : 0) | 0;
1076
+ const y = (dim >= 2 ? coords[1] : 0) | 0;
1077
+ const z = (dim >= 3 ? coords[2] : 0) | 0;
1078
+ t.write(x, y, z, value);
1079
+ },
1080
+
1081
+ /**
1082
+ * `textureDimensions(t, level) -> u32 | vec2<u32> | vec3<u32>`
1083
+ *
1084
+ * Returns the texture's size as a vector matching its dimensionality.
1085
+ * 1D textures return a scalar `u32` in WGSL; we still hand back a
1086
+ * length-1 Uint32Array so the calling shader can swizzle/index it
1087
+ * uniformly with the multi-axis case.
1088
+ *
1089
+ * @param {import("./CPUBitmapData.js").CPUBitmapData} t
1090
+ * @param {number} [_level]
1091
+ * @returns {Uint32Array}
1092
+ */
1093
+ textureDimensions(t, _level = 0) {
1094
+ assert.defined(t, 'texture');
1095
+ const dim = t.dimensionality;
1096
+ if (dim === 1) return new Uint32Array([t.size[0]]);
1097
+ if (dim === 2) return new Uint32Array([t.size[0], t.size[1]]);
1098
+ return new Uint32Array([t.size[0], t.size[1], t.size[2]]);
1099
+ },
1100
+
1101
+ // ---------------------------------------------------------------------
1102
+ // Atomic ops on `atomic<T>` (modeled as { value: number } objects)
1103
+ // ---------------------------------------------------------------------
1104
+
1105
+ atomicLoad(p) { return p.value; },
1106
+ atomicStore(p, v) { p.value = v; },
1107
+ atomicAdd(p, v) { const old = p.value; p.value = (p.value + v) | 0; return old; },
1108
+ atomicSub(p, v) { const old = p.value; p.value = (p.value - v) | 0; return old; },
1109
+ atomicMin(p, v) { const old = p.value; p.value = Math.min(p.value, v); return old; },
1110
+ atomicMax(p, v) { const old = p.value; p.value = Math.max(p.value, v); return old; },
1111
+ atomicAnd(p, v) { const old = p.value; p.value = p.value & v; return old; },
1112
+ atomicOr(p, v) { const old = p.value; p.value = p.value | v; return old; },
1113
+ atomicXor(p, v) { const old = p.value; p.value = p.value ^ v; return old; },
1114
+ atomicExchange(p, v) { const old = p.value; p.value = v; return old; },
1115
+ atomicCompareExchangeWeak(p, expected, v) {
1116
+ const old = p.value;
1117
+ if (old === expected) {
1118
+ p.value = v;
1119
+ return { old_value: old, exchanged: true };
1120
+ }
1121
+ return { old_value: old, exchanged: false };
1122
+ },
1123
+
1124
+ // ---------------------------------------------------------------------
1125
+ // Barriers — no-ops when running pointwise (one thread).
1126
+ // ---------------------------------------------------------------------
1127
+
1128
+ workgroupBarrier() { /* no-op */ },
1129
+ storageBarrier() { /* no-op */ },
1130
+ textureBarrier() { /* no-op */ },
1131
+
1132
+ // A barrier plus a load of the pointed-at workgroup variable, which the compiler has already
1133
+ // reduced to the variable itself — `&x` and `*x` both compile to `x`, since pointers are
1134
+ // modelled as the values they name. The barrier half is the no-op above.
1135
+ workgroupUniformLoad(value) { return value; },
1136
+
1137
+ // ---------------------------------------------------------------------
1138
+ // Fragment-stage derivatives — unsupported because we run a single pixel.
1139
+ // ---------------------------------------------------------------------
1140
+ //
1141
+ // dpdx/dpdy/fwidth and their fine/coarse variants are implicit-derivative builtins that the
1142
+ // GPU computes from a 2x2 quad of neighbouring fragment threads. The emulator dispatches one
1143
+ // pixel at a time, so there's no neighbouring data to take a finite difference against —
1144
+ // any sensible value would be a fabrication. We throw to surface this clearly in tests.
1145
+
1146
+ dpdx() { throw new Error('dpdx is not available in the fragment-shader emulator: derivatives require a 2x2 quad, but the emulator runs a single pixel.'); },
1147
+ dpdy() { throw new Error('dpdy is not available in the fragment-shader emulator: derivatives require a 2x2 quad, but the emulator runs a single pixel.'); },
1148
+ fwidth() { throw new Error('fwidth is not available in the fragment-shader emulator: derivatives require a 2x2 quad, but the emulator runs a single pixel.'); },
1149
+ dpdxFine() { throw new Error('dpdxFine is not available in the fragment-shader emulator: derivatives require a 2x2 quad, but the emulator runs a single pixel.'); },
1150
+ dpdyFine() { throw new Error('dpdyFine is not available in the fragment-shader emulator: derivatives require a 2x2 quad, but the emulator runs a single pixel.'); },
1151
+ fwidthFine() { throw new Error('fwidthFine is not available in the fragment-shader emulator: derivatives require a 2x2 quad, but the emulator runs a single pixel.'); },
1152
+ dpdxCoarse() { throw new Error('dpdxCoarse is not available in the fragment-shader emulator: derivatives require a 2x2 quad, but the emulator runs a single pixel.'); },
1153
+ dpdyCoarse() { throw new Error('dpdyCoarse is not available in the fragment-shader emulator: derivatives require a 2x2 quad, but the emulator runs a single pixel.'); },
1154
+ fwidthCoarse() { throw new Error('fwidthCoarse is not available in the fragment-shader emulator: derivatives require a 2x2 quad, but the emulator runs a single pixel.'); },
1155
+
1156
+ // ---------------------------------------------------------------------
1157
+ // Pack / unpack — fixed-point and float bit-level packers from the WGSL spec.
1158
+ // ---------------------------------------------------------------------
1159
+
1160
+ /** vec2<f32>(low_u16, high_u16) / 65535.0 — components in [0, 1]. */
1161
+ unpack2x16unorm(e) {
1162
+ const u = e >>> 0;
1163
+ return new Float32Array([(u & 0xFFFF) / 65535.0, ((u >>> 16) & 0xFFFF) / 65535.0]);
1164
+ },
1165
+
1166
+ /** vec2<f32>(low_i16, high_i16) / 32767.0 — components in [-1, 1]. */
1167
+ unpack2x16snorm(e) {
1168
+ const u = e >>> 0;
1169
+ const sign16 = (h) => (h & 0x8000) !== 0 ? h - 0x10000 : h;
1170
+ const x = sign16(u & 0xFFFF) / 32767.0;
1171
+ const y = sign16((u >>> 16) & 0xFFFF) / 32767.0;
1172
+ return new Float32Array([Math.max(x, -1), Math.max(y, -1)]);
1173
+ },
1174
+
1175
+ /** vec4<f32>(byte0, byte1, byte2, byte3) / 255.0 — components in [0, 1]. */
1176
+ unpack4x8unorm(e) {
1177
+ const u = e >>> 0;
1178
+ return new Float32Array([
1179
+ (u & 0xFF) / 255.0,
1180
+ ((u >>> 8) & 0xFF) / 255.0,
1181
+ ((u >>> 16) & 0xFF) / 255.0,
1182
+ ((u >>> 24) & 0xFF) / 255.0,
1183
+ ]);
1184
+ },
1185
+
1186
+ /** vec4<f32>(i8 byte0..3) / 127.0 — components in [-1, 1]. */
1187
+ unpack4x8snorm(e) {
1188
+ const u = e >>> 0;
1189
+ const sign8 = (b) => (b & 0x80) !== 0 ? b - 0x100 : b;
1190
+ const out = new Float32Array(4);
1191
+ for (let i = 0; i < 4; i++) {
1192
+ out[i] = Math.max(sign8((u >>> (i * 8)) & 0xFF) / 127.0, -1);
1193
+ }
1194
+ return out;
1195
+ },
1196
+
1197
+ /** Inverse of unpack2x16unorm — clamp to [0, 1] and round to 16-bit integer. */
1198
+ pack2x16unorm(v) {
1199
+ const x = Math.round(Math.min(Math.max(v[0], 0), 1) * 65535.0);
1200
+ const y = Math.round(Math.min(Math.max(v[1], 0), 1) * 65535.0);
1201
+ return ((y << 16) | x) >>> 0;
1202
+ },
1203
+
1204
+ /** Inverse of unpack2x16snorm — clamp to [-1, 1] and round to 16-bit signed integer. */
1205
+ pack2x16snorm(v) {
1206
+ const enc = (f) => Math.round(Math.min(Math.max(f, -1), 1) * 32767.0) & 0xFFFF;
1207
+ return ((enc(v[1]) << 16) | enc(v[0])) >>> 0;
1208
+ },
1209
+
1210
+ /** Inverse of unpack4x8unorm — clamp to [0, 1] and round to 8-bit integer per channel. */
1211
+ pack4x8unorm(v) {
1212
+ let r = 0;
1213
+ for (let i = 0; i < 4; i++) {
1214
+ const b = Math.round(Math.min(Math.max(v[i], 0), 1) * 255.0) & 0xFF;
1215
+ r |= b << (i * 8);
1216
+ }
1217
+ return r >>> 0;
1218
+ },
1219
+
1220
+ /** Inverse of unpack4x8snorm — clamp to [-1, 1] and round to 8-bit signed integer per channel. */
1221
+ pack4x8snorm(v) {
1222
+ let r = 0;
1223
+ for (let i = 0; i < 4; i++) {
1224
+ const b = Math.round(Math.min(Math.max(v[i], -1), 1) * 127.0) & 0xFF;
1225
+ r |= b << (i * 8);
1226
+ }
1227
+ return r >>> 0;
1228
+ },
1229
+ };
1230
+
1231
+ /**
1232
+ * Bit-cast (WGSL `bitcast`). Reinterprets a value's bit pattern as a different type.
1233
+ *
1234
+ * JS has no separate u32/i32/f32 scalar types — they're all `number` — so the source type
1235
+ * cannot be detected from the value at runtime. The compiler passes it explicitly when known;
1236
+ * otherwise we fall back to a heuristic: integer-targeted casts assume f32 source, float-targeted
1237
+ * casts assume u32 source (the common "interpret bit pattern as float" idiom).
1238
+ *
1239
+ * For typed-array (vector) inputs the source type is auto-detected from the array constructor.
1240
+ *
1241
+ * @param {*} v
1242
+ * @param {string} target — destination type name, e.g. `'f32'` / `'u32'` / `'i32'`
1243
+ * @param {string} [source] — source type name; required for unambiguous scalar casts
1244
+ * @returns {number | TypedArray}
1245
+ */
1246
+ WGSLLanguageCore.bitcast = function (v, target, source) {
1247
+ // Vector targets (`vec3<u32>` etc.) reduce to their scalar type — the runtime stores vectors
1248
+ // as flat typed arrays, so the per-element bitcast is the same as for a scalar of that type.
1249
+ const vec_match = typeof target === 'string' ? target.match(/^vec\d<([^>]+)>$/) : null;
1250
+ if (vec_match !== null) {
1251
+ target = vec_match[1];
1252
+ }
1253
+
1254
+ function convert_scalar(x, t_from, t_to) {
1255
+ if (t_to === 'f32') {
1256
+ if (t_from === 'u32') { BITCAST_U32[0] = x >>> 0; return BITCAST_F32[0]; }
1257
+ if (t_from === 'i32') { BITCAST_I32[0] = x | 0; return BITCAST_F32[0]; }
1258
+ } else if (t_to === 'u32') {
1259
+ if (t_from === 'f32') { BITCAST_F32[0] = x; return BITCAST_U32[0]; }
1260
+ if (t_from === 'i32') { return (x | 0) >>> 0; }
1261
+ } else if (t_to === 'i32') {
1262
+ if (t_from === 'f32') { BITCAST_F32[0] = x; return BITCAST_I32[0]; }
1263
+ if (t_from === 'u32') { return (x >>> 0) | 0; }
1264
+ }
1265
+ return x;
1266
+ }
1267
+
1268
+ if (is_vec(v)) {
1269
+ let from = source;
1270
+ if (from === undefined) {
1271
+ if (v instanceof Float32Array) from = 'f32';
1272
+ else if (v instanceof Float16Array) from = 'f16';
1273
+ else if (v instanceof Uint32Array) from = 'u32';
1274
+ else if (v instanceof Int32Array) from = 'i32';
1275
+ else from = 'f32';
1276
+ }
1277
+
1278
+ let outCtor;
1279
+ if (target === 'f32') outCtor = Float32Array;
1280
+ else if (target === 'f16') outCtor = Float16Array;
1281
+ else if (target === 'u32') outCtor = Uint32Array;
1282
+ else if (target === 'i32') outCtor = Int32Array;
1283
+ else outCtor = v.constructor;
1284
+
1285
+ const out = new outCtor(v.length);
1286
+ for (let i = 0; i < v.length; i++) {
1287
+ out[i] = convert_scalar(v[i], from, target);
1288
+ }
1289
+ return out;
1290
+ }
1291
+
1292
+ let from = source;
1293
+ if (from === undefined) {
1294
+ from = (target === 'f32' || target === 'f16') ? 'u32' : 'f32';
1295
+ }
1296
+ return convert_scalar(v, from, target);
1297
+ };