p5 2.2.3 → 2.3.0-rc.1

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 (137) hide show
  1. package/dist/accessibility/color_namer.js +9 -11
  2. package/dist/accessibility/describe.js +0 -1
  3. package/dist/accessibility/gridOutput.js +0 -1
  4. package/dist/accessibility/index.js +9 -10
  5. package/dist/accessibility/outputs.js +0 -1
  6. package/dist/accessibility/textOutput.js +0 -1
  7. package/dist/app.js +11 -10
  8. package/dist/app.node.js +122 -0
  9. package/dist/color/color_conversion.js +9 -11
  10. package/dist/color/creating_reading.js +1 -1
  11. package/dist/color/index.js +2 -2
  12. package/dist/color/p5.Color.js +1 -1
  13. package/dist/color/setting.js +25 -12
  14. package/dist/{constants-BdTiYOQI.js → constants-0wkVUfqa.js} +2 -2
  15. package/dist/core/States.js +1 -1
  16. package/dist/core/constants.js +1 -1
  17. package/dist/core/environment.js +28 -29
  18. package/dist/core/filterShaders.js +1 -1
  19. package/dist/core/friendly_errors/fes_core.js +9 -8
  20. package/dist/core/friendly_errors/file_errors.js +1 -2
  21. package/dist/core/friendly_errors/index.js +1 -1
  22. package/dist/core/friendly_errors/param_validator.js +737 -640
  23. package/dist/core/friendly_errors/sketch_verifier.js +1 -1
  24. package/dist/core/friendly_errors/stacktrace.js +0 -1
  25. package/dist/core/helpers.js +3 -4
  26. package/dist/core/init.js +24 -21
  27. package/dist/core/internationalization.js +1 -1
  28. package/dist/core/legacy.js +9 -11
  29. package/dist/core/main.js +9 -10
  30. package/dist/core/p5.Graphics.js +5 -5
  31. package/dist/core/p5.Renderer.js +3 -3
  32. package/dist/core/p5.Renderer2D.js +9 -10
  33. package/dist/core/p5.Renderer3D.js +5 -5
  34. package/dist/core/rendering.js +5 -5
  35. package/dist/core/structure.js +0 -1
  36. package/dist/core/transform.js +7 -16
  37. package/dist/{creating_reading-C7hu6sg1.js → creating_reading-DLkHH80h.js} +11 -8
  38. package/dist/data/local_storage.js +0 -1
  39. package/dist/dom/dom.js +2 -3
  40. package/dist/dom/index.js +2 -2
  41. package/dist/dom/p5.Element.js +2 -2
  42. package/dist/dom/p5.MediaElement.js +2 -2
  43. package/dist/events/acceleration.js +5 -3
  44. package/dist/events/keyboard.js +0 -1
  45. package/dist/events/pointer.js +0 -2
  46. package/dist/image/const.js +1 -1
  47. package/dist/image/filterRenderer2D.js +19 -12
  48. package/dist/image/image.js +5 -5
  49. package/dist/image/index.js +5 -5
  50. package/dist/image/loading_displaying.js +5 -5
  51. package/dist/image/p5.Image.js +3 -3
  52. package/dist/image/pixels.js +0 -1
  53. package/dist/io/files.js +5 -5
  54. package/dist/io/index.js +5 -5
  55. package/dist/io/p5.Table.js +0 -1
  56. package/dist/io/p5.TableRow.js +0 -1
  57. package/dist/io/p5.XML.js +0 -1
  58. package/dist/{ir_builders-Cd6rU9Vm.js → ir_builders-C2ebb6Lu.js} +234 -1
  59. package/dist/{main-H_nu4eDs.js → main-D2kqeMXM.js} +107 -136
  60. package/dist/math/Matrices/Matrix.js +1 -1
  61. package/dist/math/Matrices/MatrixNumjs.js +1 -1
  62. package/dist/math/calculation.js +0 -1
  63. package/dist/math/index.js +3 -1
  64. package/dist/math/math.js +3 -17
  65. package/dist/math/noise.js +0 -1
  66. package/dist/math/p5.Matrix.js +1 -2
  67. package/dist/math/p5.Vector.js +237 -279
  68. package/dist/math/patch-vector.js +75 -0
  69. package/dist/math/random.js +0 -1
  70. package/dist/math/trigonometry.js +3 -4
  71. package/dist/{p5.Renderer-BmD2P6Wv.js → p5.Renderer-CQI8PO1F.js} +31 -24
  72. package/dist/{rendering-CC8JNTwG.js → rendering-ltTIxpF2.js} +732 -44
  73. package/dist/shape/2d_primitives.js +1 -4
  74. package/dist/shape/attributes.js +43 -8
  75. package/dist/shape/curves.js +0 -1
  76. package/dist/shape/custom_shapes.js +260 -5
  77. package/dist/shape/index.js +2 -2
  78. package/dist/shape/vertex.js +0 -2
  79. package/dist/strands/ir_builders.js +1 -1
  80. package/dist/strands/ir_types.js +5 -1
  81. package/dist/strands/p5.strands.js +286 -31
  82. package/dist/strands/strands_api.js +179 -8
  83. package/dist/strands/strands_codegen.js +26 -8
  84. package/dist/strands/strands_conditionals.js +1 -1
  85. package/dist/strands/strands_for.js +1 -1
  86. package/dist/strands/strands_node.js +1 -1
  87. package/dist/strands/strands_ternary.js +56 -0
  88. package/dist/strands/strands_transpiler.js +416 -251
  89. package/dist/strands_glslBackend-i-ReKgZo.js +423 -0
  90. package/dist/type/index.js +3 -3
  91. package/dist/type/lib/Typr.js +1 -1
  92. package/dist/type/p5.Font.js +3 -3
  93. package/dist/type/textCore.js +31 -24
  94. package/dist/utilities/conversion.js +0 -1
  95. package/dist/utilities/time_date.js +0 -1
  96. package/dist/utilities/utility_functions.js +0 -1
  97. package/dist/webgl/3d_primitives.js +5 -5
  98. package/dist/webgl/GeometryBuilder.js +1 -1
  99. package/dist/webgl/ShapeBuilder.js +26 -1
  100. package/dist/webgl/enums.js +1 -1
  101. package/dist/webgl/index.js +8 -9
  102. package/dist/webgl/interaction.js +8 -4
  103. package/dist/webgl/light.js +5 -5
  104. package/dist/webgl/loading.js +60 -21
  105. package/dist/webgl/material.js +5 -5
  106. package/dist/webgl/p5.Camera.js +5 -5
  107. package/dist/webgl/p5.Framebuffer.js +5 -5
  108. package/dist/webgl/p5.Geometry.js +3 -5
  109. package/dist/webgl/p5.Quat.js +1 -1
  110. package/dist/webgl/p5.RendererGL.js +17 -21
  111. package/dist/webgl/p5.Shader.js +129 -36
  112. package/dist/webgl/p5.Texture.js +5 -5
  113. package/dist/webgl/strands_glslBackend.js +5 -386
  114. package/dist/webgl/text.js +5 -5
  115. package/dist/webgl/utils.js +5 -5
  116. package/dist/webgl2Compatibility-DA7DLMuq.js +7 -0
  117. package/dist/webgpu/index.js +7 -3
  118. package/dist/webgpu/p5.RendererWebGPU.js +1146 -180
  119. package/dist/webgpu/shaders/color.js +1 -1
  120. package/dist/webgpu/shaders/compute.js +32 -0
  121. package/dist/webgpu/shaders/functions/randomComputeWGSL.js +31 -0
  122. package/dist/webgpu/shaders/functions/randomVertWGSL.js +30 -0
  123. package/dist/webgpu/shaders/functions/randomWGSL.js +30 -0
  124. package/dist/webgpu/shaders/line.js +1 -1
  125. package/dist/webgpu/shaders/material.js +3 -3
  126. package/dist/webgpu/strands_wgslBackend.js +137 -15
  127. package/lib/p5.esm.js +4092 -1950
  128. package/lib/p5.esm.min.js +1 -1
  129. package/lib/p5.js +4092 -1950
  130. package/lib/p5.min.js +1 -1
  131. package/lib/p5.webgpu.esm.js +1748 -306
  132. package/lib/p5.webgpu.js +1747 -305
  133. package/lib/p5.webgpu.min.js +1 -1
  134. package/package.json +6 -1
  135. package/types/global.d.ts +4182 -2441
  136. package/types/p5.d.ts +2776 -1675
  137. package/dist/noise3DGLSL-Bwrdi4gi.js +0 -9
@@ -1,4 +1,4 @@
1
- import { W as WEBGPU, T as TRIANGLE_STRIP, L as LIGHTEST, D as DARKEST, S as SUBTRACT, R as REPLACE, E as EXCLUSION, a as SCREEN, M as MULTIPLY, b as REMOVE, A as ADD, B as BLEND, c as TRIANGLES, U as UNSIGNED_BYTE, F as FLOAT, H as HALF_FLOAT, d as UNSIGNED_INT, e as MIRROR, f as REPEAT, C as CLAMP, g as LINEAR, N as NEAREST } from '../constants-BdTiYOQI.js';
1
+ import { W as WEBGPU, T as TRIANGLE_STRIP, L as LIGHTEST, D as DARKEST, S as SUBTRACT, R as REPLACE, E as EXCLUSION, a as SCREEN, M as MULTIPLY, b as REMOVE, A as ADD, B as BLEND, c as TRIANGLES, U as UNSIGNED_BYTE, F as FLOAT, H as HALF_FLOAT, d as UNSIGNED_INT, e as MIRROR, f as REPEAT, C as CLAMP, g as LINEAR, N as NEAREST } from '../constants-0wkVUfqa.js';
2
2
  import { getStrokeDefs } from '../webgl/enums.js';
3
3
  import { DataType } from '../strands/ir_types.js';
4
4
  import { colorVertexShader, colorFragmentShader } from './shaders/color.js';
@@ -7,15 +7,26 @@ import { materialVertexShader, materialFragmentShader } from './shaders/material
7
7
  import { fontVertexShader, fontFragmentShader } from './shaders/font.js';
8
8
  import { blitVertexShader, blitFragmentShader } from './shaders/blit.js';
9
9
  import { wgslBackend } from './strands_wgslBackend.js';
10
- import noiseWGSL from './shaders/functions/noise3DWGSL.js';
11
10
  import { baseFilterVertexShader, baseFilterFragmentShader } from './shaders/filters/base.js';
12
11
  import { imageLightVertexShader, imageLightDiffusedFragmentShader, imageLightSpecularFragmentShader } from './shaders/imageLight.js';
12
+ import { baseComputeShader } from './shaders/compute.js';
13
+ import './shaders/functions/noise3DWGSL.js';
14
+ import './shaders/functions/randomWGSL.js';
15
+ import './shaders/functions/randomVertWGSL.js';
16
+ import './shaders/functions/randomComputeWGSL.js';
13
17
  import '../strands/ir_dag.js';
14
18
  import '../strands/strands_FES.js';
15
- import '../ir_builders-Cd6rU9Vm.js';
19
+ import '../ir_builders-C2ebb6Lu.js';
16
20
  import '../strands/ir_cfg.js';
17
21
  import '../strands/strands_builtins.js';
18
22
 
23
+ /**
24
+ * @module 3D
25
+ * @submodule p5.strands
26
+ * @for p5
27
+ */
28
+
29
+
19
30
  const FRAME_STATE = {
20
31
  PENDING: 0,
21
32
  UNPROMOTED: 1,
@@ -37,6 +48,338 @@ function rendererWebGPU(p5, fn) {
37
48
  RGBA,
38
49
  } = p5;
39
50
 
51
+ class StorageBuffer {
52
+ constructor(buffer, size, renderer, schema = null) {
53
+ this._isStorageBuffer = true;
54
+ this.buffer = buffer;
55
+ this.size = size;
56
+ this._renderer = renderer;
57
+ this._schema = schema;
58
+ }
59
+
60
+ /**
61
+ * Updates the data in the buffer with new values. The new data must be in
62
+ * the same format as the data originally passed to
63
+ * <a href="#/p5/createStorage">`createStorage()`</a>.
64
+ *
65
+ * ```js example
66
+ * let particles;
67
+ * let computeShader;
68
+ * let displayShader;
69
+ * let instance;
70
+ * const numParticles = 100;
71
+ *
72
+ * async function setup() {
73
+ * await createCanvas(100, 100, WEBGPU);
74
+ * particles = createStorage(makeParticles(width / 2, height / 2));
75
+ * computeShader = buildComputeShader(simulate);
76
+ * displayShader = buildMaterialShader(display);
77
+ * instance = buildGeometry(drawParticle);
78
+ * describe('100 orange particles shooting outward.');
79
+ * }
80
+ *
81
+ * function makeParticles(x, y) {
82
+ * let data = [];
83
+ * for (let i = 0; i < numParticles; i++) {
84
+ * let angle = (i / numParticles) * TWO_PI;
85
+ * let speed = random(0.5, 2);
86
+ * data.push({
87
+ * position: createVector(x, y),
88
+ * velocity: createVector(cos(angle) * speed, sin(angle) * speed),
89
+ * });
90
+ * }
91
+ * return data;
92
+ * }
93
+ *
94
+ * function drawParticle() {
95
+ * sphere(2);
96
+ * }
97
+ *
98
+ * function simulate() {
99
+ * let data = uniformStorage(particles);
100
+ * let idx = index.x;
101
+ * data[idx].position = data[idx].position + data[idx].velocity;
102
+ * }
103
+ *
104
+ * function display() {
105
+ * let data = uniformStorage(particles);
106
+ * worldInputs.begin();
107
+ * let pos = data[instanceID()].position;
108
+ * worldInputs.position.xy += pos - [width / 2, height / 2];
109
+ * worldInputs.end();
110
+ * }
111
+ *
112
+ * function draw() {
113
+ * background(30);
114
+ * if (frameCount % 60 === 0) {
115
+ * particles.update(makeParticles(random(width), random(height)));
116
+ * }
117
+ * compute(computeShader, numParticles);
118
+ * noStroke();
119
+ * fill(255, 200, 50);
120
+ * shader(displayShader);
121
+ * model(instance, numParticles);
122
+ * }
123
+ * ```
124
+ *
125
+ * @method update
126
+ * @for p5.StorageBuffer
127
+ * @beta
128
+ * @webgpu
129
+ * @webgpuOnly
130
+ * @param {Number[]|Float32Array|Object[]} data The new data to write into the buffer.
131
+ */
132
+ update(data) {
133
+ const device = this._renderer.device;
134
+
135
+ if (this._schema !== null) {
136
+ // Buffer was created with a struct array
137
+ if (
138
+ !Array.isArray(data) ||
139
+ data.length === 0 ||
140
+ typeof data[0] !== 'object' ||
141
+ Array.isArray(data[0])
142
+ ) {
143
+ throw new Error(
144
+ 'update() expects an array of objects matching the original struct format'
145
+ );
146
+ }
147
+
148
+ const newSchema = this._renderer._inferStructSchema(data[0]);
149
+ if (newSchema.structBody !== this._schema.structBody) {
150
+ throw new Error(
151
+ `update() data structure doesn't match the original.\n` +
152
+ ` Expected: ${this._schema.structBody}\n` +
153
+ ` Got: ${newSchema.structBody}`
154
+ );
155
+ }
156
+
157
+ const packed = this._renderer._packStructArray(data, this._schema);
158
+ if (packed.byteLength > this.size) {
159
+ throw new Error(
160
+ `update() data (${packed.byteLength} bytes) exceeds buffer size (${this.size} bytes)`
161
+ );
162
+ }
163
+ device.queue.writeBuffer(this.buffer, 0, packed);
164
+ } else {
165
+ // Buffer was created with a float array
166
+ let floatData;
167
+ if (data instanceof Float32Array) {
168
+ floatData = data;
169
+ } else if (Array.isArray(data)) {
170
+ floatData = new Float32Array(data);
171
+ } else {
172
+ throw new Error(
173
+ 'update() expects a Float32Array or array of numbers for this buffer'
174
+ );
175
+ }
176
+
177
+ if (floatData.byteLength > this.size) {
178
+ throw new Error(
179
+ `update() data (${floatData.byteLength} bytes) exceeds buffer size (${this.size} bytes)`
180
+ );
181
+ }
182
+ device.queue.writeBuffer(this.buffer, 0, floatData);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Reads data from a storage buffer back into JavaScript.
188
+ *
189
+ * Copies data from the GPU to the CPU using a temporary buffer,
190
+ * so it must be awaited. Returns a `Float32Array` for number
191
+ * buffers, or an array of plain objects for struct buffers.
192
+ *
193
+ * Note: This is a GPU -> CPU read, so calling it often (like every frame)
194
+ * can be slow.
195
+ *
196
+ * ```js example
197
+ * let data;
198
+ * let computeShader;
199
+ *
200
+ * async function setup() {
201
+ * await createCanvas(100, 100, WEBGPU);
202
+ *
203
+ * data = createStorage(new Float32Array([1, 2, 3, 4]));
204
+ * computeShader = buildComputeShader(doubleValues);
205
+ * compute(computeShader, 4);
206
+ *
207
+ * let result = await data.read();
208
+ * // result is Float32Array [2, 4, 6, 8]
209
+ * for (let i = 0; i < result.length; i++) {
210
+ * print(result[i]);
211
+ * }
212
+ * describe('Prints the values 2, 4, 6, 8 to the console.');
213
+ * }
214
+ *
215
+ * function doubleValues() {
216
+ * let d = uniformStorage(data);
217
+ * let idx = index.x;
218
+ * d[idx] = d[idx] * 2;
219
+ * }
220
+ * ```
221
+ *
222
+ * @method read
223
+ * @for p5.StorageBuffer
224
+ * @beta
225
+ * @webgpu
226
+ * @webgpuOnly
227
+ * @returns {Promise<Float32Array|Object[]>}
228
+ */
229
+ async read() {
230
+ const device = this._renderer.device;
231
+ this._renderer.flushDraw();
232
+
233
+ const stagingBuffer = device.createBuffer({
234
+ size: this.size,
235
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
236
+ });
237
+
238
+ const commandEncoder = device.createCommandEncoder();
239
+ commandEncoder.copyBufferToBuffer(this.buffer, 0, stagingBuffer, 0, this.size);
240
+ device.queue.submit([commandEncoder.finish()]);
241
+
242
+ await stagingBuffer.mapAsync(GPUMapMode.READ, 0, this.size);
243
+ const mappedRange = stagingBuffer.getMappedRange(0, this.size);
244
+
245
+ // Copy before unmapping because mapped memory becomes invalid after unmap
246
+ const rawCopy = new Float32Array(mappedRange.byteLength / 4);
247
+ rawCopy.set(new Float32Array(mappedRange));
248
+
249
+ stagingBuffer.unmap();
250
+ stagingBuffer.destroy();
251
+
252
+ if (this._schema !== null) {
253
+ return this._renderer._unpackStructArray(rawCopy, this._schema);
254
+ }
255
+ return rawCopy;
256
+ }
257
+
258
+ /**
259
+ * Updates a single element in the buffer at a given index. Use this
260
+ * when only a small number of elements need to change. If you need to
261
+ * replace all the data at once, use
262
+ * <a href="#/p5.StorageBuffer/update">`update()`</a> instead.
263
+ *
264
+ * ```js
265
+ * let buf;
266
+ *
267
+ * async function setup() {
268
+ * await createCanvas(100, 100, WEBGPU);
269
+ *
270
+ * // Float buffer: update one value by index
271
+ * buf = createStorage(new Float32Array([1, 2, 3, 4]));
272
+ * buf.set(2, 9.5); // only index 2 changes → [1, 2, 9.5, 4]
273
+ *
274
+ * let result = await buf.read();
275
+ * print(result[2]); // 9.5
276
+ * describe('Prints 9.5 to the console.');
277
+ * }
278
+ * ```
279
+ *
280
+ * ```js
281
+ * let particles;
282
+ * const numParticles = 100;
283
+ *
284
+ * async function setup() {
285
+ * await createCanvas(100, 100, WEBGPU);
286
+ * particles = createStorage(makeParticles());
287
+ *
288
+ * // Replace particle 42 without touching the others
289
+ * particles.set(42, {
290
+ * position: createVector(0, 0),
291
+ * velocity: createVector(1, 0),
292
+ * });
293
+ *
294
+ * // Read back to confirm the update
295
+ * let result = await particles.read();
296
+ * print(result[42].position.x, result[42].position.y); // 0, 0
297
+ * describe('Prints the position of particle 42 after updating it.');
298
+ * }
299
+ *
300
+ * function makeParticles() {
301
+ * let data = [];
302
+ * for (let i = 0; i < numParticles; i++) {
303
+ * data.push({
304
+ * position: createVector(random(width), random(height)),
305
+ * velocity: createVector(random(-1, 1), random(-1, 1)),
306
+ * });
307
+ * }
308
+ * return data;
309
+ * }
310
+ * ```
311
+ *
312
+ * @method set
313
+ * @for p5.StorageBuffer
314
+ * @beta
315
+ * @webgpu
316
+ * @webgpuOnly
317
+ * @param {Number} index The zero-based index of the element to update.
318
+ * @param {Number|Object} value The new value. Pass a number for float
319
+ * buffers, or a plain object matching the original struct layout for
320
+ * struct buffers.
321
+ */
322
+ set(index, value) {
323
+ const device = this._renderer.device;
324
+
325
+ if (this._schema !== null) {
326
+ // buffer was created with an array of structs
327
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
328
+ throw new Error(
329
+ 'set() expects a plain object matching the original struct format for this buffer'
330
+ );
331
+ }
332
+
333
+ const { stride } = this._schema;
334
+ const byteOffset = index * stride;
335
+
336
+ if (byteOffset + stride > this.size) {
337
+ throw new Error(
338
+ `set() index ${index} is out of bounds for this buffer ` +
339
+ `(buffer holds ${Math.floor(this.size / stride)} elements)`
340
+ );
341
+ }
342
+
343
+ // pack just this one element using the same logic as update()
344
+ const packed = this._renderer._packStructArray([value], this._schema);
345
+ // use packed.buffer (ArrayBuffer) so the size arg is always in bytes
346
+ device.queue.writeBuffer(this.buffer, byteOffset, packed.buffer, 0, stride);
347
+ } else {
348
+ // buffer was created with a float array
349
+ if (typeof value !== 'number') {
350
+ throw new Error(
351
+ 'set() expects a number for this float buffer'
352
+ );
353
+ }
354
+
355
+ const byteOffset = index * 4;
356
+
357
+ if (byteOffset + 4 > this.size) {
358
+ throw new Error(
359
+ `set() index ${index} is out of bounds for this buffer ` +
360
+ `(buffer holds ${Math.floor(this.size / 4)} floats)`
361
+ );
362
+ }
363
+
364
+ device.queue.writeBuffer(this.buffer, byteOffset, new Float32Array([value]));
365
+ }
366
+ }
367
+ }
368
+
369
+ /**
370
+ * A block of data that shaders can read from, and compute shaders can also
371
+ * write to. This is only available in WebGPU mode.
372
+ *
373
+ * Note: <a href="#/p5/createStorage">`createStorage()`</a> is the recommended
374
+ * way to create an instance of this class.
375
+ *
376
+ * @class p5.StorageBuffer
377
+ * @beta
378
+ * @webgpu
379
+ * @webgpuOnly
380
+ */
381
+ p5.StorageBuffer = StorageBuffer;
382
+
40
383
  class RendererWebGPU extends Renderer3D {
41
384
  constructor(pInst, w, h, isMainCanvas, elt) {
42
385
  super(pInst, w, h, isMainCanvas, elt);
@@ -88,6 +431,9 @@ function rendererWebGPU(p5, fn) {
88
431
  // Retired buffers to destroy at end of frame
89
432
  this._retiredBuffers = [];
90
433
 
434
+ // Storage buffers for compute shaders
435
+ this._storageBuffers = new Set();
436
+
91
437
  // 2D canvas for pixel reading fallback
92
438
  this._pixelReadCanvas = null;
93
439
  this._pixelReadCtx = null;
@@ -164,7 +510,7 @@ function rendererWebGPU(p5, fn) {
164
510
  }
165
511
  if (this._pInst._webgpuAttributes[key] !== value) {
166
512
  //changing value of previously altered attribute
167
- this._webgpuAttributes[key] = value;
513
+ this._pInst._webgpuAttributes[key] = value;
168
514
  unchanged = false;
169
515
  }
170
516
  //setting all attributes with some change
@@ -298,9 +644,21 @@ function rendererWebGPU(p5, fn) {
298
644
  const _b = args[2] || 0;
299
645
  const _a = args[3] || 0;
300
646
 
301
- // If PENDING and no custom framebuffer, clear means stay UNPROMOTED
302
- if (this._frameState === FRAME_STATE.PENDING && !this.activeFramebuffer()) {
303
- this._frameState = FRAME_STATE.UNPROMOTED;
647
+ // If PENDING and no custom framebuffer, clear means stay UNPROMOTED.
648
+ // However, if we are still in setup (frameCount == 0), we must promote
649
+ // so that mainFramebuffer gets the cleared content. This ensures that if
650
+ // draw() later promotes without a copy, it starts from the correct state
651
+ // rather than a stale mainFramebuffer.
652
+ // Note: a mid-draw-loop transition from UNPROMOTED back to PROMOTED
653
+ // (i.e. calling background() some frames but not others) will still
654
+ // lose intermediate UNPROMOTED frame content.
655
+ if (this._frameState !== FRAME_STATE.PROMOTED && !this.activeFramebuffer()) {
656
+ if (this._pInst.frameCount > 0) {
657
+ this._frameState = FRAME_STATE.UNPROMOTED;
658
+ } else {
659
+ this._promoteToFramebufferWithoutCopy();
660
+ // clear() then targets mainFramebuffer via activeFramebuffer()
661
+ }
304
662
  }
305
663
 
306
664
  this._finishActiveRenderPass();
@@ -503,7 +861,8 @@ function rendererWebGPU(p5, fn) {
503
861
  return 4; // Cap at 4 for broader compatibility
504
862
  }
505
863
 
506
- _shaderOptions({ mode }) {
864
+ _shaderOptions({ mode, compute, workgroupSize }) {
865
+ if (compute) return { compute: true, workgroupSize };
507
866
  const activeFramebuffer = this.activeFramebuffer();
508
867
  const format = activeFramebuffer ?
509
868
  this._getWebGPUColorFormat(activeFramebuffer) :
@@ -514,9 +873,9 @@ function rendererWebGPU(p5, fn) {
514
873
  1; // No MSAA needed when blitting already-antialiased textures to canvas
515
874
  const sampleCount = this._getValidSampleCount(requestedSampleCount);
516
875
 
517
- const depthFormat = activeFramebuffer && activeFramebuffer.useDepth ?
518
- this._getWebGPUDepthFormat(activeFramebuffer) :
519
- this.depthFormat;
876
+ const depthFormat = activeFramebuffer
877
+ ? (activeFramebuffer.useDepth ? this._getWebGPUDepthFormat(activeFramebuffer) : undefined)
878
+ : this.depthFormat;
520
879
 
521
880
  const drawTarget = this.drawTarget();
522
881
  const clipping = this._clipping;
@@ -544,6 +903,31 @@ function rendererWebGPU(p5, fn) {
544
903
  _initShader(shader) {
545
904
  const device = this.device;
546
905
 
906
+ if (shader.shaderType === 'compute') {
907
+ // Compute shader initialization
908
+ shader.computeModule = device.createShaderModule({ code: shader.computeSrc() });
909
+ shader._computePipelineCache = null;
910
+ shader._workgroupSize = null;
911
+
912
+ // Create compute pipeline (deferred until first compute() call)
913
+ shader.getPipeline = ({ workgroupSize }) => {
914
+ if (!shader._computePipelineCache) {
915
+ shader._computePipelineCache = device.createComputePipeline({
916
+ layout: shader._pipelineLayout,
917
+ compute: {
918
+ module: shader.computeModule,
919
+ entryPoint: 'main'
920
+ }
921
+ });
922
+ shader._workgroupSize = workgroupSize;
923
+ }
924
+ return shader._computePipelineCache;
925
+ };
926
+
927
+ return;
928
+ }
929
+
930
+ // Render shader initialization
547
931
  shader.vertModule = device.createShaderModule({ code: shader.vertSrc() });
548
932
  shader.fragModule = device.createShaderModule({ code: shader.fragSrc() });
549
933
 
@@ -568,25 +952,27 @@ function rendererWebGPU(p5, fn) {
568
952
  },
569
953
  primitive: { topology },
570
954
  multisample: { count: sampleCount },
571
- depthStencil: {
572
- format: depthFormat,
573
- depthWriteEnabled: !clipping,
574
- depthCompare: 'less-equal',
575
- stencilFront: {
576
- compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
577
- failOp: 'keep',
578
- depthFailOp: 'keep',
579
- passOp: clipping ? 'replace' : 'keep',
580
- },
581
- stencilBack: {
582
- compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
583
- failOp: 'keep',
584
- depthFailOp: 'keep',
585
- passOp: clipping ? 'replace' : 'keep',
955
+ ...(depthFormat ? {
956
+ depthStencil: {
957
+ format: depthFormat,
958
+ depthWriteEnabled: !clipping,
959
+ depthCompare: 'less-equal',
960
+ stencilFront: {
961
+ compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
962
+ failOp: 'keep',
963
+ depthFailOp: 'keep',
964
+ passOp: clipping ? 'replace' : 'keep',
965
+ },
966
+ stencilBack: {
967
+ compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
968
+ failOp: 'keep',
969
+ depthFailOp: 'keep',
970
+ passOp: clipping ? 'replace' : 'keep',
971
+ },
972
+ stencilReadMask: 0xFF,
973
+ stencilWriteMask: clipping ? 0xFF : 0x00,
586
974
  },
587
- stencilReadMask: 0xFF,
588
- stencilWriteMask: clipping ? 0xFF : 0x00,
589
- },
975
+ } : {}),
590
976
  });
591
977
  shader._pipelineCache.set(key, pipeline);
592
978
  }
@@ -642,7 +1028,9 @@ function rendererWebGPU(p5, fn) {
642
1028
  entries.push({
643
1029
  bufferGroup,
644
1030
  binding: bufferGroup.binding,
645
- visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
1031
+ visibility: shader.shaderType === 'compute'
1032
+ ? GPUShaderStage.COMPUTE
1033
+ : GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
646
1034
  buffer: { type: 'uniform', hasDynamicOffset: bufferGroup.dynamic },
647
1035
  });
648
1036
  structEntries.set(bufferGroup.group, entries);
@@ -676,6 +1064,24 @@ function rendererWebGPU(p5, fn) {
676
1064
  groupEntries.set(group, entries);
677
1065
  }
678
1066
 
1067
+ // Add storage buffer bindings
1068
+ for (const storage of shader._storageBuffers || []) {
1069
+ const group = storage.group;
1070
+ const entries = groupEntries.get(group) || [];
1071
+
1072
+ entries.push({
1073
+ binding: storage.binding,
1074
+ visibility: storage.visibility,
1075
+ buffer: {
1076
+ type: storage.accessMode === 'read' ? 'read-only-storage' : 'storage'
1077
+ },
1078
+ storage: storage,
1079
+ });
1080
+
1081
+ entries.sort((a, b) => a.binding - b.binding);
1082
+ groupEntries.set(group, entries);
1083
+ }
1084
+
679
1085
  // Create layouts and bind groups
680
1086
  const groupEntriesArr = [];
681
1087
  for (const [group, entries] of groupEntries) {
@@ -694,6 +1100,7 @@ function rendererWebGPU(p5, fn) {
694
1100
  shader._pipelineLayout = this.device.createPipelineLayout({
695
1101
  bindGroupLayouts: shader._bindGroupLayouts,
696
1102
  });
1103
+ shader._compiled = true;
697
1104
  }
698
1105
 
699
1106
  _getBlendState(mode) {
@@ -940,8 +1347,11 @@ function rendererWebGPU(p5, fn) {
940
1347
 
941
1348
  _resetBuffersBeforeDraw() {
942
1349
  this._finishActiveRenderPass();
1350
+
943
1351
  // Set state to PENDING - we'll decide on first draw
944
- this._frameState = FRAME_STATE.PENDING;
1352
+ if (this._pInst.frameCount > 0) {
1353
+ this._frameState = FRAME_STATE.PENDING;
1354
+ }
945
1355
 
946
1356
  // Clear depth buffer but DON'T start any render pass yet
947
1357
  const activeFramebuffer = this.activeFramebuffer();
@@ -1052,6 +1462,8 @@ function rendererWebGPU(p5, fn) {
1052
1462
  // once we're drawing to the framebuffer, because normally
1053
1463
  // those are reset.
1054
1464
  const savedModelMatrix = this.states.uModelMatrix.copy();
1465
+ this.states.uModelMatrix.set(this.states.uModelMatrix.copy());
1466
+ this.states.uModelMatrix.reset();
1055
1467
  this.mainFramebuffer.defaultCamera.set(this.states.curCamera);
1056
1468
 
1057
1469
  this.mainFramebuffer.begin();
@@ -1060,6 +1472,11 @@ function rendererWebGPU(p5, fn) {
1060
1472
  }
1061
1473
 
1062
1474
  _promoteToFramebufferWithoutCopy() {
1475
+ // Already promoted this frame
1476
+ if (this._frameState === FRAME_STATE.PROMOTED) {
1477
+ return;
1478
+ }
1479
+
1063
1480
  // Ensure mainFramebuffer matches canvas size
1064
1481
  if (this.mainFramebuffer.width !== this.width ||
1065
1482
  this.mainFramebuffer.height !== this.height) {
@@ -1074,6 +1491,8 @@ function rendererWebGPU(p5, fn) {
1074
1491
 
1075
1492
  // Preserve transformation state
1076
1493
  const savedModelMatrix = this.states.uModelMatrix.copy();
1494
+ this.states.uModelMatrix.set(this.states.uModelMatrix.copy());
1495
+ this.states.uModelMatrix.reset();
1077
1496
  this.mainFramebuffer.defaultCamera.set(this.states.curCamera);
1078
1497
 
1079
1498
  // Begin rendering to mainFramebuffer
@@ -1387,7 +1806,6 @@ function rendererWebGPU(p5, fn) {
1387
1806
  }
1388
1807
  this.flushDraw();
1389
1808
 
1390
- // this._pInst.background('red');
1391
1809
  this._pInst.push();
1392
1810
  this.states.setValue('enableLighting', false);
1393
1811
  this.states.setValue('activeImageLight', null);
@@ -1452,25 +1870,9 @@ function rendererWebGPU(p5, fn) {
1452
1870
 
1453
1871
  this._beginActiveRenderPass();
1454
1872
  const passEncoder = this.activeRenderPass;
1455
- const currentShader = this._curShader;
1456
- const shaderOptions = this._shaderOptions({ mode });
1457
- if (this.activeShader !== currentShader || this._shaderOptionsDifferent(shaderOptions)) {
1458
- passEncoder.setPipeline(currentShader.getPipeline(shaderOptions));
1459
- }
1460
- this.activeShader = currentShader;
1461
- this.activeShaderOptions = shaderOptions;
1462
1873
 
1463
- // Set stencil reference value for clipping
1464
- const drawTarget = this.drawTarget();
1465
- if (drawTarget._isClipApplied && !this._clipping) {
1466
- // When using the clip mask, test against reference value 0 (background)
1467
- // WebGL uses NOTEQUAL with ref 0, so fragments pass where stencil != 0
1468
- // In WebGPU with 'not-equal', we need ref 0 to pass where stencil != 0
1469
- passEncoder.setStencilReference(0);
1470
- } else if (this._clipping) {
1471
- // When writing to the clip mask, write reference value 1
1472
- passEncoder.setStencilReference(1);
1473
- }
1874
+ const currentShader = this._curShader;
1875
+ this.setupShaderBindGroups(currentShader, passEncoder, { mode, buffers });
1474
1876
  // Bind vertex buffers
1475
1877
  for (const buffer of currentShader._vertexBuffers || this._getVertexBuffers(currentShader)) {
1476
1878
  const location = currentShader.attributes[buffer.attr].location;
@@ -1478,6 +1880,58 @@ function rendererWebGPU(p5, fn) {
1478
1880
  passEncoder.setVertexBuffer(location, gpuBuffer, 0);
1479
1881
  }
1480
1882
 
1883
+ if (currentShader.shaderType === "fill") {
1884
+ // Bind index buffer and issue draw
1885
+ if (buffers.indexBuffer) {
1886
+ const indexFormat = buffers.indexFormat || "uint16";
1887
+ passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
1888
+ passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
1889
+ } else {
1890
+ passEncoder.draw(geometry.vertices.length, count, 0, 0);
1891
+ }
1892
+ } else if (currentShader.shaderType === "text") {
1893
+ if (!buffers.indexBuffer) {
1894
+ throw new Error("Text geometry must have an index buffer");
1895
+ }
1896
+ const indexFormat = buffers.indexFormat || "uint16";
1897
+ passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
1898
+ passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
1899
+ }
1900
+
1901
+ if (buffers.lineVerticesBuffer && currentShader.shaderType === "stroke") {
1902
+ passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0);
1903
+ }
1904
+
1905
+ // Mark that we have pending draws that need submission
1906
+ this._hasPendingDraws = true;
1907
+ }
1908
+
1909
+ setupShaderBindGroups(currentShader, passEncoder, shaderOptionsParams) {
1910
+ const shaderOptions = this._shaderOptions(shaderOptionsParams);
1911
+ if (
1912
+ shaderOptions.compute ||
1913
+ this.activeShader !== currentShader ||
1914
+ this._shaderOptionsDifferent(shaderOptions)
1915
+ ) {
1916
+ passEncoder.setPipeline(currentShader.getPipeline(shaderOptions));
1917
+ }
1918
+ if (!shaderOptions.compute) {
1919
+ this.activeShader = currentShader;
1920
+ this.activeShaderOptions = shaderOptions;
1921
+
1922
+ // Set stencil reference value for clipping
1923
+ const drawTarget = this.drawTarget();
1924
+ if (drawTarget._isClipApplied && !this._clipping) {
1925
+ // When using the clip mask, test against reference value 0 (background)
1926
+ // WebGL uses NOTEQUAL with ref 0, so fragments pass where stencil != 0
1927
+ // In WebGPU with 'not-equal', we need ref 0 to pass where stencil != 0
1928
+ passEncoder.setStencilReference(0);
1929
+ } else if (this._clipping) {
1930
+ // When writing to the clip mask, write reference value 1
1931
+ passEncoder.setStencilReference(1);
1932
+ }
1933
+ }
1934
+
1481
1935
  for (const bufferGroup of currentShader._uniformBufferGroups) {
1482
1936
  if (bufferGroup.dynamic) {
1483
1937
  // Bind uniforms into a part of a big dynamic memory block because
@@ -1530,6 +1984,13 @@ function rendererWebGPU(p5, fn) {
1530
1984
  currentShader.buffersDirty.delete(key);
1531
1985
  }
1532
1986
  }
1987
+ for (const storage of currentShader._storageBuffers || []) {
1988
+ const key = storage.group * 1000 + storage.binding;
1989
+ if (currentShader.buffersDirty.has(key)) {
1990
+ currentShader._cachedBindGroup[storage.group] = undefined;
1991
+ currentShader.buffersDirty.delete(key);
1992
+ }
1993
+ }
1533
1994
 
1534
1995
  // Bind sampler/texture uniforms and uniform buffers
1535
1996
  for (const iter of currentShader._groupEntries) {
@@ -1559,6 +2020,19 @@ function rendererWebGPU(p5, fn) {
1559
2020
  : { buffer: uniformBufferInfo.buffer },
1560
2021
  });
1561
2022
  }
2023
+ } else if (entry.storage && !bindGroup) {
2024
+ // Storage buffer binding
2025
+ const uniform = currentShader.uniforms[entry.storage.name];
2026
+ if (!uniform || !uniform._cachedData || !uniform._cachedData._isStorageBuffer) {
2027
+ throw new Error(
2028
+ `Storage buffer "${entry.storage.name}" not set. ` +
2029
+ `Use shader.setUniform("${entry.storage.name}", storageBuffer)`
2030
+ );
2031
+ }
2032
+ bgEntries.push({
2033
+ binding: entry.binding,
2034
+ resource: { buffer: uniform._cachedData.buffer },
2035
+ });
1562
2036
  } else if (!bindGroup) {
1563
2037
  bgEntries.push({
1564
2038
  binding: entry.binding,
@@ -1592,84 +2066,71 @@ function rendererWebGPU(p5, fn) {
1592
2066
  );
1593
2067
  }
1594
2068
  }
2069
+ return passEncoder;
2070
+ }
1595
2071
 
1596
- if (currentShader.shaderType === "fill") {
1597
- // Bind index buffer and issue draw
1598
- if (buffers.indexBuffer) {
1599
- const indexFormat = buffers.indexFormat || "uint16";
1600
- passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
1601
- passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
2072
+ //////////////////////////////////////////////
2073
+ // SHADER
2074
+ //////////////////////////////////////////////
2075
+
2076
+ // Writes a single field's value into a Float32Array+DataView at (baseOffset + field.offset).
2077
+ //
2078
+ // Field interface (shared by uniform fields from _parseStruct and struct storage schema fields):
2079
+ // baseType: string - 'f32', 'i32', 'u32', etc.
2080
+ // size: number - byte size of the field
2081
+ // offset: number - byte offset of the field within its struct
2082
+ // packInPlace: bool - true for mat3, written with manual column padding
2083
+ //
2084
+ // value: number or number[] - the data to write
2085
+ _packField(field, value, floatView, dataView, baseOffset) {
2086
+ if (value === undefined) return;
2087
+
2088
+ // Duck typing instead of instanceof to avoid importing a separate
2089
+ // copy of the Color/Vector classes
2090
+ if (value?.isVector) {
2091
+ value = value.values.length !== value.dimensions ? value.values.slice(0, value.dimensions) : value.values;
2092
+ } else if (value?.isColor) {
2093
+ value = value._getRGBA([1, 1, 1, 1]);
2094
+ }
2095
+ const byteOffset = baseOffset + field.offset;
2096
+ if (field.baseType === 'u32') {
2097
+ if (field.size === 4) {
2098
+ dataView.setUint32(byteOffset, value, true);
1602
2099
  } else {
1603
- passEncoder.draw(geometry.vertices.length, count, 0, 0);
2100
+ for (let i = 0; i < value.length; i++) {
2101
+ dataView.setUint32(byteOffset + i * 4, value[i], true);
2102
+ }
1604
2103
  }
1605
- } else if (currentShader.shaderType === "text") {
1606
- if (!buffers.indexBuffer) {
1607
- throw new Error("Text geometry must have an index buffer");
2104
+ } else if (field.baseType === 'i32') {
2105
+ if (field.size === 4) {
2106
+ dataView.setInt32(byteOffset, value, true);
2107
+ } else {
2108
+ for (let i = 0; i < value.length; i++) {
2109
+ dataView.setInt32(byteOffset + i * 4, value[i], true);
2110
+ }
1608
2111
  }
1609
- const indexFormat = buffers.indexFormat || "uint16";
1610
- passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
1611
- passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
1612
- }
1613
-
1614
- if (buffers.lineVerticesBuffer && currentShader.shaderType === "stroke") {
1615
- passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0);
2112
+ } else if (field.packInPlace) {
2113
+ // In-place packing for mat3: write directly to buffer with padding
2114
+ const base = byteOffset / 4;
2115
+ floatView[base + 0] = value[0]; floatView[base + 1] = value[1]; floatView[base + 2] = value[2];
2116
+ floatView[base + 4] = value[3]; floatView[base + 5] = value[4]; floatView[base + 6] = value[5];
2117
+ floatView[base + 8] = value[6]; floatView[base + 9] = value[7]; floatView[base + 10] = value[8];
2118
+ } else if (field.size === 4) {
2119
+ floatView.set([value], byteOffset / 4);
2120
+ } else {
2121
+ floatView.set(value, byteOffset / 4);
1616
2122
  }
1617
-
1618
- // Mark that we have pending draws that need submission
1619
- this._hasPendingDraws = true;
1620
2123
  }
1621
2124
 
1622
- //////////////////////////////////////////////
1623
- // SHADER
1624
- //////////////////////////////////////////////
1625
-
1626
2125
  _packUniformGroup(shader, groupUniforms, bufferInfo) {
1627
2126
  // Pack a single group's uniforms into a buffer
1628
2127
  const data = bufferInfo.data;
1629
2128
  const dataView = bufferInfo.dataView;
1630
-
1631
2129
  const offset = bufferInfo.offset || 0;
1632
2130
  for (const uniform of groupUniforms) {
1633
2131
  const fullUniform = shader.uniforms[uniform.name];
1634
2132
  if (!fullUniform || fullUniform.isSampler) continue;
1635
- const uniformData = fullUniform._mappedData;
1636
-
1637
- if (fullUniform.baseType === 'u32') {
1638
- if (fullUniform.size === 4) {
1639
- dataView.setUint32(offset + fullUniform.offset, uniformData, true);
1640
- } else {
1641
- for (let i = 0; i < uniformData.length; i++) {
1642
- dataView.setUint32(offset + fullUniform.offset + i * 4, uniformData[i], true);
1643
- }
1644
- }
1645
- } else if (fullUniform.baseType === 'i32') {
1646
- if (fullUniform.size === 4) {
1647
- dataView.setInt32(offset + fullUniform.offset, uniformData, true);
1648
- } else {
1649
- for (let i = 0; i < uniformData.length; i++) {
1650
- dataView.setInt32(offset + fullUniform.offset + i * 4, uniformData[i], true);
1651
- }
1652
- }
1653
- } else if (fullUniform.packInPlace) {
1654
- // In-place packing for mat3: write directly to buffer with padding
1655
- const baseOffset = (offset + fullUniform.offset) / 4;
1656
- // Column 0
1657
- data[baseOffset + 0] = uniformData[0];
1658
- data[baseOffset + 1] = uniformData[1];
1659
- data[baseOffset + 2] = uniformData[2];
1660
- // Column 1
1661
- data[baseOffset + 4] = uniformData[3];
1662
- data[baseOffset + 5] = uniformData[4];
1663
- data[baseOffset + 6] = uniformData[5];
1664
- // Column 2
1665
- data[baseOffset + 8] = uniformData[6];
1666
- data[baseOffset + 9] = uniformData[7];
1667
- data[baseOffset + 10] = uniformData[8];
1668
- } else if (fullUniform.size === 4) {
1669
- data.set([uniformData], (offset + fullUniform.offset) / 4);
1670
- } else if (uniformData !== undefined) {
1671
- data.set(uniformData, (offset + fullUniform.offset) / 4);
1672
- }
2133
+ this._packField(fullUniform, fullUniform._mappedData, data, dataView, offset);
1673
2134
  }
1674
2135
  }
1675
2136
 
@@ -1816,10 +2277,11 @@ function rendererWebGPU(p5, fn) {
1816
2277
  const uniformVarRegex = /@group\((\d+)\)\s+@binding\((\d+)\)\s+var<uniform>\s+(\w+)\s*:\s*(\w+);/g;
1817
2278
 
1818
2279
  let match;
1819
- while ((match = uniformVarRegex.exec(shader.vertSrc())) !== null) {
2280
+ const src = shader.shaderType === 'compute' ? shader.computeSrc() : shader.vertSrc();
2281
+ while ((match = uniformVarRegex.exec(src)) !== null) {
1820
2282
  const [_, groupNum, binding, varName, structType] = match;
1821
2283
  const bindingIndex = parseInt(binding);
1822
- const uniforms = this._parseStruct(shader.vertSrc(), structType);
2284
+ const uniforms = this._parseStruct(src, structType);
1823
2285
 
1824
2286
  uniformGroups.push({
1825
2287
  group: parseInt(groupNum),
@@ -1830,7 +2292,7 @@ function rendererWebGPU(p5, fn) {
1830
2292
  });
1831
2293
  }
1832
2294
 
1833
- if (uniformGroups.length === 0) {
2295
+ if (uniformGroups.length === 0 && shader.shaderType !== 'compute') {
1834
2296
  throw new Error('Expected at least one uniform struct bound to @group(0)');
1835
2297
  }
1836
2298
 
@@ -1857,6 +2319,10 @@ function rendererWebGPU(p5, fn) {
1857
2319
  // TODO: support other texture types
1858
2320
  const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d<f32>|sampler);/g;
1859
2321
 
2322
+ // Extract storage buffers
2323
+ const storageBuffers = {};
2324
+ const storageRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var<storage,\s*(read|read_write)>\s+(\w+)\s*:\s*array<\w+>/g;
2325
+
1860
2326
  // Track which bindings are taken by the struct properties we've parsed
1861
2327
  // (the rest should be textures/samplers)
1862
2328
  const structUniformBindings = {};
@@ -1866,8 +2332,11 @@ function rendererWebGPU(p5, fn) {
1866
2332
 
1867
2333
  for (const [src, visibility] of [
1868
2334
  [shader.vertSrc(), GPUShaderStage.VERTEX],
1869
- [shader.fragSrc(), GPUShaderStage.FRAGMENT]
2335
+ [shader.fragSrc(), GPUShaderStage.FRAGMENT],
2336
+ [shader.computeSrc ? shader.computeSrc() : null, GPUShaderStage.COMPUTE]
1870
2337
  ]) {
2338
+ if (!src) continue; // Skip if shader stage doesn't exist
2339
+
1871
2340
  let match;
1872
2341
  while ((match = samplerRegex.exec(src)) !== null) {
1873
2342
  const [_, group, binding, name, type] = match;
@@ -1902,21 +2371,51 @@ function rendererWebGPU(p5, fn) {
1902
2371
  samplerNode.textureSource = sampler;
1903
2372
  }
1904
2373
  }
2374
+
2375
+ // Parse storage buffers
2376
+ while ((match = storageRegex.exec(src)) !== null) {
2377
+ const [_, group, binding, accessMode, name] = match;
2378
+ const groupIndex = parseInt(group);
2379
+ const bindingIndex = parseInt(binding);
2380
+
2381
+ const key = `${groupIndex},${bindingIndex}`;
2382
+ const existing = storageBuffers[key];
2383
+ // If any stage uses read_write, the bind group layout must use read_write
2384
+ const finalAccessMode = (existing?.accessMode === 'read_write' || accessMode === 'read_write')
2385
+ ? 'read_write'
2386
+ : accessMode;
2387
+
2388
+ storageBuffers[key] = {
2389
+ visibility: (existing?.visibility || 0) | visibility,
2390
+ group: groupIndex,
2391
+ binding: bindingIndex,
2392
+ name,
2393
+ accessMode: finalAccessMode, // 'read' or 'read_write'
2394
+ isStorage: true,
2395
+ type: 'storage'
2396
+ };
2397
+ }
1905
2398
  }
1906
- return [...Object.values(allUniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers)];
2399
+
2400
+ // Store storage buffers on shader for later use
2401
+ shader._storageBuffers = Object.values(storageBuffers);
2402
+
2403
+ return [...Object.values(allUniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers), ...Object.values(storageBuffers)];
1907
2404
  }
1908
2405
 
1909
- getNextBindingIndex({ vert, frag }, group = 0) {
2406
+ getNextBindingIndex({ vert, frag, compute }, group = 0) {
1910
2407
  // Get the highest binding index in the specified group and return the next available
1911
- const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var(?:<uniform>)?\s+(\w+)\s*:\s*(texture_2d<f32>|sampler|uniform|\w+)/g;
2408
+ const bindingRegex = /@group\((\d+)\)\s*@binding\((\d+)\)/g;
1912
2409
  let maxBindingIndex = -1;
1913
2410
 
1914
- for (const [src, visibility] of [
1915
- [vert, GPUShaderStage.VERTEX],
1916
- [frag, GPUShaderStage.FRAGMENT]
1917
- ]) {
2411
+ const sources = [];
2412
+ if (vert) sources.push([vert, GPUShaderStage.VERTEX]);
2413
+ if (frag) sources.push([frag, GPUShaderStage.FRAGMENT]);
2414
+ if (compute) sources.push([compute, GPUShaderStage.COMPUTE]);
2415
+
2416
+ for (const [src, visibility] of sources) {
1918
2417
  let match;
1919
- while ((match = samplerRegex.exec(src)) !== null) {
2418
+ while ((match = bindingRegex.exec(src)) !== null) {
1920
2419
  const [_, groupIndex, bindingIndex] = match;
1921
2420
  if (parseInt(groupIndex) === group) {
1922
2421
  maxBindingIndex = Math.max(maxBindingIndex, parseInt(bindingIndex));
@@ -1931,7 +2430,7 @@ function rendererWebGPU(p5, fn) {
1931
2430
  if (uniform.isSampler) {
1932
2431
  uniform.texture =
1933
2432
  data instanceof Texture ? data : this.getTexture(data);
1934
- } else {
2433
+ } else if (!data?._isStorageBuffer) {
1935
2434
  uniform._mappedData = this._mapUniformData(uniform, uniform._cachedData);
1936
2435
  }
1937
2436
  shader.buffersDirty.add(uniform.group * 1000 + uniform.binding);
@@ -2038,7 +2537,7 @@ function rendererWebGPU(p5, fn) {
2038
2537
  rgb += components.emissive;
2039
2538
  return vec4<f32>(rgb, components.opacity);
2040
2539
  }`,
2041
- "vec4f getFinalColor": "(color: vec4<f32>) { return color; }",
2540
+ "vec4f getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
2042
2541
  "void afterFragment": "() {}",
2043
2542
  },
2044
2543
  }
@@ -2063,7 +2562,7 @@ function rendererWebGPU(p5, fn) {
2063
2562
  },
2064
2563
  fragment: {
2065
2564
  "void beforeFragment": "() {}",
2066
- "vec4<f32> getFinalColor": "(color: vec4<f32>) { return color; }",
2565
+ "vec4<f32> getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
2067
2566
  "void afterFragment": "() {}",
2068
2567
  },
2069
2568
  }
@@ -2089,7 +2588,7 @@ function rendererWebGPU(p5, fn) {
2089
2588
  fragment: {
2090
2589
  "void beforeFragment": "() {}",
2091
2590
  "Inputs getPixelInputs": "(inputs: Inputs) { return inputs; }",
2092
- "vec4<f32> getFinalColor": "(color: vec4<f32>) { return color; }",
2591
+ "vec4<f32> getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
2093
2592
  "bool shouldDiscard": "(outside: bool) { return outside; };",
2094
2593
  "void afterFragment": "() {}",
2095
2594
  },
@@ -2248,11 +2747,87 @@ function rendererWebGPU(p5, fn) {
2248
2747
  }
2249
2748
  );
2250
2749
 
2251
- let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment)\s*)?fn main[^{]+\{)/);
2252
- if (shaderType !== 'fragment') {
2253
- if (!main.match(/\@builtin\s*\(\s*instance_index\s*\)/)) {
2254
- main = main.replace(/\)\s*(->|\{)/, ', @builtin(instance_index) instanceID: u32) $1');
2750
+ let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment|compute)\s*(?:@workgroup_size\([^)]+\)\s*)?)?fn main[^{]+\{)/);
2751
+
2752
+ const getBuiltinParamName = (mainSrc, builtinName) => {
2753
+ const match = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`).exec(mainSrc);
2754
+ return match ? match[1] : null;
2755
+ };
2756
+
2757
+ const ensureBuiltinParam = (mainSrc, builtinName, fallbackName, typeName) => {
2758
+ const existingName = getBuiltinParamName(mainSrc, builtinName);
2759
+ if (existingName) {
2760
+ return { mainSrc, argName: existingName };
2255
2761
  }
2762
+
2763
+ const hasParams = /\(\s*\S/.test(mainSrc);
2764
+ const injectedMain = mainSrc.replace(
2765
+ /\)\s*(->|\{)/,
2766
+ `${hasParams ? ', ' : ''}@builtin(${builtinName}) ${fallbackName}: ${typeName}) $1`
2767
+ );
2768
+
2769
+ return { mainSrc: injectedMain, argName: fallbackName };
2770
+ };
2771
+
2772
+ const getMainStructParameter = (mainSrc) => {
2773
+ const match = /fn main\s*\(\s*(\w+)\s*:\s*(\w+)/.exec(mainSrc);
2774
+ if (!match) return null;
2775
+ return { inputName: match[1], structName: match[2] };
2776
+ };
2777
+
2778
+ const getStructBuiltinFieldName = (structName, builtinName) => {
2779
+ const structMatch = new RegExp(`struct\\s+${structName}\\s*\\{([^}]*)\\}`, 's').exec(preMain);
2780
+ if (!structMatch) return null;
2781
+ const fieldMatch = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`, 's').exec(structMatch[1]);
2782
+ return fieldMatch ? fieldMatch[1] : null;
2783
+ };
2784
+
2785
+ const appendHookParams = (params, additionalParams) => {
2786
+ if (additionalParams.length === 0) return params;
2787
+ const hasParams = !/^\(\s*\)$/.test(params);
2788
+ return `${params.slice(0, -1)}${hasParams ? ', ' : ''}${additionalParams.join(', ')})`;
2789
+ };
2790
+
2791
+ let hookExtraParams = [];
2792
+ let hookExtraArgs = [];
2793
+
2794
+ if (shaderType === 'vertex') {
2795
+ const ensuredInstance = ensureBuiltinParam(main, 'instance_index', 'instanceID', 'u32');
2796
+ main = ensuredInstance.mainSrc;
2797
+
2798
+ const ensuredVertex = ensureBuiltinParam(main, 'vertex_index', '_p5VertexId', 'u32');
2799
+ main = ensuredVertex.mainSrc;
2800
+
2801
+ hookExtraParams = ['instanceID: u32', '_p5VertexId: u32'];
2802
+ hookExtraArgs = [ensuredInstance.argName, ensuredVertex.argName];
2803
+ } else if (shaderType === 'fragment') {
2804
+ const directPositionArg = getBuiltinParamName(main, 'position');
2805
+ let fragmentPositionArg = directPositionArg;
2806
+
2807
+ if (!fragmentPositionArg) {
2808
+ const mainStructParam = getMainStructParameter(main);
2809
+ if (mainStructParam) {
2810
+ const positionField = getStructBuiltinFieldName(mainStructParam.structName, 'position');
2811
+ if (positionField) {
2812
+ fragmentPositionArg = `${mainStructParam.inputName}.${positionField}`;
2813
+ }
2814
+ }
2815
+ }
2816
+
2817
+ if (!fragmentPositionArg) {
2818
+ const ensuredPosition = ensureBuiltinParam(main, 'position', '_p5FragPos', 'vec4<f32>');
2819
+ main = ensuredPosition.mainSrc;
2820
+ fragmentPositionArg = ensuredPosition.argName;
2821
+ }
2822
+
2823
+ hookExtraParams = ['_p5FragPos: vec4<f32>'];
2824
+ hookExtraArgs = [fragmentPositionArg];
2825
+ } else if (shaderType === 'compute') {
2826
+ const ensuredGlobalId = ensureBuiltinParam(main, 'global_invocation_id', '_p5GlobalId', 'vec3<u32>');
2827
+ main = ensuredGlobalId.mainSrc;
2828
+
2829
+ hookExtraParams = ['_p5GlobalId: vec3<u32>'];
2830
+ hookExtraArgs = [ensuredGlobalId.argName];
2256
2831
  }
2257
2832
 
2258
2833
  // Inject hook uniforms as a separate struct at a new binding
@@ -2272,6 +2847,7 @@ function rendererWebGPU(p5, fn) {
2272
2847
  const nextBinding = this.getNextBindingIndex({
2273
2848
  vert: shaderType === 'vertex' ? preMain + (shader.hooks.vertex?.declarations ?? '') + shader.hooks.declarations : shader._vertSrc,
2274
2849
  frag: shaderType === 'fragment' ? preMain + (shader.hooks.fragment?.declarations ?? '') + shader.hooks.declarations : shader._fragSrc,
2850
+ compute: shaderType === 'compute' ? preMain + (shader.hooks.compute?.declarations ?? '') + shader.hooks.declarations : shader._computeSrc,
2275
2851
  }, 0);
2276
2852
 
2277
2853
  // Create HookUniforms struct and binding
@@ -2282,8 +2858,14 @@ ${hookUniformFields}}
2282
2858
 
2283
2859
  @group(0) @binding(${nextBinding}) var<uniform> hooks: HookUniforms;
2284
2860
  `;
2285
- // Insert before the first @group binding
2286
- preMain = preMain.replace(/(@group\(0\)\s+@binding)/, `${hookUniformsDecl}\n$1`);
2861
+ // Insert before the first @group binding, or at the end if there are none
2862
+ const replaced = preMain.replace(/(@group\(0\)\s+@binding)/, `${hookUniformsDecl}\n$1`);
2863
+ if (replaced === preMain) {
2864
+ // No @group bindings found in base shader, append to preMain
2865
+ preMain = preMain + '\n' + hookUniformsDecl;
2866
+ } else {
2867
+ preMain = replaced;
2868
+ }
2287
2869
  }
2288
2870
 
2289
2871
  // Handle varying variables by injecting them into VertexOutput and FragmentInput structs
@@ -2349,10 +2931,9 @@ ${hookUniformFields}}
2349
2931
  initStatements += ` ${varName} = INPUT_VAR.${varName};\n`;
2350
2932
  }
2351
2933
 
2352
- // Find the input parameter name from the main function signature (anchored to start)
2353
- const inputMatch = main.match(/fn main\s*\((\w+):\s*\w+\)/);
2354
- if (inputMatch) {
2355
- const inputVarName = inputMatch[1];
2934
+ const mainStructParam = getMainStructParameter(main);
2935
+ if (mainStructParam) {
2936
+ const inputVarName = mainStructParam.inputName;
2356
2937
  initStatements = initStatements.replace(/INPUT_VAR/g, inputVarName);
2357
2938
  // Insert after the main function parameter but before any other code (anchored to start)
2358
2939
  postMain = initStatements + postMain;
@@ -2360,12 +2941,56 @@ ${hookUniformFields}}
2360
2941
  }
2361
2942
  }
2362
2943
 
2944
+ // Handle instanceID varying for fragment access
2945
+ if (shader.hooks.instanceIDVarying) {
2946
+ const { name, declaration, source, interpolation } = shader.hooks.instanceIDVarying;
2947
+ const nextLocIndex = this._getNextAvailableLocation(preMain, shaderType);
2948
+ const interpAttr = interpolation ? ` @interpolate(${interpolation})` : '';
2949
+ const [varName, varType] = declaration.split(':').map(s => s.trim());
2950
+ const structMember = `@location(${nextLocIndex})${interpAttr} ${declaration},`;
2951
+
2952
+ if (shaderType === 'vertex') {
2953
+ // Inject into VertexOutput struct
2954
+ preMain = preMain.replace(
2955
+ /struct\s+VertexOutput\s+\{([^}]*)\}/,
2956
+ (match, body) => `struct VertexOutput {${body}\n${structMember}}`
2957
+ );
2958
+ // Add private global
2959
+ preMain += `var<private> ${declaration};\n`;
2960
+ // Assign from built-in instanceID at start of main()
2961
+ postMain = `\n ${varName} = ${source};\n` + postMain;
2962
+ // Copy to output struct before return
2963
+ const returnMatch = postMain.match(/return\s+(\w+)\s*;/);
2964
+ if (returnMatch) {
2965
+ const outputVarName = returnMatch[1];
2966
+ postMain = postMain.replace(
2967
+ /(return\s+\w+\s*;)/g,
2968
+ `${outputVarName}.${varName} = ${varName};\n $1`
2969
+ );
2970
+ }
2971
+ } else if (shaderType === 'fragment') {
2972
+ // Inject into FragmentInput struct
2973
+ preMain = preMain.replace(
2974
+ /struct\s+FragmentInput\s+\{([^}]*)\}/,
2975
+ (match, body) => `struct FragmentInput {${body}\n${structMember}}`
2976
+ );
2977
+ // Add private global
2978
+ preMain += `var<private> ${declaration};\n`;
2979
+ // Initialize from input struct at start of main()
2980
+ const mainStructParam = getMainStructParameter(main);
2981
+ if (mainStructParam) {
2982
+ const inputVarName = mainStructParam.inputName;
2983
+ postMain = `\n ${varName} = ${inputVarName}.${varName};\n` + postMain;
2984
+ }
2985
+ }
2986
+ }
2987
+
2363
2988
  let hooks = '';
2364
2989
  let defines = '';
2365
2990
  if (shader.hooks.declarations) {
2366
2991
  hooks += shader.hooks.declarations + '\n';
2367
2992
  }
2368
- if (shader.hooks[shaderType].declarations) {
2993
+ if (shader.hooks[shaderType] && shader.hooks[shaderType].declarations) {
2369
2994
  hooks += shader.hooks[shaderType].declarations + '\n';
2370
2995
  }
2371
2996
  for (const hookDef in shader.hooks.helpers) {
@@ -2389,11 +3014,7 @@ ${hookUniformFields}}
2389
3014
 
2390
3015
  let [_, params, body] = /^(\([^\)]*\))((?:.|\n)*)$/.exec(shader.hooks[shaderType][hookDef]);
2391
3016
 
2392
- if (shaderType !== 'fragment') {
2393
- // Splice the instance ID in as a final parameter to every WGSL hook function
2394
- let hasParams = !!params.match(/^\(\s*\S+.*\)$/);
2395
- params = params.slice(0, -1) + (hasParams ? ', ' : '') + 'instanceID: u32)';
2396
- }
3017
+ params = appendHookParams(params, hookExtraParams);
2397
3018
 
2398
3019
  if (hookType === 'void') {
2399
3020
  hooks += `fn HOOK_${hookName}${params}${body}\n`;
@@ -2402,40 +3023,45 @@ ${hookUniformFields}}
2402
3023
  }
2403
3024
  }
2404
3025
 
2405
- // Add the instance ID as a final parameter to each hook call
2406
- if (shaderType !== 'fragment') {
2407
- const addInstanceIDParam = (src) => {
2408
- let result = src;
2409
- let idx = 0;
2410
- let match;
2411
- do {
2412
- match = /HOOK_\w+\(/.exec(result.slice(idx));
2413
- if (match) {
2414
- idx += match.index + match[0].length - 1;
2415
- let nesting = 0;
2416
- let hasParams = false;
2417
- while (idx < result.length) {
2418
- if (result[idx] === '(') {
2419
- nesting++;
2420
- } else if (result[idx] === ')') {
2421
- nesting--;
2422
- } else if (result[idx].match(/\S/)) {
2423
- hasParams = true;
2424
- }
2425
- idx++;
2426
- if (nesting === 0) {
2427
- break;
2428
- }
3026
+ // Pass stage-specific builtins from main to each hook call.
3027
+ // Collect ALL HOOK_ calls (including nested ones) then insert
3028
+ // extra args from right to left so position shifts don't
3029
+ // invalidate earlier insertion points.
3030
+ if (hookExtraArgs.length > 0) {
3031
+ const addHookArgs = (src) => {
3032
+ const insertions = [];
3033
+ let searchIdx = 0;
3034
+ let m;
3035
+ while ((m = /HOOK_\w+\(/.exec(src.slice(searchIdx))) !== null) {
3036
+ const openParen = searchIdx + m.index + m[0].length - 1;
3037
+ let pos = openParen + 1;
3038
+ let nesting = 1;
3039
+ let hasParams = false;
3040
+ while (pos < src.length && nesting > 0) {
3041
+ if (src[pos] === '(') nesting++;
3042
+ else if (src[pos] === ')') {
3043
+ nesting--;
3044
+ if (nesting === 0) break;
3045
+ } else if (/\S/.test(src[pos])) {
3046
+ hasParams = true;
2429
3047
  }
2430
- const insertion = (hasParams ? ', ' : '') + 'instanceID';
2431
- result = result.slice(0, idx-1) + insertion + result.slice(idx-1);
2432
- idx += insertion.length;
3048
+ pos++;
2433
3049
  }
2434
- } while (match);
3050
+ insertions.push({ pos, hasParams });
3051
+ searchIdx = openParen + 1;
3052
+ }
3053
+
3054
+ insertions.sort((a, b) => b.pos - a.pos);
3055
+
3056
+ let result = src;
3057
+ for (const { pos, hasParams } of insertions) {
3058
+ const insertion = (hasParams ? ', ' : '') + hookExtraArgs.join(', ');
3059
+ result = result.slice(0, pos) + insertion + result.slice(pos);
3060
+ }
2435
3061
  return result;
2436
3062
  };
2437
- preMain = addInstanceIDParam(preMain);
2438
- postMain = addInstanceIDParam(postMain);
3063
+ preMain = addHookArgs(preMain);
3064
+ postMain = addHookArgs(postMain);
2439
3065
  }
2440
3066
 
2441
3067
  return preMain + '\n' + defines + hooks + main + postMain;
@@ -2494,6 +3120,10 @@ ${hookUniformFields}}
2494
3120
  body = shader.hooks.fragment[hookName];
2495
3121
  fullSrc = shader._fragSrc;
2496
3122
  }
3123
+ if (!body) {
3124
+ body = shader.hooks.compute[hookName];
3125
+ fullSrc = shader._computeSrc;
3126
+ }
2497
3127
  if (!body) {
2498
3128
  throw new Error(`Can't find hook ${hookName}!`);
2499
3129
  }
@@ -2625,7 +3255,7 @@ ${hookUniformFields}}
2625
3255
  }
2626
3256
 
2627
3257
  defaultFramebufferAntialias() {
2628
- return true;
3258
+ return this._pInst._webgpuAttributes?.antialias !== false;
2629
3259
  }
2630
3260
 
2631
3261
  supportsFramebufferAntialias() {
@@ -2818,6 +3448,267 @@ ${hookUniformFields}}
2818
3448
  };
2819
3449
  }
2820
3450
 
3451
+ // Maps a plain JS value to the WGSL type string that represents it in a struct.
3452
+ _jsValueToWgslType(value) {
3453
+ if (typeof value === 'number') return 'f32';
3454
+ // Duck typing instead of instanceof to avoid importing a separate
3455
+ // copy of the Color/Vector classes
3456
+ if (value?.isVector) {
3457
+ if (value.dimensions === 2) return 'vec2f';
3458
+ if (value.dimensions === 3) return 'vec3f';
3459
+ if (value.dimensions === 4) return 'vec4f';
3460
+ throw new Error(`Unsupported vector dimension ${value.dimensions} for struct storage field`);
3461
+ }
3462
+ if (value?.isColor) {
3463
+ return 'vec4f';
3464
+ }
3465
+ if (Array.isArray(value)) {
3466
+ if (value.length === 2) return 'vec2f';
3467
+ if (value.length === 3) return 'vec3f';
3468
+ if (value.length === 4) return 'vec4f';
3469
+ throw new Error(`Unsupported array length ${value.length} for struct storage field`);
3470
+ }
3471
+ throw new Error(`Unsupported value type ${typeof value} for struct storage field`);
3472
+ }
3473
+
3474
+ // Infers a struct schema from the first element of a struct array.
3475
+ //
3476
+ // Returns { fields, stride, structBody } where:
3477
+ // fields: field has the _packField interface (baseType, size, offset, packInPlace) plus:
3478
+ // name: string - JS property name
3479
+ // dim: number - float component count, used when creating StrandsNodes
3480
+ // structBody: everything inside the { ... } of a WGSL struct definition
3481
+ // stride: how many bytes are reserved for this struct in the buffer
3482
+ _inferStructSchema(firstElement) {
3483
+ const entries = Object.entries(firstElement);
3484
+
3485
+ if (!p5.disableFriendlyErrors) {
3486
+ for (const [name, value] of entries) {
3487
+ if (
3488
+ value !== null &&
3489
+ typeof value === 'object' &&
3490
+ !Array.isArray(value) &&
3491
+ // Duck typing instead of instanceof to avoid importing a separate
3492
+ // copy of the Color/Vector classes
3493
+ !value?.isVector &&
3494
+ !value?.isColor
3495
+ ) {
3496
+ p5._friendlyError(
3497
+ `The "${name}" property in your storage data contains a nested object. ` +
3498
+ `Make sure you only use properties with numbers, arrays of numbers, or p5.Vector.`,
3499
+ 'createStorage'
3500
+ );
3501
+ }
3502
+ }
3503
+ }
3504
+
3505
+ const fieldLines = entries.map(([name, value]) =>
3506
+ ` ${name}: ${this._jsValueToWgslType(value)},`
3507
+ ).join('\n');
3508
+ const structBody = `{\n${fieldLines}\n}`;
3509
+ const elements = this._parseStruct(`struct _Tmp ${structBody}`, '_Tmp');
3510
+
3511
+ let maxEnd = 0;
3512
+ let maxAlign = 1;
3513
+ const fields = entries.map(([name, value]) => {
3514
+ const el = elements[name];
3515
+ maxEnd = Math.max(maxEnd, el.offsetEnd);
3516
+ // Alignment for scalars/vectors: <=4 -> 4, <=8 -> 8, else 16
3517
+ const align = el.size <= 4 ? 4 : el.size <= 8 ? 8 : 16;
3518
+ maxAlign = Math.max(maxAlign, align);
3519
+ // Track original JS type for reconstruction during readback
3520
+ const kind = value?.isVector ? 'vector'
3521
+ : value?.isColor ? 'color'
3522
+ : undefined;
3523
+ return {
3524
+ name,
3525
+ baseType: el.baseType,
3526
+ size: el.size,
3527
+ offset: el.offset,
3528
+ packInPlace: el.packInPlace ?? false,
3529
+ dim: el.size / 4,
3530
+ kind,
3531
+ };
3532
+ });
3533
+
3534
+ const stride = Math.ceil(maxEnd / maxAlign) * maxAlign;
3535
+ return { fields, stride, structBody };
3536
+ }
3537
+
3538
+ // Packs an array of plain objects into a Float32Array using the given struct schema.
3539
+ // Reuses _packField so layout rules match uniform packing exactly.
3540
+ _packStructArray(data, schema) {
3541
+ const { fields, stride } = schema;
3542
+ const totalBytes = Math.max(data.length * stride, 16);
3543
+ const alignedBytes = Math.ceil(totalBytes / 16) * 16;
3544
+ const buffer = new ArrayBuffer(alignedBytes);
3545
+ const floatView = new Float32Array(buffer);
3546
+ const dataView = new DataView(buffer);
3547
+ for (let i = 0; i < data.length; i++) {
3548
+ const item = data[i];
3549
+ const baseOffset = i * stride;
3550
+ for (const field of fields) {
3551
+ this._packField(field, item[field.name], floatView, dataView, baseOffset);
3552
+ }
3553
+ }
3554
+ return floatView;
3555
+ }
3556
+
3557
+ // Inverse of _packStructArray reads packed buffer back into plain JS objects
3558
+ // using the same schema layout - fields, stride and offsets
3559
+ _unpackStructArray(floatView, schema) {
3560
+ const { fields, stride } = schema;
3561
+ const dataView = new DataView(floatView.buffer);
3562
+ const count = Math.floor(floatView.byteLength / stride);
3563
+ const result = [];
3564
+
3565
+ for (let i = 0; i < count; i++) {
3566
+ const item = {};
3567
+ const baseOffset = i * stride;
3568
+ for (const field of fields) {
3569
+ const byteOffset = baseOffset + field.offset;
3570
+ const n = field.size / 4;
3571
+
3572
+ if (field.baseType === 'u32') {
3573
+ if (n === 1) {
3574
+ item[field.name] = dataView.getUint32(byteOffset, true);
3575
+ } else {
3576
+ item[field.name] = Array.from({ length: n }, (_, j) =>
3577
+ dataView.getUint32(byteOffset + j * 4, true)
3578
+ );
3579
+ }
3580
+ } else if (field.baseType === 'i32') {
3581
+ if (n === 1) {
3582
+ item[field.name] = dataView.getInt32(byteOffset, true);
3583
+ } else {
3584
+ item[field.name] = Array.from({ length: n }, (_, j) =>
3585
+ dataView.getInt32(byteOffset + j * 4, true)
3586
+ );
3587
+ }
3588
+ } else {
3589
+ const idx = byteOffset / 4;
3590
+ if (n === 1) {
3591
+ item[field.name] = floatView[idx];
3592
+ } else {
3593
+ const values = Array.from(floatView.slice(idx, idx + n));
3594
+ if (field.kind === 'vector') {
3595
+ item[field.name] = this._pInst.createVector(...values);
3596
+ } else if (field.kind === 'color') {
3597
+ // Color was packed as normalized RGBA [0-1] via _getRGBA([1,1,1,1])
3598
+ // Scale back to the current colorMode range
3599
+ const maxes = this.states.colorMaxes[this.states.colorMode];
3600
+ item[field.name] = this._pInst.color(
3601
+ values[0] * maxes[0], values[1] * maxes[1],
3602
+ values[2] * maxes[2], values[3] * maxes[3]
3603
+ );
3604
+ } else {
3605
+ item[field.name] = values;
3606
+ }
3607
+ }
3608
+ }
3609
+ }
3610
+ result.push(item);
3611
+ }
3612
+
3613
+ return result;
3614
+ }
3615
+
3616
+ createStorage(dataOrCount) {
3617
+ const device = this.device;
3618
+
3619
+ // Struct array: an array of plain objects
3620
+ if (Array.isArray(dataOrCount) && dataOrCount.length > 0 &&
3621
+ typeof dataOrCount[0] === 'object' && !Array.isArray(dataOrCount[0])) {
3622
+ if (!p5.disableFriendlyErrors && dataOrCount.length > 1) {
3623
+ const firstKeys = Object.keys(dataOrCount[0]);
3624
+ let warned = false;
3625
+ for (let i = 1; i < dataOrCount.length; i++) {
3626
+ const el = dataOrCount[i];
3627
+ const elKeys = Object.keys(el);
3628
+ const sameKeys = firstKeys.length === elKeys.length &&
3629
+ firstKeys.every((k, j) => k === elKeys[j]);
3630
+ if (!sameKeys) {
3631
+ p5._friendlyError(
3632
+ `Element ${i} has different fields than element 0. ` +
3633
+ `All elements should have the same properties.`,
3634
+ 'createStorage'
3635
+ );
3636
+ break;
3637
+ }
3638
+ for (const key of firstKeys) {
3639
+ const firstType = this._jsValueToWgslType(dataOrCount[0][key]);
3640
+ const elType = this._jsValueToWgslType(el[key]);
3641
+ if (firstType !== elType) {
3642
+ p5._friendlyError(
3643
+ `The "${key}" property of element ${i} has type ${elType} ` +
3644
+ `but element 0 has type ${firstType}. Proporties should have the same type across all elements.`,
3645
+ 'createStorage'
3646
+ );
3647
+ warned = true;
3648
+ break;
3649
+ }
3650
+ }
3651
+ if (warned) break;
3652
+ }
3653
+ }
3654
+ const schema = this._inferStructSchema(dataOrCount[0]);
3655
+ const packed = this._packStructArray(dataOrCount, schema);
3656
+ const size = packed.byteLength;
3657
+ const buffer = device.createBuffer({
3658
+ size,
3659
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
3660
+ mappedAtCreation: true,
3661
+ });
3662
+ new Float32Array(buffer.getMappedRange()).set(packed);
3663
+ buffer.unmap();
3664
+ const storageBuffer = new StorageBuffer(buffer, size, this, schema);
3665
+ this._storageBuffers.add(storageBuffer);
3666
+ return storageBuffer;
3667
+ }
3668
+
3669
+ // Determine buffer size and initial data
3670
+ let size, initialData;
3671
+ if (typeof dataOrCount === 'number') {
3672
+ // createStorage(count) - zero-initialized
3673
+ size = dataOrCount * 4; // floats are 4 bytes
3674
+ initialData = new Float32Array(dataOrCount);
3675
+ } else {
3676
+ // createStorage(array) - from data
3677
+ if (dataOrCount instanceof Float32Array) {
3678
+ initialData = dataOrCount;
3679
+ } else if (Array.isArray(dataOrCount)) {
3680
+ initialData = new Float32Array(dataOrCount);
3681
+ } else {
3682
+ throw new Error('createStorage expects a number or array/Float32Array');
3683
+ }
3684
+ size = initialData.byteLength;
3685
+ }
3686
+
3687
+ // Align to 16 bytes (WGSL storage buffer alignment requirement)
3688
+ size = Math.ceil(size / 16) * 16;
3689
+
3690
+ // Create storage buffer with STORAGE | COPY_DST | COPY_SRC usage
3691
+ const buffer = device.createBuffer({
3692
+ size,
3693
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
3694
+ mappedAtCreation: initialData.length > 0
3695
+ });
3696
+
3697
+ // Write initial data if provided
3698
+ if (initialData.length > 0) {
3699
+ const mapping = new Float32Array(buffer.getMappedRange());
3700
+ mapping.set(initialData);
3701
+ buffer.unmap();
3702
+ }
3703
+
3704
+ const storageBuffer = new StorageBuffer(buffer, size, this);
3705
+
3706
+ // Track for cleanup
3707
+ this._storageBuffers.add(storageBuffer);
3708
+
3709
+ return storageBuffer;
3710
+ }
3711
+
2821
3712
  _getWebGPUColorFormat(framebuffer) {
2822
3713
  if (framebuffer.format === FLOAT) {
2823
3714
  return framebuffer.channels === RGBA ? 'rgba32float' : 'rgba32float';
@@ -3114,10 +4005,6 @@ ${hookUniformFields}}
3114
4005
  return super.filter(...args);
3115
4006
  }
3116
4007
 
3117
- getNoiseShaderSnippet() {
3118
- return noiseWGSL;
3119
- }
3120
-
3121
4008
 
3122
4009
  baseFilterShader() {
3123
4010
  if (!this._baseFilterShader) {
@@ -3141,6 +4028,21 @@ ${hookUniformFields}}
3141
4028
  return this._baseFilterShader;
3142
4029
  }
3143
4030
 
4031
+ baseComputeShader() {
4032
+ if (!this._baseComputeShader) {
4033
+ this._baseComputeShader = new Shader(
4034
+ this,
4035
+ baseComputeShader,
4036
+ {
4037
+ compute: {
4038
+ 'void iteration': '(index: vec3<i32>) {}',
4039
+ },
4040
+ }
4041
+ );
4042
+ }
4043
+ return this._baseComputeShader;
4044
+ }
4045
+
3144
4046
  /*
3145
4047
  * WebGPU-specific implementation of imageLight shader creation
3146
4048
  */
@@ -3240,6 +4142,69 @@ ${hookUniformFields}}
3240
4142
  glDataType: dataType || 'uint8'
3241
4143
  };
3242
4144
  }
4145
+
4146
+ compute(shader, x, y = 1, z = 1) {
4147
+ if (shader.shaderType !== 'compute') {
4148
+ throw new Error('compute() can only be called with a compute shader');
4149
+ }
4150
+
4151
+ this._finishActiveRenderPass();
4152
+
4153
+ // Ensure shader is initialized and finalized
4154
+ if (!shader._compiled) {
4155
+ shader.init();
4156
+ }
4157
+
4158
+ // Set default uniforms
4159
+ shader.setDefaultUniforms();
4160
+ shader.setUniform('uTotalCount', [x, y, z]);
4161
+
4162
+ // Calculate optimal workgroup size (8x8x1 = 64 threads per workgroup)
4163
+ const WORKGROUP_SIZE_X = 8;
4164
+ const WORKGROUP_SIZE_Y = 8;
4165
+ const WORKGROUP_SIZE_Z = 1;
4166
+
4167
+ // auto spreading: if any dimension is too large or for performance optimization,
4168
+ // spread total iteration count across dimensions
4169
+ const totalIterations = x * y * z;
4170
+ const MAX_THREADS_PER_DIM = 65535 * 8;
4171
+
4172
+ let px = x;
4173
+ let py = y;
4174
+ let pz = z;
4175
+
4176
+ // we spread if we exceed GPU limits OR if it involves a large 1D dispatch
4177
+ const exceedsLimits = x > MAX_THREADS_PER_DIM || y > MAX_THREADS_PER_DIM || z > MAX_THREADS_PER_DIM;
4178
+ const isLarge1D = totalIterations > 1024 && y === 1 && z === 1;
4179
+
4180
+ if (exceedsLimits || isLarge1D) {
4181
+ // Always use 2D square spreading (√N × √N).
4182
+ // Benchmarks showed 2D square equals or outperforms 3D cube at every
4183
+ // scale tested, with simpler index reconstruction in the shader.
4184
+ px = Math.ceil(Math.sqrt(totalIterations));
4185
+ py = Math.ceil(totalIterations / px);
4186
+ pz = 1;
4187
+ }
4188
+
4189
+ shader.setUniform('uPhysicalCount', [px, py, pz]);
4190
+
4191
+ const workgroupCountX = Math.ceil(px / WORKGROUP_SIZE_X);
4192
+ const workgroupCountY = Math.ceil(py / WORKGROUP_SIZE_Y);
4193
+ const workgroupCountZ = Math.ceil(pz / WORKGROUP_SIZE_Z);
4194
+
4195
+ const commandEncoder = this.device.createCommandEncoder();
4196
+ const passEncoder = commandEncoder.beginComputePass();
4197
+ this.setupShaderBindGroups(shader, passEncoder, {
4198
+ compute: true,
4199
+ workgroupSize: [WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y, WORKGROUP_SIZE_Z],
4200
+ });
4201
+
4202
+ // Dispatch compute workgroups
4203
+ passEncoder.dispatchWorkgroups(workgroupCountX, workgroupCountY, workgroupCountZ);
4204
+
4205
+ passEncoder.end();
4206
+ this.device.queue.submit([commandEncoder.finish()]);
4207
+ }
3243
4208
  }
3244
4209
 
3245
4210
  p5.RendererWebGPU = RendererWebGPU;
@@ -3250,6 +4215,7 @@ ${hookUniformFields}}
3250
4215
  fn.setAttributes = async function (key, value) {
3251
4216
  return this._renderer._setAttributes(key, value);
3252
4217
  };
4218
+
3253
4219
  }
3254
4220
 
3255
4221
  if (typeof p5 !== "undefined") {