p5 2.2.3 → 2.3.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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-CYF6mp5_.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-D2MtO721.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 +233 -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-C0Kzy71d.js} +31 -24
  72. package/dist/{rendering-CC8JNTwG.js → rendering-CvNr0bB8.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 +1036 -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 +4088 -1950
  128. package/lib/p5.esm.min.js +1 -1
  129. package/lib/p5.js +4088 -1950
  130. package/lib/p5.min.js +1 -1
  131. package/lib/p5.webgpu.esm.js +1638 -306
  132. package/lib/p5.webgpu.js +1637 -305
  133. package/lib/p5.webgpu.min.js +1 -1
  134. package/package.json +6 -1
  135. package/types/global.d.ts +4137 -2396
  136. package/types/p5.d.ts +2702 -1658
  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-CYF6mp5_.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,228 @@ 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
+ /**
260
+ * A block of data that shaders can read from, and compute shaders can also
261
+ * write to. This is only available in WebGPU mode.
262
+ *
263
+ * Note: <a href="#/p5/createStorage">`createStorage()`</a> is the recommended
264
+ * way to create an instance of this class.
265
+ *
266
+ * @class p5.StorageBuffer
267
+ * @beta
268
+ * @webgpu
269
+ * @webgpuOnly
270
+ */
271
+ p5.StorageBuffer = StorageBuffer;
272
+
40
273
  class RendererWebGPU extends Renderer3D {
41
274
  constructor(pInst, w, h, isMainCanvas, elt) {
42
275
  super(pInst, w, h, isMainCanvas, elt);
@@ -88,6 +321,9 @@ function rendererWebGPU(p5, fn) {
88
321
  // Retired buffers to destroy at end of frame
89
322
  this._retiredBuffers = [];
90
323
 
324
+ // Storage buffers for compute shaders
325
+ this._storageBuffers = new Set();
326
+
91
327
  // 2D canvas for pixel reading fallback
92
328
  this._pixelReadCanvas = null;
93
329
  this._pixelReadCtx = null;
@@ -164,7 +400,7 @@ function rendererWebGPU(p5, fn) {
164
400
  }
165
401
  if (this._pInst._webgpuAttributes[key] !== value) {
166
402
  //changing value of previously altered attribute
167
- this._webgpuAttributes[key] = value;
403
+ this._pInst._webgpuAttributes[key] = value;
168
404
  unchanged = false;
169
405
  }
170
406
  //setting all attributes with some change
@@ -298,9 +534,21 @@ function rendererWebGPU(p5, fn) {
298
534
  const _b = args[2] || 0;
299
535
  const _a = args[3] || 0;
300
536
 
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;
537
+ // If PENDING and no custom framebuffer, clear means stay UNPROMOTED.
538
+ // However, if we are still in setup (frameCount == 0), we must promote
539
+ // so that mainFramebuffer gets the cleared content. This ensures that if
540
+ // draw() later promotes without a copy, it starts from the correct state
541
+ // rather than a stale mainFramebuffer.
542
+ // Note: a mid-draw-loop transition from UNPROMOTED back to PROMOTED
543
+ // (i.e. calling background() some frames but not others) will still
544
+ // lose intermediate UNPROMOTED frame content.
545
+ if (this._frameState !== FRAME_STATE.PROMOTED && !this.activeFramebuffer()) {
546
+ if (this._pInst.frameCount > 0) {
547
+ this._frameState = FRAME_STATE.UNPROMOTED;
548
+ } else {
549
+ this._promoteToFramebufferWithoutCopy();
550
+ // clear() then targets mainFramebuffer via activeFramebuffer()
551
+ }
304
552
  }
305
553
 
306
554
  this._finishActiveRenderPass();
@@ -503,7 +751,8 @@ function rendererWebGPU(p5, fn) {
503
751
  return 4; // Cap at 4 for broader compatibility
504
752
  }
505
753
 
506
- _shaderOptions({ mode }) {
754
+ _shaderOptions({ mode, compute, workgroupSize }) {
755
+ if (compute) return { compute: true, workgroupSize };
507
756
  const activeFramebuffer = this.activeFramebuffer();
508
757
  const format = activeFramebuffer ?
509
758
  this._getWebGPUColorFormat(activeFramebuffer) :
@@ -514,9 +763,9 @@ function rendererWebGPU(p5, fn) {
514
763
  1; // No MSAA needed when blitting already-antialiased textures to canvas
515
764
  const sampleCount = this._getValidSampleCount(requestedSampleCount);
516
765
 
517
- const depthFormat = activeFramebuffer && activeFramebuffer.useDepth ?
518
- this._getWebGPUDepthFormat(activeFramebuffer) :
519
- this.depthFormat;
766
+ const depthFormat = activeFramebuffer
767
+ ? (activeFramebuffer.useDepth ? this._getWebGPUDepthFormat(activeFramebuffer) : undefined)
768
+ : this.depthFormat;
520
769
 
521
770
  const drawTarget = this.drawTarget();
522
771
  const clipping = this._clipping;
@@ -544,6 +793,31 @@ function rendererWebGPU(p5, fn) {
544
793
  _initShader(shader) {
545
794
  const device = this.device;
546
795
 
796
+ if (shader.shaderType === 'compute') {
797
+ // Compute shader initialization
798
+ shader.computeModule = device.createShaderModule({ code: shader.computeSrc() });
799
+ shader._computePipelineCache = null;
800
+ shader._workgroupSize = null;
801
+
802
+ // Create compute pipeline (deferred until first compute() call)
803
+ shader.getPipeline = ({ workgroupSize }) => {
804
+ if (!shader._computePipelineCache) {
805
+ shader._computePipelineCache = device.createComputePipeline({
806
+ layout: shader._pipelineLayout,
807
+ compute: {
808
+ module: shader.computeModule,
809
+ entryPoint: 'main'
810
+ }
811
+ });
812
+ shader._workgroupSize = workgroupSize;
813
+ }
814
+ return shader._computePipelineCache;
815
+ };
816
+
817
+ return;
818
+ }
819
+
820
+ // Render shader initialization
547
821
  shader.vertModule = device.createShaderModule({ code: shader.vertSrc() });
548
822
  shader.fragModule = device.createShaderModule({ code: shader.fragSrc() });
549
823
 
@@ -568,25 +842,27 @@ function rendererWebGPU(p5, fn) {
568
842
  },
569
843
  primitive: { topology },
570
844
  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',
845
+ ...(depthFormat ? {
846
+ depthStencil: {
847
+ format: depthFormat,
848
+ depthWriteEnabled: !clipping,
849
+ depthCompare: 'less-equal',
850
+ stencilFront: {
851
+ compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
852
+ failOp: 'keep',
853
+ depthFailOp: 'keep',
854
+ passOp: clipping ? 'replace' : 'keep',
855
+ },
856
+ stencilBack: {
857
+ compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
858
+ failOp: 'keep',
859
+ depthFailOp: 'keep',
860
+ passOp: clipping ? 'replace' : 'keep',
861
+ },
862
+ stencilReadMask: 0xFF,
863
+ stencilWriteMask: clipping ? 0xFF : 0x00,
586
864
  },
587
- stencilReadMask: 0xFF,
588
- stencilWriteMask: clipping ? 0xFF : 0x00,
589
- },
865
+ } : {}),
590
866
  });
591
867
  shader._pipelineCache.set(key, pipeline);
592
868
  }
@@ -642,7 +918,9 @@ function rendererWebGPU(p5, fn) {
642
918
  entries.push({
643
919
  bufferGroup,
644
920
  binding: bufferGroup.binding,
645
- visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
921
+ visibility: shader.shaderType === 'compute'
922
+ ? GPUShaderStage.COMPUTE
923
+ : GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
646
924
  buffer: { type: 'uniform', hasDynamicOffset: bufferGroup.dynamic },
647
925
  });
648
926
  structEntries.set(bufferGroup.group, entries);
@@ -676,6 +954,24 @@ function rendererWebGPU(p5, fn) {
676
954
  groupEntries.set(group, entries);
677
955
  }
678
956
 
957
+ // Add storage buffer bindings
958
+ for (const storage of shader._storageBuffers || []) {
959
+ const group = storage.group;
960
+ const entries = groupEntries.get(group) || [];
961
+
962
+ entries.push({
963
+ binding: storage.binding,
964
+ visibility: storage.visibility,
965
+ buffer: {
966
+ type: storage.accessMode === 'read' ? 'read-only-storage' : 'storage'
967
+ },
968
+ storage: storage,
969
+ });
970
+
971
+ entries.sort((a, b) => a.binding - b.binding);
972
+ groupEntries.set(group, entries);
973
+ }
974
+
679
975
  // Create layouts and bind groups
680
976
  const groupEntriesArr = [];
681
977
  for (const [group, entries] of groupEntries) {
@@ -694,6 +990,7 @@ function rendererWebGPU(p5, fn) {
694
990
  shader._pipelineLayout = this.device.createPipelineLayout({
695
991
  bindGroupLayouts: shader._bindGroupLayouts,
696
992
  });
993
+ shader._compiled = true;
697
994
  }
698
995
 
699
996
  _getBlendState(mode) {
@@ -940,8 +1237,11 @@ function rendererWebGPU(p5, fn) {
940
1237
 
941
1238
  _resetBuffersBeforeDraw() {
942
1239
  this._finishActiveRenderPass();
1240
+
943
1241
  // Set state to PENDING - we'll decide on first draw
944
- this._frameState = FRAME_STATE.PENDING;
1242
+ if (this._pInst.frameCount > 0) {
1243
+ this._frameState = FRAME_STATE.PENDING;
1244
+ }
945
1245
 
946
1246
  // Clear depth buffer but DON'T start any render pass yet
947
1247
  const activeFramebuffer = this.activeFramebuffer();
@@ -1052,6 +1352,8 @@ function rendererWebGPU(p5, fn) {
1052
1352
  // once we're drawing to the framebuffer, because normally
1053
1353
  // those are reset.
1054
1354
  const savedModelMatrix = this.states.uModelMatrix.copy();
1355
+ this.states.uModelMatrix.set(this.states.uModelMatrix.copy());
1356
+ this.states.uModelMatrix.reset();
1055
1357
  this.mainFramebuffer.defaultCamera.set(this.states.curCamera);
1056
1358
 
1057
1359
  this.mainFramebuffer.begin();
@@ -1060,6 +1362,11 @@ function rendererWebGPU(p5, fn) {
1060
1362
  }
1061
1363
 
1062
1364
  _promoteToFramebufferWithoutCopy() {
1365
+ // Already promoted this frame
1366
+ if (this._frameState === FRAME_STATE.PROMOTED) {
1367
+ return;
1368
+ }
1369
+
1063
1370
  // Ensure mainFramebuffer matches canvas size
1064
1371
  if (this.mainFramebuffer.width !== this.width ||
1065
1372
  this.mainFramebuffer.height !== this.height) {
@@ -1074,6 +1381,8 @@ function rendererWebGPU(p5, fn) {
1074
1381
 
1075
1382
  // Preserve transformation state
1076
1383
  const savedModelMatrix = this.states.uModelMatrix.copy();
1384
+ this.states.uModelMatrix.set(this.states.uModelMatrix.copy());
1385
+ this.states.uModelMatrix.reset();
1077
1386
  this.mainFramebuffer.defaultCamera.set(this.states.curCamera);
1078
1387
 
1079
1388
  // Begin rendering to mainFramebuffer
@@ -1387,7 +1696,6 @@ function rendererWebGPU(p5, fn) {
1387
1696
  }
1388
1697
  this.flushDraw();
1389
1698
 
1390
- // this._pInst.background('red');
1391
1699
  this._pInst.push();
1392
1700
  this.states.setValue('enableLighting', false);
1393
1701
  this.states.setValue('activeImageLight', null);
@@ -1452,25 +1760,9 @@ function rendererWebGPU(p5, fn) {
1452
1760
 
1453
1761
  this._beginActiveRenderPass();
1454
1762
  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
1763
 
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
- }
1764
+ const currentShader = this._curShader;
1765
+ this.setupShaderBindGroups(currentShader, passEncoder, { mode, buffers });
1474
1766
  // Bind vertex buffers
1475
1767
  for (const buffer of currentShader._vertexBuffers || this._getVertexBuffers(currentShader)) {
1476
1768
  const location = currentShader.attributes[buffer.attr].location;
@@ -1478,6 +1770,58 @@ function rendererWebGPU(p5, fn) {
1478
1770
  passEncoder.setVertexBuffer(location, gpuBuffer, 0);
1479
1771
  }
1480
1772
 
1773
+ if (currentShader.shaderType === "fill") {
1774
+ // Bind index buffer and issue draw
1775
+ if (buffers.indexBuffer) {
1776
+ const indexFormat = buffers.indexFormat || "uint16";
1777
+ passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
1778
+ passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
1779
+ } else {
1780
+ passEncoder.draw(geometry.vertices.length, count, 0, 0);
1781
+ }
1782
+ } else if (currentShader.shaderType === "text") {
1783
+ if (!buffers.indexBuffer) {
1784
+ throw new Error("Text geometry must have an index buffer");
1785
+ }
1786
+ const indexFormat = buffers.indexFormat || "uint16";
1787
+ passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
1788
+ passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
1789
+ }
1790
+
1791
+ if (buffers.lineVerticesBuffer && currentShader.shaderType === "stroke") {
1792
+ passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0);
1793
+ }
1794
+
1795
+ // Mark that we have pending draws that need submission
1796
+ this._hasPendingDraws = true;
1797
+ }
1798
+
1799
+ setupShaderBindGroups(currentShader, passEncoder, shaderOptionsParams) {
1800
+ const shaderOptions = this._shaderOptions(shaderOptionsParams);
1801
+ if (
1802
+ shaderOptions.compute ||
1803
+ this.activeShader !== currentShader ||
1804
+ this._shaderOptionsDifferent(shaderOptions)
1805
+ ) {
1806
+ passEncoder.setPipeline(currentShader.getPipeline(shaderOptions));
1807
+ }
1808
+ if (!shaderOptions.compute) {
1809
+ this.activeShader = currentShader;
1810
+ this.activeShaderOptions = shaderOptions;
1811
+
1812
+ // Set stencil reference value for clipping
1813
+ const drawTarget = this.drawTarget();
1814
+ if (drawTarget._isClipApplied && !this._clipping) {
1815
+ // When using the clip mask, test against reference value 0 (background)
1816
+ // WebGL uses NOTEQUAL with ref 0, so fragments pass where stencil != 0
1817
+ // In WebGPU with 'not-equal', we need ref 0 to pass where stencil != 0
1818
+ passEncoder.setStencilReference(0);
1819
+ } else if (this._clipping) {
1820
+ // When writing to the clip mask, write reference value 1
1821
+ passEncoder.setStencilReference(1);
1822
+ }
1823
+ }
1824
+
1481
1825
  for (const bufferGroup of currentShader._uniformBufferGroups) {
1482
1826
  if (bufferGroup.dynamic) {
1483
1827
  // Bind uniforms into a part of a big dynamic memory block because
@@ -1530,6 +1874,13 @@ function rendererWebGPU(p5, fn) {
1530
1874
  currentShader.buffersDirty.delete(key);
1531
1875
  }
1532
1876
  }
1877
+ for (const storage of currentShader._storageBuffers || []) {
1878
+ const key = storage.group * 1000 + storage.binding;
1879
+ if (currentShader.buffersDirty.has(key)) {
1880
+ currentShader._cachedBindGroup[storage.group] = undefined;
1881
+ currentShader.buffersDirty.delete(key);
1882
+ }
1883
+ }
1533
1884
 
1534
1885
  // Bind sampler/texture uniforms and uniform buffers
1535
1886
  for (const iter of currentShader._groupEntries) {
@@ -1559,6 +1910,19 @@ function rendererWebGPU(p5, fn) {
1559
1910
  : { buffer: uniformBufferInfo.buffer },
1560
1911
  });
1561
1912
  }
1913
+ } else if (entry.storage && !bindGroup) {
1914
+ // Storage buffer binding
1915
+ const uniform = currentShader.uniforms[entry.storage.name];
1916
+ if (!uniform || !uniform._cachedData || !uniform._cachedData._isStorageBuffer) {
1917
+ throw new Error(
1918
+ `Storage buffer "${entry.storage.name}" not set. ` +
1919
+ `Use shader.setUniform("${entry.storage.name}", storageBuffer)`
1920
+ );
1921
+ }
1922
+ bgEntries.push({
1923
+ binding: entry.binding,
1924
+ resource: { buffer: uniform._cachedData.buffer },
1925
+ });
1562
1926
  } else if (!bindGroup) {
1563
1927
  bgEntries.push({
1564
1928
  binding: entry.binding,
@@ -1592,84 +1956,71 @@ function rendererWebGPU(p5, fn) {
1592
1956
  );
1593
1957
  }
1594
1958
  }
1959
+ return passEncoder;
1960
+ }
1595
1961
 
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);
1962
+ //////////////////////////////////////////////
1963
+ // SHADER
1964
+ //////////////////////////////////////////////
1965
+
1966
+ // Writes a single field's value into a Float32Array+DataView at (baseOffset + field.offset).
1967
+ //
1968
+ // Field interface (shared by uniform fields from _parseStruct and struct storage schema fields):
1969
+ // baseType: string - 'f32', 'i32', 'u32', etc.
1970
+ // size: number - byte size of the field
1971
+ // offset: number - byte offset of the field within its struct
1972
+ // packInPlace: bool - true for mat3, written with manual column padding
1973
+ //
1974
+ // value: number or number[] - the data to write
1975
+ _packField(field, value, floatView, dataView, baseOffset) {
1976
+ if (value === undefined) return;
1977
+
1978
+ // Duck typing instead of instanceof to avoid importing a separate
1979
+ // copy of the Color/Vector classes
1980
+ if (value?.isVector) {
1981
+ value = value.values.length !== value.dimensions ? value.values.slice(0, value.dimensions) : value.values;
1982
+ } else if (value?.isColor) {
1983
+ value = value._getRGBA([1, 1, 1, 1]);
1984
+ }
1985
+ const byteOffset = baseOffset + field.offset;
1986
+ if (field.baseType === 'u32') {
1987
+ if (field.size === 4) {
1988
+ dataView.setUint32(byteOffset, value, true);
1602
1989
  } else {
1603
- passEncoder.draw(geometry.vertices.length, count, 0, 0);
1990
+ for (let i = 0; i < value.length; i++) {
1991
+ dataView.setUint32(byteOffset + i * 4, value[i], true);
1992
+ }
1604
1993
  }
1605
- } else if (currentShader.shaderType === "text") {
1606
- if (!buffers.indexBuffer) {
1607
- throw new Error("Text geometry must have an index buffer");
1994
+ } else if (field.baseType === 'i32') {
1995
+ if (field.size === 4) {
1996
+ dataView.setInt32(byteOffset, value, true);
1997
+ } else {
1998
+ for (let i = 0; i < value.length; i++) {
1999
+ dataView.setInt32(byteOffset + i * 4, value[i], true);
2000
+ }
1608
2001
  }
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);
2002
+ } else if (field.packInPlace) {
2003
+ // In-place packing for mat3: write directly to buffer with padding
2004
+ const base = byteOffset / 4;
2005
+ floatView[base + 0] = value[0]; floatView[base + 1] = value[1]; floatView[base + 2] = value[2];
2006
+ floatView[base + 4] = value[3]; floatView[base + 5] = value[4]; floatView[base + 6] = value[5];
2007
+ floatView[base + 8] = value[6]; floatView[base + 9] = value[7]; floatView[base + 10] = value[8];
2008
+ } else if (field.size === 4) {
2009
+ floatView.set([value], byteOffset / 4);
2010
+ } else {
2011
+ floatView.set(value, byteOffset / 4);
1616
2012
  }
1617
-
1618
- // Mark that we have pending draws that need submission
1619
- this._hasPendingDraws = true;
1620
2013
  }
1621
2014
 
1622
- //////////////////////////////////////////////
1623
- // SHADER
1624
- //////////////////////////////////////////////
1625
-
1626
2015
  _packUniformGroup(shader, groupUniforms, bufferInfo) {
1627
2016
  // Pack a single group's uniforms into a buffer
1628
2017
  const data = bufferInfo.data;
1629
2018
  const dataView = bufferInfo.dataView;
1630
-
1631
2019
  const offset = bufferInfo.offset || 0;
1632
2020
  for (const uniform of groupUniforms) {
1633
2021
  const fullUniform = shader.uniforms[uniform.name];
1634
2022
  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
- }
2023
+ this._packField(fullUniform, fullUniform._mappedData, data, dataView, offset);
1673
2024
  }
1674
2025
  }
1675
2026
 
@@ -1816,10 +2167,11 @@ function rendererWebGPU(p5, fn) {
1816
2167
  const uniformVarRegex = /@group\((\d+)\)\s+@binding\((\d+)\)\s+var<uniform>\s+(\w+)\s*:\s*(\w+);/g;
1817
2168
 
1818
2169
  let match;
1819
- while ((match = uniformVarRegex.exec(shader.vertSrc())) !== null) {
2170
+ const src = shader.shaderType === 'compute' ? shader.computeSrc() : shader.vertSrc();
2171
+ while ((match = uniformVarRegex.exec(src)) !== null) {
1820
2172
  const [_, groupNum, binding, varName, structType] = match;
1821
2173
  const bindingIndex = parseInt(binding);
1822
- const uniforms = this._parseStruct(shader.vertSrc(), structType);
2174
+ const uniforms = this._parseStruct(src, structType);
1823
2175
 
1824
2176
  uniformGroups.push({
1825
2177
  group: parseInt(groupNum),
@@ -1830,7 +2182,7 @@ function rendererWebGPU(p5, fn) {
1830
2182
  });
1831
2183
  }
1832
2184
 
1833
- if (uniformGroups.length === 0) {
2185
+ if (uniformGroups.length === 0 && shader.shaderType !== 'compute') {
1834
2186
  throw new Error('Expected at least one uniform struct bound to @group(0)');
1835
2187
  }
1836
2188
 
@@ -1857,6 +2209,10 @@ function rendererWebGPU(p5, fn) {
1857
2209
  // TODO: support other texture types
1858
2210
  const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d<f32>|sampler);/g;
1859
2211
 
2212
+ // Extract storage buffers
2213
+ const storageBuffers = {};
2214
+ const storageRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var<storage,\s*(read|read_write)>\s+(\w+)\s*:\s*array<\w+>/g;
2215
+
1860
2216
  // Track which bindings are taken by the struct properties we've parsed
1861
2217
  // (the rest should be textures/samplers)
1862
2218
  const structUniformBindings = {};
@@ -1866,8 +2222,11 @@ function rendererWebGPU(p5, fn) {
1866
2222
 
1867
2223
  for (const [src, visibility] of [
1868
2224
  [shader.vertSrc(), GPUShaderStage.VERTEX],
1869
- [shader.fragSrc(), GPUShaderStage.FRAGMENT]
2225
+ [shader.fragSrc(), GPUShaderStage.FRAGMENT],
2226
+ [shader.computeSrc ? shader.computeSrc() : null, GPUShaderStage.COMPUTE]
1870
2227
  ]) {
2228
+ if (!src) continue; // Skip if shader stage doesn't exist
2229
+
1871
2230
  let match;
1872
2231
  while ((match = samplerRegex.exec(src)) !== null) {
1873
2232
  const [_, group, binding, name, type] = match;
@@ -1902,21 +2261,51 @@ function rendererWebGPU(p5, fn) {
1902
2261
  samplerNode.textureSource = sampler;
1903
2262
  }
1904
2263
  }
2264
+
2265
+ // Parse storage buffers
2266
+ while ((match = storageRegex.exec(src)) !== null) {
2267
+ const [_, group, binding, accessMode, name] = match;
2268
+ const groupIndex = parseInt(group);
2269
+ const bindingIndex = parseInt(binding);
2270
+
2271
+ const key = `${groupIndex},${bindingIndex}`;
2272
+ const existing = storageBuffers[key];
2273
+ // If any stage uses read_write, the bind group layout must use read_write
2274
+ const finalAccessMode = (existing?.accessMode === 'read_write' || accessMode === 'read_write')
2275
+ ? 'read_write'
2276
+ : accessMode;
2277
+
2278
+ storageBuffers[key] = {
2279
+ visibility: (existing?.visibility || 0) | visibility,
2280
+ group: groupIndex,
2281
+ binding: bindingIndex,
2282
+ name,
2283
+ accessMode: finalAccessMode, // 'read' or 'read_write'
2284
+ isStorage: true,
2285
+ type: 'storage'
2286
+ };
2287
+ }
1905
2288
  }
1906
- return [...Object.values(allUniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers)];
2289
+
2290
+ // Store storage buffers on shader for later use
2291
+ shader._storageBuffers = Object.values(storageBuffers);
2292
+
2293
+ return [...Object.values(allUniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers), ...Object.values(storageBuffers)];
1907
2294
  }
1908
2295
 
1909
- getNextBindingIndex({ vert, frag }, group = 0) {
2296
+ getNextBindingIndex({ vert, frag, compute }, group = 0) {
1910
2297
  // 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;
2298
+ const bindingRegex = /@group\((\d+)\)\s*@binding\((\d+)\)/g;
1912
2299
  let maxBindingIndex = -1;
1913
2300
 
1914
- for (const [src, visibility] of [
1915
- [vert, GPUShaderStage.VERTEX],
1916
- [frag, GPUShaderStage.FRAGMENT]
1917
- ]) {
2301
+ const sources = [];
2302
+ if (vert) sources.push([vert, GPUShaderStage.VERTEX]);
2303
+ if (frag) sources.push([frag, GPUShaderStage.FRAGMENT]);
2304
+ if (compute) sources.push([compute, GPUShaderStage.COMPUTE]);
2305
+
2306
+ for (const [src, visibility] of sources) {
1918
2307
  let match;
1919
- while ((match = samplerRegex.exec(src)) !== null) {
2308
+ while ((match = bindingRegex.exec(src)) !== null) {
1920
2309
  const [_, groupIndex, bindingIndex] = match;
1921
2310
  if (parseInt(groupIndex) === group) {
1922
2311
  maxBindingIndex = Math.max(maxBindingIndex, parseInt(bindingIndex));
@@ -1931,7 +2320,7 @@ function rendererWebGPU(p5, fn) {
1931
2320
  if (uniform.isSampler) {
1932
2321
  uniform.texture =
1933
2322
  data instanceof Texture ? data : this.getTexture(data);
1934
- } else {
2323
+ } else if (!data?._isStorageBuffer) {
1935
2324
  uniform._mappedData = this._mapUniformData(uniform, uniform._cachedData);
1936
2325
  }
1937
2326
  shader.buffersDirty.add(uniform.group * 1000 + uniform.binding);
@@ -2038,7 +2427,7 @@ function rendererWebGPU(p5, fn) {
2038
2427
  rgb += components.emissive;
2039
2428
  return vec4<f32>(rgb, components.opacity);
2040
2429
  }`,
2041
- "vec4f getFinalColor": "(color: vec4<f32>) { return color; }",
2430
+ "vec4f getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
2042
2431
  "void afterFragment": "() {}",
2043
2432
  },
2044
2433
  }
@@ -2063,7 +2452,7 @@ function rendererWebGPU(p5, fn) {
2063
2452
  },
2064
2453
  fragment: {
2065
2454
  "void beforeFragment": "() {}",
2066
- "vec4<f32> getFinalColor": "(color: vec4<f32>) { return color; }",
2455
+ "vec4<f32> getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
2067
2456
  "void afterFragment": "() {}",
2068
2457
  },
2069
2458
  }
@@ -2089,7 +2478,7 @@ function rendererWebGPU(p5, fn) {
2089
2478
  fragment: {
2090
2479
  "void beforeFragment": "() {}",
2091
2480
  "Inputs getPixelInputs": "(inputs: Inputs) { return inputs; }",
2092
- "vec4<f32> getFinalColor": "(color: vec4<f32>) { return color; }",
2481
+ "vec4<f32> getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
2093
2482
  "bool shouldDiscard": "(outside: bool) { return outside; };",
2094
2483
  "void afterFragment": "() {}",
2095
2484
  },
@@ -2248,11 +2637,87 @@ function rendererWebGPU(p5, fn) {
2248
2637
  }
2249
2638
  );
2250
2639
 
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');
2640
+ let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment|compute)\s*(?:@workgroup_size\([^)]+\)\s*)?)?fn main[^{]+\{)/);
2641
+
2642
+ const getBuiltinParamName = (mainSrc, builtinName) => {
2643
+ const match = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`).exec(mainSrc);
2644
+ return match ? match[1] : null;
2645
+ };
2646
+
2647
+ const ensureBuiltinParam = (mainSrc, builtinName, fallbackName, typeName) => {
2648
+ const existingName = getBuiltinParamName(mainSrc, builtinName);
2649
+ if (existingName) {
2650
+ return { mainSrc, argName: existingName };
2651
+ }
2652
+
2653
+ const hasParams = /\(\s*\S/.test(mainSrc);
2654
+ const injectedMain = mainSrc.replace(
2655
+ /\)\s*(->|\{)/,
2656
+ `${hasParams ? ', ' : ''}@builtin(${builtinName}) ${fallbackName}: ${typeName}) $1`
2657
+ );
2658
+
2659
+ return { mainSrc: injectedMain, argName: fallbackName };
2660
+ };
2661
+
2662
+ const getMainStructParameter = (mainSrc) => {
2663
+ const match = /fn main\s*\(\s*(\w+)\s*:\s*(\w+)/.exec(mainSrc);
2664
+ if (!match) return null;
2665
+ return { inputName: match[1], structName: match[2] };
2666
+ };
2667
+
2668
+ const getStructBuiltinFieldName = (structName, builtinName) => {
2669
+ const structMatch = new RegExp(`struct\\s+${structName}\\s*\\{([^}]*)\\}`, 's').exec(preMain);
2670
+ if (!structMatch) return null;
2671
+ const fieldMatch = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`, 's').exec(structMatch[1]);
2672
+ return fieldMatch ? fieldMatch[1] : null;
2673
+ };
2674
+
2675
+ const appendHookParams = (params, additionalParams) => {
2676
+ if (additionalParams.length === 0) return params;
2677
+ const hasParams = !/^\(\s*\)$/.test(params);
2678
+ return `${params.slice(0, -1)}${hasParams ? ', ' : ''}${additionalParams.join(', ')})`;
2679
+ };
2680
+
2681
+ let hookExtraParams = [];
2682
+ let hookExtraArgs = [];
2683
+
2684
+ if (shaderType === 'vertex') {
2685
+ const ensuredInstance = ensureBuiltinParam(main, 'instance_index', 'instanceID', 'u32');
2686
+ main = ensuredInstance.mainSrc;
2687
+
2688
+ const ensuredVertex = ensureBuiltinParam(main, 'vertex_index', '_p5VertexId', 'u32');
2689
+ main = ensuredVertex.mainSrc;
2690
+
2691
+ hookExtraParams = ['instanceID: u32', '_p5VertexId: u32'];
2692
+ hookExtraArgs = [ensuredInstance.argName, ensuredVertex.argName];
2693
+ } else if (shaderType === 'fragment') {
2694
+ const directPositionArg = getBuiltinParamName(main, 'position');
2695
+ let fragmentPositionArg = directPositionArg;
2696
+
2697
+ if (!fragmentPositionArg) {
2698
+ const mainStructParam = getMainStructParameter(main);
2699
+ if (mainStructParam) {
2700
+ const positionField = getStructBuiltinFieldName(mainStructParam.structName, 'position');
2701
+ if (positionField) {
2702
+ fragmentPositionArg = `${mainStructParam.inputName}.${positionField}`;
2703
+ }
2704
+ }
2255
2705
  }
2706
+
2707
+ if (!fragmentPositionArg) {
2708
+ const ensuredPosition = ensureBuiltinParam(main, 'position', '_p5FragPos', 'vec4<f32>');
2709
+ main = ensuredPosition.mainSrc;
2710
+ fragmentPositionArg = ensuredPosition.argName;
2711
+ }
2712
+
2713
+ hookExtraParams = ['_p5FragPos: vec4<f32>'];
2714
+ hookExtraArgs = [fragmentPositionArg];
2715
+ } else if (shaderType === 'compute') {
2716
+ const ensuredGlobalId = ensureBuiltinParam(main, 'global_invocation_id', '_p5GlobalId', 'vec3<u32>');
2717
+ main = ensuredGlobalId.mainSrc;
2718
+
2719
+ hookExtraParams = ['_p5GlobalId: vec3<u32>'];
2720
+ hookExtraArgs = [ensuredGlobalId.argName];
2256
2721
  }
2257
2722
 
2258
2723
  // Inject hook uniforms as a separate struct at a new binding
@@ -2272,6 +2737,7 @@ function rendererWebGPU(p5, fn) {
2272
2737
  const nextBinding = this.getNextBindingIndex({
2273
2738
  vert: shaderType === 'vertex' ? preMain + (shader.hooks.vertex?.declarations ?? '') + shader.hooks.declarations : shader._vertSrc,
2274
2739
  frag: shaderType === 'fragment' ? preMain + (shader.hooks.fragment?.declarations ?? '') + shader.hooks.declarations : shader._fragSrc,
2740
+ compute: shaderType === 'compute' ? preMain + (shader.hooks.compute?.declarations ?? '') + shader.hooks.declarations : shader._computeSrc,
2275
2741
  }, 0);
2276
2742
 
2277
2743
  // Create HookUniforms struct and binding
@@ -2282,8 +2748,14 @@ ${hookUniformFields}}
2282
2748
 
2283
2749
  @group(0) @binding(${nextBinding}) var<uniform> hooks: HookUniforms;
2284
2750
  `;
2285
- // Insert before the first @group binding
2286
- preMain = preMain.replace(/(@group\(0\)\s+@binding)/, `${hookUniformsDecl}\n$1`);
2751
+ // Insert before the first @group binding, or at the end if there are none
2752
+ const replaced = preMain.replace(/(@group\(0\)\s+@binding)/, `${hookUniformsDecl}\n$1`);
2753
+ if (replaced === preMain) {
2754
+ // No @group bindings found in base shader, append to preMain
2755
+ preMain = preMain + '\n' + hookUniformsDecl;
2756
+ } else {
2757
+ preMain = replaced;
2758
+ }
2287
2759
  }
2288
2760
 
2289
2761
  // Handle varying variables by injecting them into VertexOutput and FragmentInput structs
@@ -2349,10 +2821,9 @@ ${hookUniformFields}}
2349
2821
  initStatements += ` ${varName} = INPUT_VAR.${varName};\n`;
2350
2822
  }
2351
2823
 
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];
2824
+ const mainStructParam = getMainStructParameter(main);
2825
+ if (mainStructParam) {
2826
+ const inputVarName = mainStructParam.inputName;
2356
2827
  initStatements = initStatements.replace(/INPUT_VAR/g, inputVarName);
2357
2828
  // Insert after the main function parameter but before any other code (anchored to start)
2358
2829
  postMain = initStatements + postMain;
@@ -2360,12 +2831,56 @@ ${hookUniformFields}}
2360
2831
  }
2361
2832
  }
2362
2833
 
2834
+ // Handle instanceID varying for fragment access
2835
+ if (shader.hooks.instanceIDVarying) {
2836
+ const { name, declaration, source, interpolation } = shader.hooks.instanceIDVarying;
2837
+ const nextLocIndex = this._getNextAvailableLocation(preMain, shaderType);
2838
+ const interpAttr = interpolation ? ` @interpolate(${interpolation})` : '';
2839
+ const [varName, varType] = declaration.split(':').map(s => s.trim());
2840
+ const structMember = `@location(${nextLocIndex})${interpAttr} ${declaration},`;
2841
+
2842
+ if (shaderType === 'vertex') {
2843
+ // Inject into VertexOutput struct
2844
+ preMain = preMain.replace(
2845
+ /struct\s+VertexOutput\s+\{([^}]*)\}/,
2846
+ (match, body) => `struct VertexOutput {${body}\n${structMember}}`
2847
+ );
2848
+ // Add private global
2849
+ preMain += `var<private> ${declaration};\n`;
2850
+ // Assign from built-in instanceID at start of main()
2851
+ postMain = `\n ${varName} = ${source};\n` + postMain;
2852
+ // Copy to output struct before return
2853
+ const returnMatch = postMain.match(/return\s+(\w+)\s*;/);
2854
+ if (returnMatch) {
2855
+ const outputVarName = returnMatch[1];
2856
+ postMain = postMain.replace(
2857
+ /(return\s+\w+\s*;)/g,
2858
+ `${outputVarName}.${varName} = ${varName};\n $1`
2859
+ );
2860
+ }
2861
+ } else if (shaderType === 'fragment') {
2862
+ // Inject into FragmentInput struct
2863
+ preMain = preMain.replace(
2864
+ /struct\s+FragmentInput\s+\{([^}]*)\}/,
2865
+ (match, body) => `struct FragmentInput {${body}\n${structMember}}`
2866
+ );
2867
+ // Add private global
2868
+ preMain += `var<private> ${declaration};\n`;
2869
+ // Initialize from input struct at start of main()
2870
+ const mainStructParam = getMainStructParameter(main);
2871
+ if (mainStructParam) {
2872
+ const inputVarName = mainStructParam.inputName;
2873
+ postMain = `\n ${varName} = ${inputVarName}.${varName};\n` + postMain;
2874
+ }
2875
+ }
2876
+ }
2877
+
2363
2878
  let hooks = '';
2364
2879
  let defines = '';
2365
2880
  if (shader.hooks.declarations) {
2366
2881
  hooks += shader.hooks.declarations + '\n';
2367
2882
  }
2368
- if (shader.hooks[shaderType].declarations) {
2883
+ if (shader.hooks[shaderType] && shader.hooks[shaderType].declarations) {
2369
2884
  hooks += shader.hooks[shaderType].declarations + '\n';
2370
2885
  }
2371
2886
  for (const hookDef in shader.hooks.helpers) {
@@ -2389,11 +2904,7 @@ ${hookUniformFields}}
2389
2904
 
2390
2905
  let [_, params, body] = /^(\([^\)]*\))((?:.|\n)*)$/.exec(shader.hooks[shaderType][hookDef]);
2391
2906
 
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
- }
2907
+ params = appendHookParams(params, hookExtraParams);
2397
2908
 
2398
2909
  if (hookType === 'void') {
2399
2910
  hooks += `fn HOOK_${hookName}${params}${body}\n`;
@@ -2402,40 +2913,45 @@ ${hookUniformFields}}
2402
2913
  }
2403
2914
  }
2404
2915
 
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
- }
2916
+ // Pass stage-specific builtins from main to each hook call.
2917
+ // Collect ALL HOOK_ calls (including nested ones) then insert
2918
+ // extra args from right to left so position shifts don't
2919
+ // invalidate earlier insertion points.
2920
+ if (hookExtraArgs.length > 0) {
2921
+ const addHookArgs = (src) => {
2922
+ const insertions = [];
2923
+ let searchIdx = 0;
2924
+ let m;
2925
+ while ((m = /HOOK_\w+\(/.exec(src.slice(searchIdx))) !== null) {
2926
+ const openParen = searchIdx + m.index + m[0].length - 1;
2927
+ let pos = openParen + 1;
2928
+ let nesting = 1;
2929
+ let hasParams = false;
2930
+ while (pos < src.length && nesting > 0) {
2931
+ if (src[pos] === '(') nesting++;
2932
+ else if (src[pos] === ')') {
2933
+ nesting--;
2934
+ if (nesting === 0) break;
2935
+ } else if (/\S/.test(src[pos])) {
2936
+ hasParams = true;
2429
2937
  }
2430
- const insertion = (hasParams ? ', ' : '') + 'instanceID';
2431
- result = result.slice(0, idx-1) + insertion + result.slice(idx-1);
2432
- idx += insertion.length;
2938
+ pos++;
2433
2939
  }
2434
- } while (match);
2940
+ insertions.push({ pos, hasParams });
2941
+ searchIdx = openParen + 1;
2942
+ }
2943
+
2944
+ insertions.sort((a, b) => b.pos - a.pos);
2945
+
2946
+ let result = src;
2947
+ for (const { pos, hasParams } of insertions) {
2948
+ const insertion = (hasParams ? ', ' : '') + hookExtraArgs.join(', ');
2949
+ result = result.slice(0, pos) + insertion + result.slice(pos);
2950
+ }
2435
2951
  return result;
2436
2952
  };
2437
- preMain = addInstanceIDParam(preMain);
2438
- postMain = addInstanceIDParam(postMain);
2953
+ preMain = addHookArgs(preMain);
2954
+ postMain = addHookArgs(postMain);
2439
2955
  }
2440
2956
 
2441
2957
  return preMain + '\n' + defines + hooks + main + postMain;
@@ -2494,6 +3010,10 @@ ${hookUniformFields}}
2494
3010
  body = shader.hooks.fragment[hookName];
2495
3011
  fullSrc = shader._fragSrc;
2496
3012
  }
3013
+ if (!body) {
3014
+ body = shader.hooks.compute[hookName];
3015
+ fullSrc = shader._computeSrc;
3016
+ }
2497
3017
  if (!body) {
2498
3018
  throw new Error(`Can't find hook ${hookName}!`);
2499
3019
  }
@@ -2625,7 +3145,7 @@ ${hookUniformFields}}
2625
3145
  }
2626
3146
 
2627
3147
  defaultFramebufferAntialias() {
2628
- return true;
3148
+ return this._pInst._webgpuAttributes?.antialias !== false;
2629
3149
  }
2630
3150
 
2631
3151
  supportsFramebufferAntialias() {
@@ -2818,6 +3338,267 @@ ${hookUniformFields}}
2818
3338
  };
2819
3339
  }
2820
3340
 
3341
+ // Maps a plain JS value to the WGSL type string that represents it in a struct.
3342
+ _jsValueToWgslType(value) {
3343
+ if (typeof value === 'number') return 'f32';
3344
+ // Duck typing instead of instanceof to avoid importing a separate
3345
+ // copy of the Color/Vector classes
3346
+ if (value?.isVector) {
3347
+ if (value.dimensions === 2) return 'vec2f';
3348
+ if (value.dimensions === 3) return 'vec3f';
3349
+ if (value.dimensions === 4) return 'vec4f';
3350
+ throw new Error(`Unsupported vector dimension ${value.dimensions} for struct storage field`);
3351
+ }
3352
+ if (value?.isColor) {
3353
+ return 'vec4f';
3354
+ }
3355
+ if (Array.isArray(value)) {
3356
+ if (value.length === 2) return 'vec2f';
3357
+ if (value.length === 3) return 'vec3f';
3358
+ if (value.length === 4) return 'vec4f';
3359
+ throw new Error(`Unsupported array length ${value.length} for struct storage field`);
3360
+ }
3361
+ throw new Error(`Unsupported value type ${typeof value} for struct storage field`);
3362
+ }
3363
+
3364
+ // Infers a struct schema from the first element of a struct array.
3365
+ //
3366
+ // Returns { fields, stride, structBody } where:
3367
+ // fields: field has the _packField interface (baseType, size, offset, packInPlace) plus:
3368
+ // name: string - JS property name
3369
+ // dim: number - float component count, used when creating StrandsNodes
3370
+ // structBody: everything inside the { ... } of a WGSL struct definition
3371
+ // stride: how many bytes are reserved for this struct in the buffer
3372
+ _inferStructSchema(firstElement) {
3373
+ const entries = Object.entries(firstElement);
3374
+
3375
+ if (!p5.disableFriendlyErrors) {
3376
+ for (const [name, value] of entries) {
3377
+ if (
3378
+ value !== null &&
3379
+ typeof value === 'object' &&
3380
+ !Array.isArray(value) &&
3381
+ // Duck typing instead of instanceof to avoid importing a separate
3382
+ // copy of the Color/Vector classes
3383
+ !value?.isVector &&
3384
+ !value?.isColor
3385
+ ) {
3386
+ p5._friendlyError(
3387
+ `The "${name}" property in your storage data contains a nested object. ` +
3388
+ `Make sure you only use properties with numbers, arrays of numbers, or p5.Vector.`,
3389
+ 'createStorage'
3390
+ );
3391
+ }
3392
+ }
3393
+ }
3394
+
3395
+ const fieldLines = entries.map(([name, value]) =>
3396
+ ` ${name}: ${this._jsValueToWgslType(value)},`
3397
+ ).join('\n');
3398
+ const structBody = `{\n${fieldLines}\n}`;
3399
+ const elements = this._parseStruct(`struct _Tmp ${structBody}`, '_Tmp');
3400
+
3401
+ let maxEnd = 0;
3402
+ let maxAlign = 1;
3403
+ const fields = entries.map(([name, value]) => {
3404
+ const el = elements[name];
3405
+ maxEnd = Math.max(maxEnd, el.offsetEnd);
3406
+ // Alignment for scalars/vectors: <=4 -> 4, <=8 -> 8, else 16
3407
+ const align = el.size <= 4 ? 4 : el.size <= 8 ? 8 : 16;
3408
+ maxAlign = Math.max(maxAlign, align);
3409
+ // Track original JS type for reconstruction during readback
3410
+ const kind = value?.isVector ? 'vector'
3411
+ : value?.isColor ? 'color'
3412
+ : undefined;
3413
+ return {
3414
+ name,
3415
+ baseType: el.baseType,
3416
+ size: el.size,
3417
+ offset: el.offset,
3418
+ packInPlace: el.packInPlace ?? false,
3419
+ dim: el.size / 4,
3420
+ kind,
3421
+ };
3422
+ });
3423
+
3424
+ const stride = Math.ceil(maxEnd / maxAlign) * maxAlign;
3425
+ return { fields, stride, structBody };
3426
+ }
3427
+
3428
+ // Packs an array of plain objects into a Float32Array using the given struct schema.
3429
+ // Reuses _packField so layout rules match uniform packing exactly.
3430
+ _packStructArray(data, schema) {
3431
+ const { fields, stride } = schema;
3432
+ const totalBytes = Math.max(data.length * stride, 16);
3433
+ const alignedBytes = Math.ceil(totalBytes / 16) * 16;
3434
+ const buffer = new ArrayBuffer(alignedBytes);
3435
+ const floatView = new Float32Array(buffer);
3436
+ const dataView = new DataView(buffer);
3437
+ for (let i = 0; i < data.length; i++) {
3438
+ const item = data[i];
3439
+ const baseOffset = i * stride;
3440
+ for (const field of fields) {
3441
+ this._packField(field, item[field.name], floatView, dataView, baseOffset);
3442
+ }
3443
+ }
3444
+ return floatView;
3445
+ }
3446
+
3447
+ // Inverse of _packStructArray reads packed buffer back into plain JS objects
3448
+ // using the same schema layout - fields, stride and offsets
3449
+ _unpackStructArray(floatView, schema) {
3450
+ const { fields, stride } = schema;
3451
+ const dataView = new DataView(floatView.buffer);
3452
+ const count = Math.floor(floatView.byteLength / stride);
3453
+ const result = [];
3454
+
3455
+ for (let i = 0; i < count; i++) {
3456
+ const item = {};
3457
+ const baseOffset = i * stride;
3458
+ for (const field of fields) {
3459
+ const byteOffset = baseOffset + field.offset;
3460
+ const n = field.size / 4;
3461
+
3462
+ if (field.baseType === 'u32') {
3463
+ if (n === 1) {
3464
+ item[field.name] = dataView.getUint32(byteOffset, true);
3465
+ } else {
3466
+ item[field.name] = Array.from({ length: n }, (_, j) =>
3467
+ dataView.getUint32(byteOffset + j * 4, true)
3468
+ );
3469
+ }
3470
+ } else if (field.baseType === 'i32') {
3471
+ if (n === 1) {
3472
+ item[field.name] = dataView.getInt32(byteOffset, true);
3473
+ } else {
3474
+ item[field.name] = Array.from({ length: n }, (_, j) =>
3475
+ dataView.getInt32(byteOffset + j * 4, true)
3476
+ );
3477
+ }
3478
+ } else {
3479
+ const idx = byteOffset / 4;
3480
+ if (n === 1) {
3481
+ item[field.name] = floatView[idx];
3482
+ } else {
3483
+ const values = Array.from(floatView.slice(idx, idx + n));
3484
+ if (field.kind === 'vector') {
3485
+ item[field.name] = this._pInst.createVector(...values);
3486
+ } else if (field.kind === 'color') {
3487
+ // Color was packed as normalized RGBA [0-1] via _getRGBA([1,1,1,1])
3488
+ // Scale back to the current colorMode range
3489
+ const maxes = this.states.colorMaxes[this.states.colorMode];
3490
+ item[field.name] = this._pInst.color(
3491
+ values[0] * maxes[0], values[1] * maxes[1],
3492
+ values[2] * maxes[2], values[3] * maxes[3]
3493
+ );
3494
+ } else {
3495
+ item[field.name] = values;
3496
+ }
3497
+ }
3498
+ }
3499
+ }
3500
+ result.push(item);
3501
+ }
3502
+
3503
+ return result;
3504
+ }
3505
+
3506
+ createStorage(dataOrCount) {
3507
+ const device = this.device;
3508
+
3509
+ // Struct array: an array of plain objects
3510
+ if (Array.isArray(dataOrCount) && dataOrCount.length > 0 &&
3511
+ typeof dataOrCount[0] === 'object' && !Array.isArray(dataOrCount[0])) {
3512
+ if (!p5.disableFriendlyErrors && dataOrCount.length > 1) {
3513
+ const firstKeys = Object.keys(dataOrCount[0]);
3514
+ let warned = false;
3515
+ for (let i = 1; i < dataOrCount.length; i++) {
3516
+ const el = dataOrCount[i];
3517
+ const elKeys = Object.keys(el);
3518
+ const sameKeys = firstKeys.length === elKeys.length &&
3519
+ firstKeys.every((k, j) => k === elKeys[j]);
3520
+ if (!sameKeys) {
3521
+ p5._friendlyError(
3522
+ `Element ${i} has different fields than element 0. ` +
3523
+ `All elements should have the same properties.`,
3524
+ 'createStorage'
3525
+ );
3526
+ break;
3527
+ }
3528
+ for (const key of firstKeys) {
3529
+ const firstType = this._jsValueToWgslType(dataOrCount[0][key]);
3530
+ const elType = this._jsValueToWgslType(el[key]);
3531
+ if (firstType !== elType) {
3532
+ p5._friendlyError(
3533
+ `The "${key}" property of element ${i} has type ${elType} ` +
3534
+ `but element 0 has type ${firstType}. Proporties should have the same type across all elements.`,
3535
+ 'createStorage'
3536
+ );
3537
+ warned = true;
3538
+ break;
3539
+ }
3540
+ }
3541
+ if (warned) break;
3542
+ }
3543
+ }
3544
+ const schema = this._inferStructSchema(dataOrCount[0]);
3545
+ const packed = this._packStructArray(dataOrCount, schema);
3546
+ const size = packed.byteLength;
3547
+ const buffer = device.createBuffer({
3548
+ size,
3549
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
3550
+ mappedAtCreation: true,
3551
+ });
3552
+ new Float32Array(buffer.getMappedRange()).set(packed);
3553
+ buffer.unmap();
3554
+ const storageBuffer = new StorageBuffer(buffer, size, this, schema);
3555
+ this._storageBuffers.add(storageBuffer);
3556
+ return storageBuffer;
3557
+ }
3558
+
3559
+ // Determine buffer size and initial data
3560
+ let size, initialData;
3561
+ if (typeof dataOrCount === 'number') {
3562
+ // createStorage(count) - zero-initialized
3563
+ size = dataOrCount * 4; // floats are 4 bytes
3564
+ initialData = new Float32Array(dataOrCount);
3565
+ } else {
3566
+ // createStorage(array) - from data
3567
+ if (dataOrCount instanceof Float32Array) {
3568
+ initialData = dataOrCount;
3569
+ } else if (Array.isArray(dataOrCount)) {
3570
+ initialData = new Float32Array(dataOrCount);
3571
+ } else {
3572
+ throw new Error('createStorage expects a number or array/Float32Array');
3573
+ }
3574
+ size = initialData.byteLength;
3575
+ }
3576
+
3577
+ // Align to 16 bytes (WGSL storage buffer alignment requirement)
3578
+ size = Math.ceil(size / 16) * 16;
3579
+
3580
+ // Create storage buffer with STORAGE | COPY_DST | COPY_SRC usage
3581
+ const buffer = device.createBuffer({
3582
+ size,
3583
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
3584
+ mappedAtCreation: initialData.length > 0
3585
+ });
3586
+
3587
+ // Write initial data if provided
3588
+ if (initialData.length > 0) {
3589
+ const mapping = new Float32Array(buffer.getMappedRange());
3590
+ mapping.set(initialData);
3591
+ buffer.unmap();
3592
+ }
3593
+
3594
+ const storageBuffer = new StorageBuffer(buffer, size, this);
3595
+
3596
+ // Track for cleanup
3597
+ this._storageBuffers.add(storageBuffer);
3598
+
3599
+ return storageBuffer;
3600
+ }
3601
+
2821
3602
  _getWebGPUColorFormat(framebuffer) {
2822
3603
  if (framebuffer.format === FLOAT) {
2823
3604
  return framebuffer.channels === RGBA ? 'rgba32float' : 'rgba32float';
@@ -3114,10 +3895,6 @@ ${hookUniformFields}}
3114
3895
  return super.filter(...args);
3115
3896
  }
3116
3897
 
3117
- getNoiseShaderSnippet() {
3118
- return noiseWGSL;
3119
- }
3120
-
3121
3898
 
3122
3899
  baseFilterShader() {
3123
3900
  if (!this._baseFilterShader) {
@@ -3141,6 +3918,21 @@ ${hookUniformFields}}
3141
3918
  return this._baseFilterShader;
3142
3919
  }
3143
3920
 
3921
+ baseComputeShader() {
3922
+ if (!this._baseComputeShader) {
3923
+ this._baseComputeShader = new Shader(
3924
+ this,
3925
+ baseComputeShader,
3926
+ {
3927
+ compute: {
3928
+ 'void iteration': '(index: vec3<i32>) {}',
3929
+ },
3930
+ }
3931
+ );
3932
+ }
3933
+ return this._baseComputeShader;
3934
+ }
3935
+
3144
3936
  /*
3145
3937
  * WebGPU-specific implementation of imageLight shader creation
3146
3938
  */
@@ -3240,6 +4032,69 @@ ${hookUniformFields}}
3240
4032
  glDataType: dataType || 'uint8'
3241
4033
  };
3242
4034
  }
4035
+
4036
+ compute(shader, x, y = 1, z = 1) {
4037
+ if (shader.shaderType !== 'compute') {
4038
+ throw new Error('compute() can only be called with a compute shader');
4039
+ }
4040
+
4041
+ this._finishActiveRenderPass();
4042
+
4043
+ // Ensure shader is initialized and finalized
4044
+ if (!shader._compiled) {
4045
+ shader.init();
4046
+ }
4047
+
4048
+ // Set default uniforms
4049
+ shader.setDefaultUniforms();
4050
+ shader.setUniform('uTotalCount', [x, y, z]);
4051
+
4052
+ // Calculate optimal workgroup size (8x8x1 = 64 threads per workgroup)
4053
+ const WORKGROUP_SIZE_X = 8;
4054
+ const WORKGROUP_SIZE_Y = 8;
4055
+ const WORKGROUP_SIZE_Z = 1;
4056
+
4057
+ // auto spreading: if any dimension is too large or for performance optimization,
4058
+ // spread total iteration count across dimensions
4059
+ const totalIterations = x * y * z;
4060
+ const MAX_THREADS_PER_DIM = 65535 * 8;
4061
+
4062
+ let px = x;
4063
+ let py = y;
4064
+ let pz = z;
4065
+
4066
+ // we spread if we exceed GPU limits OR if it involves a large 1D dispatch
4067
+ const exceedsLimits = x > MAX_THREADS_PER_DIM || y > MAX_THREADS_PER_DIM || z > MAX_THREADS_PER_DIM;
4068
+ const isLarge1D = totalIterations > 1024 && y === 1 && z === 1;
4069
+
4070
+ if (exceedsLimits || isLarge1D) {
4071
+ // Always use 2D square spreading (√N × √N).
4072
+ // Benchmarks showed 2D square equals or outperforms 3D cube at every
4073
+ // scale tested, with simpler index reconstruction in the shader.
4074
+ px = Math.ceil(Math.sqrt(totalIterations));
4075
+ py = Math.ceil(totalIterations / px);
4076
+ pz = 1;
4077
+ }
4078
+
4079
+ shader.setUniform('uPhysicalCount', [px, py, pz]);
4080
+
4081
+ const workgroupCountX = Math.ceil(px / WORKGROUP_SIZE_X);
4082
+ const workgroupCountY = Math.ceil(py / WORKGROUP_SIZE_Y);
4083
+ const workgroupCountZ = Math.ceil(pz / WORKGROUP_SIZE_Z);
4084
+
4085
+ const commandEncoder = this.device.createCommandEncoder();
4086
+ const passEncoder = commandEncoder.beginComputePass();
4087
+ this.setupShaderBindGroups(shader, passEncoder, {
4088
+ compute: true,
4089
+ workgroupSize: [WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y, WORKGROUP_SIZE_Z],
4090
+ });
4091
+
4092
+ // Dispatch compute workgroups
4093
+ passEncoder.dispatchWorkgroups(workgroupCountX, workgroupCountY, workgroupCountZ);
4094
+
4095
+ passEncoder.end();
4096
+ this.device.queue.submit([commandEncoder.finish()]);
4097
+ }
3243
4098
  }
3244
4099
 
3245
4100
  p5.RendererWebGPU = RendererWebGPU;
@@ -3250,6 +4105,7 @@ ${hookUniformFields}}
3250
4105
  fn.setAttributes = async function (key, value) {
3251
4106
  return this._renderer._setAttributes(key, value);
3252
4107
  };
4108
+
3253
4109
  }
3254
4110
 
3255
4111
  if (typeof p5 !== "undefined") {