p5 2.2.1-rc.0 → 2.2.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 (117) hide show
  1. package/dist/accessibility/color_namer.js +5 -6
  2. package/dist/accessibility/describe.js +4 -26
  3. package/dist/accessibility/index.js +5 -6
  4. package/dist/accessibility/outputs.js +6 -38
  5. package/dist/app.js +5 -6
  6. package/dist/color/color_conversion.js +5 -6
  7. package/dist/color/creating_reading.js +1 -1
  8. package/dist/color/index.js +2 -2
  9. package/dist/color/p5.Color.js +1 -1
  10. package/dist/color/setting.js +59 -357
  11. package/dist/{constants-DEJVKr9Z.js → constants-DQyACdzq.js} +11 -61
  12. package/dist/core/constants.js +1 -1
  13. package/dist/core/environment.js +26 -158
  14. package/dist/core/filterShaders.js +1 -1
  15. package/dist/core/friendly_errors/fes_core.js +1 -1
  16. package/dist/core/friendly_errors/file_errors.js +1 -1
  17. package/dist/core/friendly_errors/index.js +1 -1
  18. package/dist/core/friendly_errors/param_validator.js +1 -1
  19. package/dist/core/friendly_errors/sketch_verifier.js +1 -1
  20. package/dist/core/helpers.js +1 -1
  21. package/dist/core/init.js +5 -6
  22. package/dist/core/internationalization.js +1 -1
  23. package/dist/core/legacy.js +5 -6
  24. package/dist/core/main.js +5 -6
  25. package/dist/core/p5.Graphics.js +4 -5
  26. package/dist/core/p5.Renderer.js +3 -4
  27. package/dist/core/p5.Renderer2D.js +5 -6
  28. package/dist/core/p5.Renderer3D.js +4 -5
  29. package/dist/core/rendering.js +4 -5
  30. package/dist/core/structure.js +13 -52
  31. package/dist/core/transform.js +32 -176
  32. package/dist/{creating_reading-CgHCHxqN.js → creating_reading-ZXzcZEsb.js} +3 -196
  33. package/dist/data/local_storage.js +4 -30
  34. package/dist/dom/dom.js +24 -159
  35. package/dist/dom/index.js +2 -2
  36. package/dist/dom/p5.Element.js +31 -208
  37. package/dist/dom/p5.File.js +1 -32
  38. package/dist/dom/p5.MediaElement.js +10 -113
  39. package/dist/events/acceleration.js +11 -64
  40. package/dist/events/keyboard.js +13 -81
  41. package/dist/events/pointer.js +18 -160
  42. package/dist/image/const.js +1 -1
  43. package/dist/image/filterRenderer2D.js +4 -5
  44. package/dist/image/image.js +4 -5
  45. package/dist/image/index.js +4 -5
  46. package/dist/image/loading_displaying.js +4 -5
  47. package/dist/image/p5.Image.js +3 -4
  48. package/dist/image/pixels.js +17 -100
  49. package/dist/io/files.js +4 -5
  50. package/dist/io/index.js +4 -5
  51. package/dist/io/p5.Table.js +66 -158
  52. package/dist/io/p5.TableRow.js +48 -71
  53. package/dist/io/p5.XML.js +6 -99
  54. package/dist/io/utilities.js +8 -3
  55. package/dist/{main-_RXV5Lx8.js → main-DvN69W3f.js} +13 -42
  56. package/dist/math/Matrices/Matrix.js +87 -126
  57. package/dist/math/Matrices/MatrixNumjs.js +1 -5
  58. package/dist/math/calculation.js +10 -112
  59. package/dist/math/index.js +1 -1
  60. package/dist/math/math.js +2 -12
  61. package/dist/math/noise.js +5 -32
  62. package/dist/math/p5.Matrix.js +3 -3
  63. package/dist/math/p5.Vector.js +104 -345
  64. package/dist/math/random.js +5 -32
  65. package/dist/math/trigonometry.js +15 -105
  66. package/dist/{p5.Renderer-QoFcvj3f.js → p5.Renderer-D-5LdCRz.js} +25 -178
  67. package/dist/{rendering-CsICjEXA.js → rendering-h9unX5K0.js} +254 -1156
  68. package/dist/shape/2d_primitives.js +33 -194
  69. package/dist/shape/attributes.js +12 -73
  70. package/dist/shape/curves.js +30 -95
  71. package/dist/shape/custom_shapes.js +63 -144
  72. package/dist/shape/index.js +2 -2
  73. package/dist/shape/vertex.js +21 -106
  74. package/dist/strands/p5.strands.js +248 -46
  75. package/dist/type/index.js +3 -4
  76. package/dist/type/p5.Font.js +4 -49
  77. package/dist/type/textCore.js +5 -158
  78. package/dist/utilities/conversion.js +17 -104
  79. package/dist/utilities/time_date.js +3 -40
  80. package/dist/utilities/utility_functions.js +6 -48
  81. package/dist/webgl/3d_primitives.js +4 -5
  82. package/dist/webgl/GeometryBuilder.js +1 -2
  83. package/dist/webgl/ShapeBuilder.js +22 -2
  84. package/dist/webgl/enums.js +1 -1
  85. package/dist/webgl/index.js +4 -5
  86. package/dist/webgl/interaction.js +6 -33
  87. package/dist/webgl/light.js +4 -5
  88. package/dist/webgl/loading.js +12 -46
  89. package/dist/webgl/material.js +4 -5
  90. package/dist/webgl/p5.Camera.js +4 -5
  91. package/dist/webgl/p5.DataArray.js +0 -4
  92. package/dist/webgl/p5.Framebuffer.js +4 -5
  93. package/dist/webgl/p5.Geometry.js +12 -106
  94. package/dist/webgl/p5.Quat.js +1 -1
  95. package/dist/webgl/p5.RendererGL.js +7 -18
  96. package/dist/webgl/p5.Shader.js +12 -36
  97. package/dist/webgl/p5.Texture.js +4 -5
  98. package/dist/webgl/text.js +4 -5
  99. package/dist/webgl/utils.js +4 -5
  100. package/dist/webgpu/index.js +1 -1
  101. package/dist/webgpu/p5.RendererWebGPU.js +529 -208
  102. package/dist/webgpu/shaders/color.js +32 -17
  103. package/dist/webgpu/shaders/filters/base.js +18 -7
  104. package/dist/webgpu/shaders/font.js +52 -40
  105. package/dist/webgpu/shaders/line.js +50 -36
  106. package/dist/webgpu/shaders/material.js +90 -83
  107. package/dist/webgpu/strands_wgslBackend.js +5 -2
  108. package/lib/p5.esm.js +5576 -7811
  109. package/lib/p5.esm.min.js +1 -1
  110. package/lib/p5.js +5576 -7811
  111. package/lib/p5.min.js +1 -1
  112. package/lib/p5.webgpu.esm.js +786 -453
  113. package/lib/p5.webgpu.js +786 -453
  114. package/lib/p5.webgpu.min.js +1 -1
  115. package/package.json +13 -13
  116. package/types/global.d.ts +16905 -16783
  117. package/types/p5.d.ts +11142 -11081
@@ -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-DEJVKr9Z.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-DQyACdzq.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';
@@ -41,10 +41,27 @@ function rendererWebGPU(p5, fn) {
41
41
  constructor(pInst, w, h, isMainCanvas, elt) {
42
42
  super(pInst, w, h, isMainCanvas, elt);
43
43
 
44
- this.renderPass = {};
44
+ // Used to group draws into one big render pass
45
+ this.activeRenderPass = null;
46
+ this.activeRenderPassEncoder = null;
47
+ this.activeShaderOptions = null;
48
+ this.activeShader = null;
45
49
 
46
50
  this.samplers = new Map();
47
51
 
52
+ // Some uniforms update every frame, like model matrices and sometimes colors.
53
+ // The fastest way to handle these is to use mapped memory. We'll batch those
54
+ // into bigger buffers with dynamic offsets, separate from the usual system
55
+ // where bind groups have their own little buffers that get cached when they
56
+ // are unchanged
57
+ this.uniformBufferAlignment = 256;
58
+ this.activeUniformBuffers = [];
59
+ this.currentUniformBuffer = undefined;
60
+ this.uniformBufferPool = [];
61
+ this.resettingUniformBuffers = [];
62
+
63
+ this.dynamicEntryOffsets = new Uint32Array(64);
64
+
48
65
  // Cache for current frame's canvas texture view
49
66
  this.currentCanvasColorTexture = null;
50
67
  this.currentCanvasColorTextureView = null;
@@ -208,6 +225,68 @@ function rendererWebGPU(p5, fn) {
208
225
  return this.currentCanvasColorTextureView;
209
226
  }
210
227
 
228
+ _beginActiveRenderPass() {
229
+ if (this.activeRenderPass) return;
230
+
231
+ // Use framebuffer texture if active, otherwise use canvas texture
232
+ const activeFramebuffer = this.activeFramebuffer();
233
+
234
+ const colorAttachment = {
235
+ view: activeFramebuffer
236
+ ? (activeFramebuffer.aaColorTexture
237
+ ? activeFramebuffer.aaColorTextureView
238
+ : activeFramebuffer.colorTextureView)
239
+ : this._getCanvasColorTextureView(),
240
+ loadOp: "load",
241
+ storeOp: "store",
242
+ // If using multisampled texture, resolve to non-multisampled texture
243
+ resolveTarget: activeFramebuffer && activeFramebuffer.aaColorTexture
244
+ ? activeFramebuffer.colorTextureView
245
+ : undefined,
246
+ };
247
+
248
+ // Use framebuffer depth texture if active, otherwise use canvas depth texture
249
+ const depthTextureView = activeFramebuffer
250
+ ? (activeFramebuffer.aaDepthTexture
251
+ ? activeFramebuffer.aaDepthTextureView
252
+ : activeFramebuffer.depthTextureView)
253
+ : this.depthTextureView;
254
+ const renderPassDescriptor = {
255
+ colorAttachments: [colorAttachment],
256
+ depthStencilAttachment: depthTextureView
257
+ ? {
258
+ view: depthTextureView,
259
+ depthLoadOp: "load",
260
+ depthStoreOp: "store",
261
+ depthClearValue: 1.0,
262
+ stencilLoadOp: "load",
263
+ stencilStoreOp: "store",
264
+ depthReadOnly: false,
265
+ stencilReadOnly: false,
266
+ }
267
+ : undefined,
268
+ };
269
+ const commandEncoder = this.device.createCommandEncoder();
270
+ const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
271
+ this.activeRenderPassEncoder = commandEncoder;
272
+ this.activeRenderPass = passEncoder;
273
+ }
274
+
275
+ _finishActiveRenderPass() {
276
+ if (!this.activeRenderPass) return;
277
+
278
+ const commandEncoder = this.activeRenderPassEncoder;
279
+ const passEncoder = this.activeRenderPass;
280
+ passEncoder.end();
281
+
282
+ // Store the command encoder for later submission
283
+ this._pendingCommandEncoders.push(commandEncoder.finish());
284
+ this.activeRenderPassEncoder = null;
285
+ this.activeRenderPass = null;
286
+ this.activeShader = null;
287
+ this.activeShaderOptions = null;
288
+ }
289
+
211
290
  clear(...args) {
212
291
  const _r = args[0] || 0;
213
292
  const _g = args[1] || 0;
@@ -219,6 +298,8 @@ function rendererWebGPU(p5, fn) {
219
298
  this._frameState = FRAME_STATE.UNPROMOTED;
220
299
  }
221
300
 
301
+ this._finishActiveRenderPass();
302
+
222
303
  const commandEncoder = this.device.createCommandEncoder();
223
304
 
224
305
  // Use framebuffer texture if active, otherwise use canvas texture
@@ -273,6 +354,7 @@ function rendererWebGPU(p5, fn) {
273
354
  * occlude anything subsequently drawn.
274
355
  */
275
356
  clearDepth(depth = 1) {
357
+ this._finishActiveRenderPass();
276
358
  const commandEncoder = this.device.createCommandEncoder();
277
359
 
278
360
  // Use framebuffer texture if active, otherwise use canvas texture
@@ -363,7 +445,6 @@ function rendererWebGPU(p5, fn) {
363
445
  const loc = attr.location;
364
446
  if (!this.registerEnabled.has(loc)) {
365
447
  // TODO
366
- // this.renderPass.setVertexBuffer(loc, buffer);
367
448
  this.registerEnabled.add(loc);
368
449
  }
369
450
  }
@@ -446,6 +527,14 @@ function rendererWebGPU(p5, fn) {
446
527
  }
447
528
  }
448
529
 
530
+ _shaderOptionsDifferent(newOptions) {
531
+ if (!this.activeShaderOptions) return true;
532
+ for (const key in this.activeShaderOptions) {
533
+ if (this.activeShaderOptions[key] !== newOptions[key]) return true;
534
+ }
535
+ return false;
536
+ }
537
+
449
538
  _initShader(shader) {
450
539
  const device = this.device;
451
540
 
@@ -500,39 +589,39 @@ function rendererWebGPU(p5, fn) {
500
589
  }
501
590
 
502
591
  _finalizeShader(shader) {
503
- const rawSize = Math.max(
504
- 0,
505
- ...Object.values(shader.uniforms).filter(u => !u.isSampler).map(u => u.offsetEnd)
506
- );
507
- const alignedSize = Math.ceil(rawSize / 16) * 16;
508
- shader._uniformData = new Float32Array(alignedSize / 4);
509
- shader._uniformDataView = new DataView(shader._uniformData.buffer);
510
-
511
- // Create pools for uniform buffers (both GPU buffers and data arrays.) This
512
- // is so that we can queue up multiple things to be able to be drawn and have
513
- // the GPU go through them as fast as possible. If we're overwriting the same
514
- // data again and again, we would have to wait for the GPU after each primitive
515
- // that we draw.
516
- shader._uniformBufferPool = [];
517
- shader._uniformBuffersInUse = [];
518
- shader._uniformBufferSize = alignedSize;
519
-
520
- // Create the first buffer for the pool
521
- const firstGPUBuffer = this.device.createBuffer({
522
- size: alignedSize,
523
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
524
- });
525
- const firstData = new Float32Array(alignedSize / 4);
526
- const firstDataView = new DataView(firstData.buffer);
527
-
528
- shader._uniformBufferPool.push({
529
- buffer: firstGPUBuffer,
530
- data: firstData,
531
- dataView: firstDataView
532
- });
533
-
534
- // Keep backward compatibility reference
535
- shader._uniformBuffer = firstGPUBuffer;
592
+ // Per-group buffer pools. We will pull from these when we draw multiple
593
+ // times using the shader in a render pass. These are per group instead of
594
+ // global so that we can reuse the last used buffer when uniform values
595
+ // don't change.
596
+ shader._uniformBufferGroups = [];
597
+ shader.buffersDirty = new Set();
598
+
599
+ for (const group of shader._uniformGroups) {
600
+ // Calculate the size needed for this group's uniforms
601
+ const groupUniforms = Object.values(group.uniforms);
602
+ const rawSize = Math.max(
603
+ 0,
604
+ ...groupUniforms.map(u => u.offsetEnd)
605
+ );
606
+ const alignedSize = Math.ceil(rawSize / 16) * 16;
607
+
608
+ shader._uniformBufferGroups.push({
609
+ group: group.group,
610
+ binding: group.binding,
611
+ cacheKey: group.group * 1000 + group.binding,
612
+ varName: group.varName,
613
+ structType: group.structType,
614
+ uniforms: groupUniforms,
615
+ size: alignedSize,
616
+
617
+ bufferPool: [],
618
+ nextBufferPool: [],
619
+
620
+ dynamic: groupUniforms.some(u => u.name.startsWith('uModel')),
621
+ buffersInUse: new Set(),
622
+ currentBuffer: null, // For caching
623
+ });
624
+ }
536
625
 
537
626
  // Register this shader in our registry for pool cleanup
538
627
  this._shadersWithPools.push(shader);
@@ -540,12 +629,22 @@ function rendererWebGPU(p5, fn) {
540
629
  const bindGroupLayouts = new Map(); // group index -> bindGroupLayout
541
630
  const groupEntries = new Map(); // group index -> array of entries
542
631
 
543
- // We're enforcing that every shader have a single uniform struct in binding 0
544
- groupEntries.set(0, [{
545
- binding: 0,
546
- visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
547
- buffer: { type: 'uniform' },
548
- }]);
632
+ // Add all uniform group bindings to group 0
633
+ const structEntries = new Map();
634
+ for (const bufferGroup of shader._uniformBufferGroups) {
635
+ const entries = structEntries.get(bufferGroup.group) || [];
636
+ entries.push({
637
+ bufferGroup,
638
+ binding: bufferGroup.binding,
639
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
640
+ buffer: { type: 'uniform', hasDynamicOffset: bufferGroup.dynamic },
641
+ });
642
+ structEntries.set(bufferGroup.group, entries);
643
+ }
644
+ for (const [group, entries] of structEntries.entries()) {
645
+ entries.sort((a, b) => a.binding - b.binding);
646
+ groupEntries.set(group, entries);
647
+ }
549
648
 
550
649
  // Add the variable amount of samplers and texture bindings that can come after
551
650
  for (const sampler of shader.samplers) {
@@ -567,17 +666,25 @@ function rendererWebGPU(p5, fn) {
567
666
  uniform: sampler,
568
667
  });
569
668
 
669
+ entries.sort((a, b) => a.binding - b.binding);
570
670
  groupEntries.set(group, entries);
571
671
  }
572
672
 
573
673
  // Create layouts and bind groups
674
+ const groupEntriesArr = [];
574
675
  for (const [group, entries] of groupEntries) {
575
676
  const layout = this.device.createBindGroupLayout({ entries });
576
677
  bindGroupLayouts.set(group, layout);
678
+ groupEntriesArr.push([group, entries]);
577
679
  }
578
680
 
579
- shader._groupEntries = groupEntries;
681
+ shader._groupEntries = groupEntriesArr;
580
682
  shader._bindGroupLayouts = [...bindGroupLayouts.values()];
683
+ // Reuse bind groups if they don't change
684
+ shader._cachedBindGroup = {};
685
+ // Remember which dynamic buffer we last used, so that we can
686
+ // possibly cache bind groups if unchanged
687
+ shader._lastDynamicBuffer = {};
581
688
  shader._pipelineLayout = this.device.createPipelineLayout({
582
689
  bindGroupLayouts: shader._bindGroupLayouts,
583
690
  });
@@ -766,22 +873,25 @@ function rendererWebGPU(p5, fn) {
766
873
  }
767
874
 
768
875
  _getVertexBuffers(shader) {
769
- const buffers = [];
876
+ if (!shader._vertexBuffers) {
877
+ const buffers = [];
770
878
 
771
- for (const attrName in shader.attributes) {
772
- const attr = shader.attributes[attrName];
773
- if (!attr || attr.location === -1) continue;
879
+ for (const attrName in shader.attributes) {
880
+ const attr = shader.attributes[attrName];
881
+ if (!attr || attr.location === -1) continue;
774
882
 
775
- // Get the vertex buffer info associated with this attribute
776
- const renderBuffer =
777
- this.buffers[shader.shaderType].find(buf => buf.attr === attrName) ||
778
- this.buffers.user.find(buf => buf.attr === attrName);
779
- if (!renderBuffer) continue;
883
+ // Get the vertex buffer info associated with this attribute
884
+ const renderBuffer =
885
+ this.buffers[shader.shaderType].find(buf => buf.attr === attrName) ||
886
+ this.buffers.user.find(buf => buf.attr === attrName);
887
+ if (!renderBuffer) continue;
780
888
 
781
- buffers.push(renderBuffer);
889
+ buffers.push(renderBuffer);
890
+ }
891
+ shader._vertexBuffers = buffers;
782
892
  }
783
893
 
784
- return buffers;
894
+ return shader._vertexBuffers;
785
895
  }
786
896
 
787
897
  _getFormatFromSize(size) {
@@ -823,6 +933,7 @@ function rendererWebGPU(p5, fn) {
823
933
  }
824
934
 
825
935
  _resetBuffersBeforeDraw() {
936
+ this._finishActiveRenderPass();
826
937
  // Set state to PENDING - we'll decide on first draw
827
938
  this._frameState = FRAME_STATE.PENDING;
828
939
 
@@ -1076,50 +1187,106 @@ function rendererWebGPU(p5, fn) {
1076
1187
  // Uniform buffer pool management
1077
1188
  //////////////////////////////////////////////
1078
1189
 
1079
- _getUniformBufferFromPool(shader) {
1190
+ _getUniformBufferFromPool(bufferGroup) {
1080
1191
  // Try to get a buffer from the pool
1081
- if (shader._uniformBufferPool.length > 0) {
1082
- const bufferInfo = shader._uniformBufferPool.pop();
1083
- shader._uniformBuffersInUse.push(bufferInfo);
1192
+ if (bufferGroup.bufferPool.length > 0) {
1193
+ const bufferInfo = bufferGroup.bufferPool.pop();
1194
+ bufferGroup.buffersInUse.add(bufferInfo);
1084
1195
  return bufferInfo;
1085
1196
  }
1086
1197
 
1087
1198
  // No buffers available, create a new one
1088
1199
  const newBuffer = this.device.createBuffer({
1089
- size: shader._uniformBufferSize,
1200
+ size: bufferGroup.size,
1090
1201
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1091
1202
  });
1092
- const newData = new Float32Array(shader._uniformBufferSize / 4);
1203
+ const newData = new Float32Array(bufferGroup.size / 4);
1093
1204
  const newDataView = new DataView(newData.buffer);
1094
-
1095
1205
  const bufferInfo = {
1096
1206
  buffer: newBuffer,
1097
1207
  data: newData,
1098
1208
  dataView: newDataView
1099
1209
  };
1100
1210
 
1101
- shader._uniformBuffersInUse.push(bufferInfo);
1211
+ bufferGroup.buffersInUse.add(bufferInfo);
1102
1212
  return bufferInfo;
1103
1213
  }
1104
1214
 
1215
+ _getDynamicUniformBufferFromPool(bufferGroup) {
1216
+ //
1217
+ let buffer;
1218
+ if (
1219
+ this.currentUniformBuffer &&
1220
+ this.currentUniformBuffer.offset + bufferGroup.size < this.currentUniformBuffer.size
1221
+ ) {
1222
+ // We can fit this next block of uniforms into the current active memory chunk
1223
+ buffer = this.currentUniformBuffer;
1224
+ } else if (this.uniformBufferPool.length > 0) {
1225
+ buffer = this.uniformBufferPool.pop();
1226
+ this.activeUniformBuffers.push(buffer);
1227
+ } else {
1228
+ // Kinda arbitrary. Each dynamic offset has to be in groups of 256, but then
1229
+ // we can choose how many things we want to be able to fit into a block.
1230
+ // There's some overhead to each block so if we're drawing a lot of stuff,
1231
+ // bigger is better. But it's also a lot of wasted memory if we AREN'T drawing
1232
+ // a lot of stuff. So.... right now it's 40. Feel free to update this if
1233
+ // a better balance can be achieved.
1234
+ const size = 256 * 40;
1235
+ buffer = {
1236
+ dynamic: true,
1237
+ lastOffset: 0,
1238
+ offset: 0,
1239
+ size,
1240
+ buffer: this.device.createBuffer({
1241
+ size,
1242
+ usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
1243
+ mappedAtCreation: true,
1244
+ }),
1245
+ uniformBuffer: this.device.createBuffer({
1246
+ size,
1247
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1248
+ }),
1249
+ };
1250
+
1251
+ buffer.data = new Float32Array(buffer.buffer.getMappedRange());
1252
+ buffer.dataView = new DataView(buffer.data.buffer);
1253
+
1254
+ this.activeUniformBuffers.push(buffer);
1255
+ }
1256
+
1257
+ this.currentUniformBuffer = buffer;
1258
+
1259
+ return buffer;
1260
+ }
1261
+
1105
1262
  _returnUniformBuffersToPool() {
1106
1263
  // Return all used buffers back to their pools for all registered shaders
1107
1264
  for (const shader of this._shadersWithPools) {
1108
- if (shader._uniformBuffersInUse && shader._uniformBuffersInUse.length > 0) {
1109
- this._returnShaderBuffersToPool(shader);
1110
- }
1265
+ this._returnShaderBuffersToPool(shader);
1111
1266
  }
1112
1267
  }
1113
1268
 
1114
1269
  _returnShaderBuffersToPool(shader) {
1115
- // Move all buffers from inUse back to pool
1116
- while (shader._uniformBuffersInUse.length > 0) {
1117
- const bufferInfo = shader._uniformBuffersInUse.pop();
1118
- shader._uniformBufferPool.push(bufferInfo);
1270
+ if (shader._uniformBufferGroups) {
1271
+ for (const bufferGroup of shader._uniformBufferGroups) {
1272
+ while (bufferGroup.nextBufferPool.length > 0) {
1273
+ bufferGroup.bufferPool.push(bufferGroup.nextBufferPool.pop());
1274
+ }
1275
+ for (const bufferInfo of bufferGroup.buffersInUse.keys()) {
1276
+ if (bufferInfo !== bufferGroup.currentBuffer) {
1277
+ bufferGroup.nextBufferPool.push(bufferInfo);
1278
+ }
1279
+ }
1280
+ bufferGroup.buffersInUse.clear();
1281
+ if (bufferGroup.currentBuffer) {
1282
+ bufferGroup.buffersInUse.add(bufferGroup.currentBuffer);
1283
+ }
1284
+ }
1119
1285
  }
1120
1286
  }
1121
1287
 
1122
1288
  flushDraw() {
1289
+ this._finishActiveRenderPass();
1123
1290
  // Only submit if we actually had any draws
1124
1291
  if (this._hasPendingDraws) {
1125
1292
  // Create a copy of pending command encoders
@@ -1127,9 +1294,41 @@ function rendererWebGPU(p5, fn) {
1127
1294
  this._pendingCommandEncoders = [];
1128
1295
  this._hasPendingDraws = false;
1129
1296
 
1297
+ if (this.activeUniformBuffers.length > 0) {
1298
+ const encoder = this.device.createCommandEncoder();
1299
+ for (const bufferInfo of this.activeUniformBuffers) {
1300
+ bufferInfo.buffer.unmap();
1301
+ encoder.copyBufferToBuffer(
1302
+ bufferInfo.buffer,
1303
+ bufferInfo.uniformBuffer,
1304
+ );
1305
+ }
1306
+ commandsToSubmit.unshift(encoder.finish());
1307
+ }
1308
+
1130
1309
  // Submit the commands
1131
1310
  this.queue.submit(commandsToSubmit);
1132
1311
 
1312
+ for (const buf of this.activeUniformBuffers) {
1313
+ // buf.buffer = this.device.createBuffer({
1314
+ // size: buf.size,
1315
+ // usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
1316
+ // mappedAtCreation: true,
1317
+ // });
1318
+ buf.offset = 0;
1319
+ buf.lastOffset = 0;
1320
+ // this.resettingUniformBuffers.push(
1321
+ buf.buffer.mapAsync(GPUMapMode.WRITE).then(() => {
1322
+ buf.data = new Float32Array(buf.buffer.getMappedRange());
1323
+ buf.dataView = new DataView(buf.data.buffer);
1324
+ this.uniformBufferPool.push(buf);
1325
+ return buf;
1326
+ });
1327
+ // )
1328
+ }
1329
+ this.activeUniformBuffers = [];
1330
+ this.currentUniformBuffer = undefined;
1331
+
1133
1332
  // Execute post-submit callbacks after GPU work completes
1134
1333
  if (this._postSubmitCallbacks.length > 0) {
1135
1334
  const callbacks = this._postSubmitCallbacks;
@@ -1203,15 +1402,21 @@ function rendererWebGPU(p5, fn) {
1203
1402
  this._markGeometryBuffersForReturn(geometry);
1204
1403
  }
1205
1404
 
1405
+ // this.uniformBufferPool.push(...(await Promise.all(this.resettingUniformBuffers)));
1406
+ this.resettingUniformBuffers = [];
1407
+
1206
1408
  // Return all vertex buffers to their pools
1207
1409
  this._returnVertexBuffersToPool();
1208
1410
 
1209
1411
  // Destroy all retired buffers
1210
- for (const buffer of this._retiredBuffers) {
1211
- if (buffer && buffer.destroy) {
1212
- buffer.destroy();
1412
+ const retired = this._retiredBuffers;
1413
+ this._postSubmitCallbacks.push(() => {
1414
+ for (const buffer of retired) {
1415
+ if (buffer && buffer.destroy) {
1416
+ buffer.destroy();
1417
+ }
1213
1418
  }
1214
- }
1419
+ });
1215
1420
  this._retiredBuffers = [];
1216
1421
 
1217
1422
  if (this._frameState === FRAME_STATE.PROMOTED) {
@@ -1239,50 +1444,15 @@ function rendererWebGPU(p5, fn) {
1239
1444
  this._promoteToFramebufferWithoutCopy();
1240
1445
  }
1241
1446
 
1242
- const commandEncoder = this.device.createCommandEncoder();
1243
-
1244
- // Use framebuffer texture if active, otherwise use canvas texture
1245
- const activeFramebuffer = this.activeFramebuffer();
1246
-
1247
- const colorAttachment = {
1248
- view: activeFramebuffer
1249
- ? (activeFramebuffer.aaColorTexture
1250
- ? activeFramebuffer.aaColorTextureView
1251
- : activeFramebuffer.colorTextureView)
1252
- : this._getCanvasColorTextureView(),
1253
- loadOp: "load",
1254
- storeOp: "store",
1255
- // If using multisampled texture, resolve to non-multisampled texture
1256
- resolveTarget: activeFramebuffer && activeFramebuffer.aaColorTexture
1257
- ? activeFramebuffer.colorTextureView
1258
- : undefined,
1259
- };
1260
-
1261
- // Use framebuffer depth texture if active, otherwise use canvas depth texture
1262
- const depthTextureView = activeFramebuffer
1263
- ? (activeFramebuffer.aaDepthTexture
1264
- ? activeFramebuffer.aaDepthTextureView
1265
- : activeFramebuffer.depthTextureView)
1266
- : this.depthTextureView;
1267
- const renderPassDescriptor = {
1268
- colorAttachments: [colorAttachment],
1269
- depthStencilAttachment: depthTextureView
1270
- ? {
1271
- view: depthTextureView,
1272
- depthLoadOp: "load",
1273
- depthStoreOp: "store",
1274
- depthClearValue: 1.0,
1275
- stencilLoadOp: "load",
1276
- stencilStoreOp: "store",
1277
- depthReadOnly: false,
1278
- stencilReadOnly: false,
1279
- }
1280
- : undefined,
1281
- };
1282
-
1283
- const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
1447
+ this._beginActiveRenderPass();
1448
+ const passEncoder = this.activeRenderPass;
1284
1449
  const currentShader = this._curShader;
1285
- passEncoder.setPipeline(currentShader.getPipeline(this._shaderOptions({ mode })));
1450
+ const shaderOptions = this._shaderOptions({ mode });
1451
+ if (this.activeShader !== currentShader || this._shaderOptionsDifferent(shaderOptions)) {
1452
+ passEncoder.setPipeline(currentShader.getPipeline(shaderOptions));
1453
+ }
1454
+ this.activeShader = currentShader;
1455
+ this.activeShaderOptions = shaderOptions;
1286
1456
 
1287
1457
  // Set stencil reference value for clipping
1288
1458
  const drawTarget = this.drawTarget();
@@ -1296,52 +1466,125 @@ function rendererWebGPU(p5, fn) {
1296
1466
  passEncoder.setStencilReference(1);
1297
1467
  }
1298
1468
  // Bind vertex buffers
1299
- for (const buffer of this._getVertexBuffers(currentShader)) {
1469
+ for (const buffer of currentShader._vertexBuffers || this._getVertexBuffers(currentShader)) {
1300
1470
  const location = currentShader.attributes[buffer.attr].location;
1301
1471
  const gpuBuffer = buffers[buffer.dst];
1302
1472
  passEncoder.setVertexBuffer(location, gpuBuffer, 0);
1303
1473
  }
1304
- // Bind uniforms - get a buffer from the pool
1305
- const uniformBufferInfo = this._getUniformBufferFromPool(currentShader);
1306
- this._packUniforms(currentShader, uniformBufferInfo);
1307
- this.device.queue.writeBuffer(
1308
- uniformBufferInfo.buffer,
1309
- 0,
1310
- uniformBufferInfo.data.buffer,
1311
- uniformBufferInfo.data.byteOffset,
1312
- uniformBufferInfo.data.byteLength
1313
- );
1314
1474
 
1315
- // Bind sampler/texture uniforms
1316
- for (const [group, entries] of currentShader._groupEntries) {
1317
- const bgEntries = entries.map(entry => {
1318
- if (group === 0 && entry.binding === 0) {
1319
- return {
1320
- binding: 0,
1321
- resource: { buffer: uniformBufferInfo.buffer },
1322
- };
1475
+ for (const bufferGroup of currentShader._uniformBufferGroups) {
1476
+ if (bufferGroup.dynamic) {
1477
+ // Bind uniforms into a part of a big dynamic memory block because
1478
+ // the group changes often
1479
+ const uniformBufferInfo = this._getDynamicUniformBufferFromPool(bufferGroup);
1480
+ if (currentShader._lastDynamicBuffer[bufferGroup.cacheKey] !== uniformBufferInfo) {
1481
+ currentShader._cachedBindGroup[bufferGroup.group] = undefined;
1482
+ currentShader._lastDynamicBuffer[bufferGroup.cacheKey] = uniformBufferInfo;
1323
1483
  }
1484
+ this._packUniformGroup(currentShader, bufferGroup.uniforms, uniformBufferInfo);
1485
+ uniformBufferInfo.lastOffset = uniformBufferInfo.offset;
1486
+ uniformBufferInfo.offset += Math.ceil(bufferGroup.size / this.uniformBufferAlignment) * this.uniformBufferAlignment;
1324
1487
 
1325
- if (!entry.uniform.isSampler) {
1326
- throw new Error(
1327
- 'All non-texture/sampler uniforms should be in the uniform struct!'
1488
+ // Make a shallow copy so that we keep track of the last offset for this uniform
1489
+ bufferGroup.currentDynamicBuffer = uniformBufferInfo;
1490
+ bufferGroup.lastOffset = uniformBufferInfo.lastOffset;
1491
+ } else {
1492
+ // Bind uniforms to a binding-specific buffer, which may be cached for performance
1493
+ let bufferInfo;
1494
+ const dataChanged = this._hasGroupDataChanged(currentShader, bufferGroup);
1495
+
1496
+ if (!dataChanged && bufferGroup.currentBuffer) {
1497
+ // Reuse the cached buffer - no need to pack or write
1498
+ bufferInfo = bufferGroup.currentBuffer;
1499
+ bufferGroup.buffersInUse.add(bufferInfo);
1500
+ } else {
1501
+ // Data changed - get a new buffer and write to it
1502
+ bufferInfo = this._getUniformBufferFromPool(bufferGroup);
1503
+ this._packUniformGroup(currentShader, bufferGroup.uniforms, bufferInfo);
1504
+ this.device.queue.writeBuffer(
1505
+ bufferInfo.buffer,
1506
+ 0,
1507
+ bufferInfo.data.buffer,
1508
+ bufferInfo.data.byteOffset,
1509
+ bufferInfo.data.byteLength
1328
1510
  );
1511
+
1512
+ currentShader.buffersDirty.delete(bufferGroup.group * 1000 + bufferGroup.binding);
1513
+ currentShader._cachedBindGroup[bufferGroup.group] = undefined;
1514
+
1515
+ // Cache this buffer and data for next frame
1516
+ bufferGroup.currentBuffer = bufferInfo;
1329
1517
  }
1518
+ }
1519
+ }
1520
+ for (const sampler of currentShader.samplers) {
1521
+ const key = sampler.group * 1000 + sampler.binding;
1522
+ if (currentShader.buffersDirty.has(key)) {
1523
+ currentShader._cachedBindGroup[sampler.group] = undefined;
1524
+ currentShader.buffersDirty.delete(key);
1525
+ }
1526
+ }
1330
1527
 
1331
- return {
1332
- binding: entry.binding,
1333
- resource: entry.uniform.type === 'sampler'
1334
- ? (entry.uniform.textureSource.texture || this._getEmptyTexture()).getSampler()
1335
- : (entry.uniform.texture || this._getEmptyTexture()).textureHandle.view,
1336
- };
1337
- });
1528
+ // Bind sampler/texture uniforms and uniform buffers
1529
+ for (const iter of currentShader._groupEntries) {
1530
+ const group = iter[0];
1531
+ const entries = iter[1];
1532
+ let dynamicOffsetIdx = 0;
1533
+ const bgEntries = [];
1534
+ let bindGroup = currentShader._cachedBindGroup[group];
1535
+ for (const entry of entries) {
1536
+ const bufferGroup = entry.bufferGroup;
1537
+ // Check if this is a uniform buffer binding
1538
+ const uniformBufferInfo =
1539
+ bufferGroup?.currentBuffer || bufferGroup?.currentDynamicBuffer;
1540
+ if (uniformBufferInfo) {
1541
+ if (bufferGroup.dynamic) {
1542
+ this.dynamicEntryOffsets[dynamicOffsetIdx++] = bufferGroup.lastOffset;
1543
+ }
1544
+ if (!bindGroup) {
1545
+ bgEntries.push({
1546
+ binding: entry.binding,
1547
+ resource: bufferGroup.dynamic
1548
+ ? {
1549
+ buffer: uniformBufferInfo.uniformBuffer,
1550
+ offset: 0,
1551
+ size: Math.ceil(bufferGroup.size / this.uniformBufferAlignment) * this.uniformBufferAlignment,
1552
+ }
1553
+ : { buffer: uniformBufferInfo.buffer },
1554
+ });
1555
+ }
1556
+ } else if (!bindGroup) {
1557
+ bgEntries.push({
1558
+ binding: entry.binding,
1559
+ resource: entry.uniform.type === 'sampler'
1560
+ ? (entry.uniform.textureSource.texture || this._getEmptyTexture()).getSampler()
1561
+ : (entry.uniform.texture || this._getEmptyTexture()).textureHandle.view,
1562
+ });
1563
+ }
1564
+ }
1338
1565
 
1339
1566
  const layout = currentShader._bindGroupLayouts[group];
1340
- const bindGroup = this.device.createBindGroup({
1341
- layout,
1342
- entries: bgEntries,
1343
- });
1344
- passEncoder.setBindGroup(group, bindGroup);
1567
+ if (!bindGroup) {
1568
+ bindGroup = this.device.createBindGroup({
1569
+ layout,
1570
+ entries: bgEntries,
1571
+ });
1572
+ }
1573
+ currentShader._cachedBindGroup[group] = bindGroup;
1574
+ if (dynamicOffsetIdx === 0) {
1575
+ passEncoder.setBindGroup(
1576
+ group,
1577
+ bindGroup,
1578
+ );
1579
+ } else {
1580
+ passEncoder.setBindGroup(
1581
+ group,
1582
+ bindGroup,
1583
+ this.dynamicEntryOffsets,
1584
+ 0,
1585
+ dynamicOffsetIdx
1586
+ );
1587
+ }
1345
1588
  }
1346
1589
 
1347
1590
  if (currentShader.shaderType === "fill") {
@@ -1366,11 +1609,6 @@ function rendererWebGPU(p5, fn) {
1366
1609
  passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0);
1367
1610
  }
1368
1611
 
1369
- passEncoder.end();
1370
-
1371
- // Store the command encoder for later submission
1372
- this._pendingCommandEncoders.push(commandEncoder.finish());
1373
-
1374
1612
  // Mark that we have pending draws that need submission
1375
1613
  this._hasPendingDraws = true;
1376
1614
  }
@@ -1379,46 +1617,62 @@ function rendererWebGPU(p5, fn) {
1379
1617
  // SHADER
1380
1618
  //////////////////////////////////////////////
1381
1619
 
1382
- _packUniforms(shader, bufferInfo) {
1620
+ _packUniformGroup(shader, groupUniforms, bufferInfo) {
1621
+ // Pack a single group's uniforms into a buffer
1383
1622
  const data = bufferInfo.data;
1384
1623
  const dataView = bufferInfo.dataView;
1385
1624
 
1386
- for (const name in shader.uniforms) {
1387
- const uniform = shader.uniforms[name];
1388
- if (uniform.isSampler) continue;
1625
+ const offset = bufferInfo.offset || 0;
1626
+ for (const uniform of groupUniforms) {
1627
+ const fullUniform = shader.uniforms[uniform.name];
1628
+ if (!fullUniform || fullUniform.isSampler) continue;
1629
+ const uniformData = fullUniform._mappedData;
1389
1630
 
1390
- if (uniform.baseType === 'u32') {
1391
- if (uniform.size === 4) {
1392
- // Single u32
1393
- dataView.setUint32(uniform.offset, uniform._cachedData, true);
1631
+ if (fullUniform.baseType === 'u32') {
1632
+ if (fullUniform.size === 4) {
1633
+ dataView.setUint32(offset + fullUniform.offset, uniformData, true);
1394
1634
  } else {
1395
- // Vector of u32s
1396
- const uniformData = uniform._cachedData;
1397
1635
  for (let i = 0; i < uniformData.length; i++) {
1398
- dataView.setUint32(uniform.offset + i * 4, uniformData[i], true);
1636
+ dataView.setUint32(offset + fullUniform.offset + i * 4, uniformData[i], true);
1399
1637
  }
1400
1638
  }
1401
- } else if (uniform.baseType === 'i32') {
1402
- if (uniform.size === 4) {
1403
- // Single i32
1404
- dataView.setInt32(uniform.offset, uniform._cachedData, true);
1639
+ } else if (fullUniform.baseType === 'i32') {
1640
+ if (fullUniform.size === 4) {
1641
+ dataView.setInt32(offset + fullUniform.offset, uniformData, true);
1405
1642
  } else {
1406
- // Vector of i32s
1407
- const uniformData = uniform._cachedData;
1408
1643
  for (let i = 0; i < uniformData.length; i++) {
1409
- dataView.setInt32(uniform.offset + i * 4, uniformData[i], true);
1644
+ dataView.setInt32(offset + fullUniform.offset + i * 4, uniformData[i], true);
1410
1645
  }
1411
1646
  }
1412
- } else if (uniform.size === 4) {
1413
- // Single float value
1414
- data.set([uniform._cachedData], uniform.offset / 4);
1415
- } else if (uniform._cachedData !== undefined) {
1416
- // Float array (including vec2<f32>, vec3<f32>, vec4<f32>, mat4x4<f32>)
1417
- data.set(uniform._cachedData, uniform.offset / 4);
1647
+ } else if (fullUniform.packInPlace) {
1648
+ // In-place packing for mat3: write directly to buffer with padding
1649
+ const baseOffset = (offset + fullUniform.offset) / 4;
1650
+ // Column 0
1651
+ data[baseOffset + 0] = uniformData[0];
1652
+ data[baseOffset + 1] = uniformData[1];
1653
+ data[baseOffset + 2] = uniformData[2];
1654
+ // Column 1
1655
+ data[baseOffset + 4] = uniformData[3];
1656
+ data[baseOffset + 5] = uniformData[4];
1657
+ data[baseOffset + 6] = uniformData[5];
1658
+ // Column 2
1659
+ data[baseOffset + 8] = uniformData[6];
1660
+ data[baseOffset + 9] = uniformData[7];
1661
+ data[baseOffset + 10] = uniformData[8];
1662
+ } else if (fullUniform.size === 4) {
1663
+ data.set([uniformData], (offset + fullUniform.offset) / 4);
1664
+ } else if (uniformData !== undefined) {
1665
+ data.set(uniformData, (offset + fullUniform.offset) / 4);
1418
1666
  }
1419
1667
  }
1420
1668
  }
1421
1669
 
1670
+ _hasGroupDataChanged(shader, bufferGroup) {
1671
+ // First time
1672
+ if (!bufferGroup.currentBuffer) return true;
1673
+ return shader.buffersDirty.has(bufferGroup.group * 1000 + bufferGroup.binding);
1674
+ }
1675
+
1422
1676
  _parseStruct(shaderSource, structName) {
1423
1677
  const structMatch = shaderSource.match(
1424
1678
  new RegExp(`struct\\s+${structName}\\s*\\{([^\\}]+)\\}`)
@@ -1462,17 +1716,16 @@ function rendererWebGPU(p5, fn) {
1462
1716
  const align = dim === 2 ? 8 : 16;
1463
1717
  // Each column must be aligned
1464
1718
  const size = Math.ceil(dim * 4 / align) * align * dim;
1719
+ // For mat3, use in-place packing to avoid array allocation
1465
1720
  const pack = dim === 3
1466
1721
  ? (data) => [
1467
1722
  ...data.slice(0, 3),
1468
- 0,
1469
1723
  ...data.slice(3, 6),
1470
- 0,
1471
1724
  ...data.slice(6, 9),
1472
- 0
1473
1725
  ]
1474
1726
  : undefined;
1475
- return { align, size, pack, items: dim * dim, baseType: 'f32' };
1727
+ const packInPlace = dim === 3;
1728
+ return { align, size, pack, packInPlace, items: dim * dim, baseType: 'f32' };
1476
1729
  }
1477
1730
  if (/^array<.+>$/.test(type)) {
1478
1731
  const [, subtype, rawLength] = type.match(/^array<(.+),\s*(\d+)>/);
@@ -1509,7 +1762,7 @@ function rendererWebGPU(p5, fn) {
1509
1762
 
1510
1763
  while ((match = elementRegex.exec(structBody)) !== null) {
1511
1764
  const [_, location, name, type] = match;
1512
- const { size, align, pack, baseType } = baseAlignAndSize(type);
1765
+ const { size, align, pack, packInPlace, baseType } = baseAlignAndSize(type);
1513
1766
  offset = Math.ceil(offset / align) * align;
1514
1767
  const offsetEnd = offset + size;
1515
1768
  elements[name] = {
@@ -1521,6 +1774,7 @@ function rendererWebGPU(p5, fn) {
1521
1774
  offset,
1522
1775
  offsetEnd,
1523
1776
  pack,
1777
+ packInPlace,
1524
1778
  baseType
1525
1779
  };
1526
1780
  index++;
@@ -1546,22 +1800,64 @@ function rendererWebGPU(p5, fn) {
1546
1800
  }
1547
1801
 
1548
1802
  getUniformMetadata(shader) {
1549
- // Currently, for ease of parsing, we enforce that the first bind group is a
1550
- // struct, which contains all non-sampler uniforms. Then, any subsequent
1551
- // groups contain samplers.
1552
-
1553
- // Extract the struct name from the uniform variable declaration
1554
- const uniformVarRegex = /@group\(0\)\s+@binding\(0\)\s+var<uniform>\s+(\w+)\s*:\s*(\w+);/;
1555
- const uniformVarMatch = uniformVarRegex.exec(shader.vertSrc());
1556
- if (!uniformVarMatch) {
1557
- throw new Error('Expected a uniform struct bound to @group(0) @binding(0)');
1558
- }
1559
- const structType = uniformVarMatch[2];
1560
- const uniforms = this._parseStruct(shader.vertSrc(), structType);
1803
+ // Parse all uniform struct bindings in group 0.
1804
+ // TODO: support non-sampler uniforms being in other groups
1805
+
1806
+ // Each binding represents a logical group of uniforms, since they get
1807
+ // updated or cached all at once.
1808
+
1809
+ const uniformGroups = [];
1810
+ const uniformVarRegex = /@group\((\d+)\)\s+@binding\((\d+)\)\s+var<uniform>\s+(\w+)\s*:\s*(\w+);/g;
1811
+
1812
+ let match;
1813
+ while ((match = uniformVarRegex.exec(shader.vertSrc())) !== null) {
1814
+ const [_, groupNum, binding, varName, structType] = match;
1815
+ const bindingIndex = parseInt(binding);
1816
+ const uniforms = this._parseStruct(shader.vertSrc(), structType);
1817
+
1818
+ uniformGroups.push({
1819
+ group: parseInt(groupNum),
1820
+ binding: bindingIndex,
1821
+ varName,
1822
+ structType,
1823
+ uniforms
1824
+ });
1825
+ }
1826
+
1827
+ if (uniformGroups.length === 0) {
1828
+ throw new Error('Expected at least one uniform struct bound to @group(0)');
1829
+ }
1830
+
1831
+ // While we're also keeping track of the groups, the API we expose
1832
+ // to users of p5 is just a flat list of uniforms (which can be the
1833
+ // individual struct items in the group.)
1834
+ const allUniforms = {};
1835
+ for (const group of uniformGroups) {
1836
+ for (const [uniformName, uniformData] of Object.entries(group.uniforms)) {
1837
+ allUniforms[uniformName] = {
1838
+ ...uniformData,
1839
+ group: group.group,
1840
+ binding: group.binding,
1841
+ varName: group.varName
1842
+ };
1843
+ }
1844
+ }
1845
+
1846
+ // Store uniform groups for buffer pooling
1847
+ shader._uniformGroups = uniformGroups;
1848
+
1561
1849
  // Extract samplers from group bindings
1562
1850
  const samplers = {};
1563
1851
  // TODO: support other texture types
1564
1852
  const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d<f32>|sampler);/g;
1853
+
1854
+ // Track which bindings are taken by the struct properties we've parsed
1855
+ // (the rest should be textures/samplers)
1856
+ const structUniformBindings = {};
1857
+ for (const g of uniformGroups) {
1858
+ structUniformBindings[g.group + ',' + g.binding] = true;
1859
+ }
1860
+
1565
1861
  for (const [src, visibility] of [
1566
1862
  [shader.vertSrc(), GPUShaderStage.VERTEX],
1567
1863
  [shader.fragSrc(), GPUShaderStage.FRAGMENT]
@@ -1571,10 +1867,8 @@ function rendererWebGPU(p5, fn) {
1571
1867
  const [_, group, binding, name, type] = match;
1572
1868
  const groupIndex = parseInt(group);
1573
1869
  const bindingIndex = parseInt(binding);
1574
- // We're currently reserving group 0 for non-sampler stuff, which we parse
1575
- // above, so we can skip it here while we grab the remaining sampler
1576
- // uniforms
1577
- if (groupIndex === 0 && bindingIndex === 0) continue;
1870
+ // Skip struct uniform bindings which we've already parsed
1871
+ if (structUniformBindings[groupIndex + ',' + bindingIndex]) continue;
1578
1872
 
1579
1873
  const key = `${groupIndex},${bindingIndex}`;
1580
1874
  samplers[key] = {
@@ -1603,17 +1897,17 @@ function rendererWebGPU(p5, fn) {
1603
1897
  }
1604
1898
  }
1605
1899
  }
1606
- return [...Object.values(uniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers)];
1900
+ return [...Object.values(allUniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers)];
1607
1901
  }
1608
1902
 
1609
- getNextBindingIndex(shader, group = 0) {
1903
+ getNextBindingIndex({ vert, frag }, group = 0) {
1610
1904
  // Get the highest binding index in the specified group and return the next available
1611
1905
  const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d<f32>|sampler|uniform)/g;
1612
1906
  let maxBindingIndex = -1;
1613
1907
 
1614
1908
  for (const [src, visibility] of [
1615
- [shader.vertSrc(), GPUShaderStage.VERTEX],
1616
- [shader.fragSrc(), GPUShaderStage.FRAGMENT]
1909
+ [vert, GPUShaderStage.VERTEX],
1910
+ [frag, GPUShaderStage.FRAGMENT]
1617
1911
  ]) {
1618
1912
  let match;
1619
1913
  while ((match = samplerRegex.exec(src)) !== null) {
@@ -1627,11 +1921,14 @@ function rendererWebGPU(p5, fn) {
1627
1921
  return maxBindingIndex + 1;
1628
1922
  }
1629
1923
 
1630
- updateUniformValue(_shader, uniform, data) {
1924
+ updateUniformValue(shader, uniform, data) {
1631
1925
  if (uniform.isSampler) {
1632
1926
  uniform.texture =
1633
1927
  data instanceof Texture ? data : this.getTexture(data);
1928
+ } else {
1929
+ uniform._mappedData = this._mapUniformData(uniform, uniform._cachedData);
1634
1930
  }
1931
+ shader.buffersDirty.add(uniform.group * 1000 + uniform.binding);
1635
1932
  }
1636
1933
 
1637
1934
  _updateTexture(uniform, tex) {
@@ -1868,6 +2165,7 @@ function rendererWebGPU(p5, fn) {
1868
2165
  }
1869
2166
 
1870
2167
  _clearClipBuffer() {
2168
+ this._finishActiveRenderPass();
1871
2169
  const commandEncoder = this.device.createCommandEncoder();
1872
2170
 
1873
2171
  const activeFramebuffer = this.activeFramebuffer();
@@ -1951,12 +2249,33 @@ function rendererWebGPU(p5, fn) {
1951
2249
  }
1952
2250
  }
1953
2251
 
1954
- let uniforms = '';
2252
+ // Inject hook uniforms as a separate struct at a new binding
2253
+ let hookUniformFields = '';
1955
2254
  for (const key in shader.hooks.uniforms) {
1956
2255
  // WGSL format: "name: type"
1957
- uniforms += `${key},\n`;
2256
+ hookUniformFields += ` ${key},\n`;
2257
+ }
2258
+
2259
+ if (hookUniformFields) {
2260
+ // Find the next available binding in group 0
2261
+ // Use the source we're currently building (preMain) which has texture bindings. We can't call `fragSrc()`
2262
+ // or `vertSrc()` because we may be in one of those calls already, and might infinite loop
2263
+ const nextBinding = this.getNextBindingIndex({
2264
+ vert: shaderType === 'vertex' ? preMain + (shader.hooks.vertex?.declarations ?? '') + shader.hooks.declarations : shader._vertSrc,
2265
+ frag: shaderType === 'fragment' ? preMain + (shader.hooks.fragment?.declarations ?? '') + shader.hooks.declarations : shader._fragSrc,
2266
+ }, 0);
2267
+
2268
+ // Create HookUniforms struct and binding
2269
+ const hookUniformsDecl = `
2270
+ // Hook Uniforms (from .modify())
2271
+ struct HookUniforms {
2272
+ ${hookUniformFields}}
2273
+
2274
+ @group(0) @binding(${nextBinding}) var<uniform> hooks: HookUniforms;
2275
+ `;
2276
+ // Insert before the first @group binding
2277
+ preMain = preMain.replace(/(@group\(0\)\s+@binding)/, `${hookUniformsDecl}\n$1`);
1958
2278
  }
1959
- preMain = preMain.replace(/struct\s+Uniforms\s+\{/, `$&\n${uniforms}`);
1960
2279
 
1961
2280
  // Handle varying variables by injecting them into VertexOutput and FragmentInput structs
1962
2281
  if (shader.hooks.varyingVariables && shader.hooks.varyingVariables.length > 0) {
@@ -2427,6 +2746,7 @@ function rendererWebGPU(p5, fn) {
2427
2746
  }
2428
2747
 
2429
2748
  _clearFramebufferTextures(framebuffer) {
2749
+ this._finishActiveRenderPass();
2430
2750
  const commandEncoder = this.device.createCommandEncoder();
2431
2751
 
2432
2752
  // Clear the color texture (and multisampled texture if it exists)
@@ -2867,6 +3187,7 @@ function rendererWebGPU(p5, fn) {
2867
3187
  * Copy framebuffer content directly to WebGPU texture mip level
2868
3188
  */
2869
3189
  _accumulateMipLevel(framebuffer, mipmapData, mipLevel, width, height) {
3190
+ this.flushDraw();
2870
3191
  // Copy from framebuffer texture to the mip level
2871
3192
  const commandEncoder = this.device.createCommandEncoder();
2872
3193