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
@@ -11,7 +11,7 @@ const _PI = Math.PI;
11
11
  * @property {String} VERSION
12
12
  * @final
13
13
  */
14
- const VERSION = '2.2.3';
14
+ const VERSION = '2.3.0-rc.0';
15
15
 
16
16
  // GRAPHICS RENDERER
17
17
  /**
@@ -1433,6 +1433,7 @@ const NodeType = {
1433
1433
  STATEMENT: 'statement',
1434
1434
  ASSIGNMENT: 'assignment',
1435
1435
  };
1436
+ const INSTANCE_ID_VARYING_NAME = '_p5_instanceID';
1436
1437
  const NodeTypeToName = Object.fromEntries(
1437
1438
  Object.entries(NodeType).map(([key, val]) => [val, key])
1438
1439
  );
@@ -1526,6 +1527,7 @@ const OpCode = {
1526
1527
  LOGICAL_AND: 11,
1527
1528
  LOGICAL_OR: 12,
1528
1529
  MEMBER_ACCESS: 13,
1530
+ ARRAY_ACCESS: 14,
1529
1531
  },
1530
1532
  Unary: {
1531
1533
  LOGICAL_NOT: 100,
@@ -1536,7 +1538,7 @@ const OpCode = {
1536
1538
  Nary: {
1537
1539
  FUNCTION_CALL: 200,
1538
1540
  CONSTRUCTOR: 201,
1539
- }};
1541
+ TERNARY: 202}};
1540
1542
  const OperatorTable = [
1541
1543
  { arity: "unary", boolean: true, name: "not", symbol: "!", opCode: OpCode.Unary.LOGICAL_NOT },
1542
1544
  { arity: "unary", name: "neg", symbol: "-", opCode: OpCode.Unary.NEGATE },
@@ -1699,7 +1701,7 @@ ${uniforms$5}
1699
1701
  @fragment
1700
1702
  fn main(input: FragmentInput) -> @location(0) vec4<f32> {
1701
1703
  HOOK_beforeFragment();
1702
- var outColor = HOOK_getFinalColor(input.vColor);
1704
+ var outColor = HOOK_getFinalColor(input.vColor, input.vVertTexCoord);
1703
1705
  outColor = vec4<f32>(outColor.rgb * outColor.a, outColor.a);
1704
1706
  HOOK_afterFragment();
1705
1707
  return outColor;
@@ -2070,7 +2072,7 @@ fn main(input: StrokeFragmentInput) -> @location(0) vec4<f32> {
2070
2072
  discard;
2071
2073
  }
2072
2074
  }
2073
- var col = HOOK_getFinalColor(inputs.color);
2075
+ var col = HOOK_getFinalColor(inputs.color, vec2<f32>(0.0, 0.0));
2074
2076
  col = vec4<f32>(col.rgb, 1.0) * col.a;
2075
2077
  HOOK_afterFragment();
2076
2078
  return vec4<f32>(col);
@@ -2495,9 +2497,9 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
2495
2497
  inputs.emissiveMaterial
2496
2498
  );
2497
2499
 
2498
- var outColor = HOOK_getFinalColor(
2499
- HOOK_combineColors(components)
2500
- );
2500
+ var outColor = HOOK_getFinalColor(
2501
+ HOOK_combineColors(components), input.vTexCoord
2502
+ );
2501
2503
  outColor = vec4<f32>(outColor.rgb * outColor.a, outColor.a);
2502
2504
  HOOK_afterFragment();
2503
2505
  return outColor;
@@ -2849,6 +2851,201 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
2849
2851
  }
2850
2852
  `;
2851
2853
 
2854
+ // Based on https://github.com/stegu/webgl-noise/blob/22434e04d7753f7e949e8d724ab3da2864c17a0f/src/noise3D.glsl
2855
+ // MIT licensed, adapted for p5.strands and converted to WGSL
2856
+
2857
+ var noiseWGSL = `fn mod289Vec3(x: vec3<f32>) -> vec3<f32> {
2858
+ return x - floor(x * (1.0 / 289.0)) * 289.0;
2859
+ }
2860
+
2861
+ fn mod289Vec4(x: vec4<f32>) -> vec4<f32> {
2862
+ return x - floor(x * (1.0 / 289.0)) * 289.0;
2863
+ }
2864
+
2865
+ fn permute(x: vec4<f32>) -> vec4<f32> {
2866
+ return mod289Vec4(((x*34.0)+10.0)*x);
2867
+ }
2868
+
2869
+ fn taylorInvSqrt(r: vec4<f32>) -> vec4<f32> {
2870
+ return vec4<f32>(1.79284291400159) - vec4<f32>(0.85373472095314) * r;
2871
+ }
2872
+
2873
+ fn baseNoise(v: vec3<f32>) -> f32 {
2874
+ let C = vec2<f32>(1.0/6.0, 1.0/3.0);
2875
+ let D = vec4<f32>(0.0, 0.5, 1.0, 2.0);
2876
+
2877
+ // First corner
2878
+ var i = floor(v + dot(v, C.yyy));
2879
+ let x0 = v - i + dot(i, C.xxx);
2880
+
2881
+ // Other corners
2882
+ let g = step(x0.yzx, x0.xyz);
2883
+ let l = vec3<f32>(1.0) - g;
2884
+ let i1 = min(g.xyz, l.zxy);
2885
+ let i2 = max(g.xyz, l.zxy);
2886
+
2887
+ // x0 = x0 - 0.0 + 0.0 * C.xxx;
2888
+ // x1 = x0 - i1 + 1.0 * C.xxx;
2889
+ // x2 = x0 - i2 + 2.0 * C.xxx;
2890
+ // x3 = x0 - 1.0 + 3.0 * C.xxx;
2891
+ let x1 = x0 - i1 + C.xxx;
2892
+ let x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
2893
+ let x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y
2894
+
2895
+ // Permutations
2896
+ i = mod289Vec3(i);
2897
+ let p = permute( permute( permute(
2898
+ i.z + vec4<f32>(0.0, i1.z, i2.z, 1.0 ))
2899
+ + i.y + vec4<f32>(0.0, i1.y, i2.y, 1.0 ))
2900
+ + i.x + vec4<f32>(0.0, i1.x, i2.x, 1.0 ));
2901
+
2902
+ // Gradients: 7x7 points over a square, mapped onto an octahedron.
2903
+ // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
2904
+ let n_ = 0.142857142857; // 1.0/7.0
2905
+ let ns = n_ * D.wyz - D.xzx;
2906
+
2907
+ let j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7)
2908
+
2909
+ let x_ = floor(j * ns.z);
2910
+ let y_ = floor(j - 7.0 * x_ ); // mod(j,N)
2911
+
2912
+ let x = x_ *ns.x + ns.yyyy;
2913
+ let y = y_ *ns.x + ns.yyyy;
2914
+ let h = vec4<f32>(1.0) - abs(x) - abs(y);
2915
+
2916
+ let b0 = vec4<f32>( x.xy, y.xy );
2917
+ let b1 = vec4<f32>( x.zw, y.zw );
2918
+
2919
+ //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
2920
+ //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
2921
+ let s0 = floor(b0)*2.0 + vec4<f32>(1.0);
2922
+ let s1 = floor(b1)*2.0 + vec4<f32>(1.0);
2923
+ let sh = -step(h, vec4<f32>(0.0));
2924
+
2925
+ let a0 = b0.xzyw + s0.xzyw*sh.xxyy;
2926
+ let a1 = b1.xzyw + s1.xzyw*sh.zzww;
2927
+
2928
+ let p0 = vec3<f32>(a0.xy, h.x);
2929
+ let p1 = vec3<f32>(a0.zw, h.y);
2930
+ let p2 = vec3<f32>(a1.xy, h.z);
2931
+ let p3 = vec3<f32>(a1.zw, h.w);
2932
+
2933
+ //Normalise gradients
2934
+ let norm = taylorInvSqrt(vec4<f32>(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
2935
+ let p0_norm = p0 * norm.x;
2936
+ let p1_norm = p1 * norm.y;
2937
+ let p2_norm = p2 * norm.z;
2938
+ let p3_norm = p3 * norm.w;
2939
+
2940
+ // Mix final noise value
2941
+ var m = max(vec4<f32>(0.5) - vec4<f32>(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), vec4<f32>(0.0));
2942
+ m = m * m;
2943
+ return 105.0 * dot( m*m, vec4<f32>( dot(p0_norm,x0), dot(p1_norm,x1),
2944
+ dot(p2_norm,x2), dot(p3_norm,x3) ) );
2945
+ }
2946
+
2947
+ fn noise(st: vec3<f32>, octaves: i32, ampFalloff: f32) -> f32 {
2948
+ var result = 0.0;
2949
+ var amplitude = 1.0;
2950
+ var frequency = 1.0;
2951
+
2952
+ for (var i = 0; i < 8; i++) {
2953
+ if (i >= octaves) { break; }
2954
+ result += amplitude * baseNoise(st * frequency);
2955
+ frequency *= 2.0;
2956
+ amplitude *= ampFalloff;
2957
+ }
2958
+ return (result + 1.0) * 0.5;
2959
+ }`;
2960
+
2961
+ // _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW)
2962
+ // Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/)
2963
+ // α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal)
2964
+ // α₂ = 1/φ₂² = 0.5698402910
2965
+ // 1/φ = 0.6180339887 (golden ratio conjugate)
2966
+ //
2967
+ // Fragment shader version: pixelCoord is passed in from main via @builtin(position).
2968
+
2969
+ var randomWGSL = `
2970
+ var<private> _p5_randomCallIndex: i32 = 0;
2971
+
2972
+ fn _p5_hash(p: vec3<f32>) -> f32 {
2973
+ var p3 = fract(p * vec3<f32>(0.1031, 0.1030, 0.0973));
2974
+ p3 = p3 + dot(p3, p3.yxz + 33.33);
2975
+ return fract((p3.x + p3.y) * p3.z);
2976
+ }
2977
+
2978
+ fn random(seed: f32, pixelCoord: vec2<f32>) -> f32 {
2979
+ let callIndex = f32(_p5_randomCallIndex);
2980
+ _p5_randomCallIndex = _p5_randomCallIndex + 1;
2981
+ let s = fract(seed * 0.7548776662);
2982
+ return _p5_hash(vec3<f32>(
2983
+ pixelCoord.x + s,
2984
+ pixelCoord.y + callIndex * 0.5698402910,
2985
+ s + callIndex * 0.6180339887
2986
+ ));
2987
+ }
2988
+ `;
2989
+
2990
+ // _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW)
2991
+ // Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/)
2992
+ // α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal)
2993
+ // α₂ = 1/φ₂² = 0.5698402910
2994
+ // 1/φ = 0.6180339887 (golden ratio conjugate)
2995
+ //
2996
+ // Vertex shader version: vertexId is passed in from main via @builtin(vertex_index).
2997
+
2998
+ var randomVertWGSL = `
2999
+ var<private> _p5_randomCallIndex: i32 = 0;
3000
+
3001
+ fn _p5_hash(p: vec3<f32>) -> f32 {
3002
+ var p3 = fract(p * vec3<f32>(0.1031, 0.1030, 0.0973));
3003
+ p3 = p3 + dot(p3, p3.yxz + 33.33);
3004
+ return fract((p3.x + p3.y) * p3.z);
3005
+ }
3006
+
3007
+ fn random(seed: f32, vertexId: f32) -> f32 {
3008
+ let callIndex = f32(_p5_randomCallIndex);
3009
+ _p5_randomCallIndex = _p5_randomCallIndex + 1;
3010
+ let s = fract(seed * 0.7548776662);
3011
+ return _p5_hash(vec3<f32>(
3012
+ vertexId + s,
3013
+ vertexId * 0.5698402910 + callIndex * 0.6180339887,
3014
+ s + callIndex * 0.7548776662
3015
+ ));
3016
+ }
3017
+ `;
3018
+
3019
+ // _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW)
3020
+ // Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/)
3021
+ // α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal)
3022
+ // α₂ = 1/φ₂² = 0.5698402910
3023
+ // 1/φ = 0.6180339887 (golden ratio conjugate)
3024
+ //
3025
+ // Compute shader version: invocationId is passed in from main via @builtin(global_invocation_id).
3026
+
3027
+ var randomComputeWGSL = `
3028
+ var<private> _p5_randomCallIndex: i32 = 0;
3029
+
3030
+ fn _p5_hash(p: vec3<f32>) -> f32 {
3031
+ var p3 = fract(p * vec3<f32>(0.1031, 0.1030, 0.0973));
3032
+ p3 = p3 + dot(p3, p3.yxz + 33.33);
3033
+ return fract((p3.x + p3.y) * p3.z);
3034
+ }
3035
+
3036
+ fn random(seed: f32, invocationId: vec3<u32>) -> f32 {
3037
+ let id = vec3<f32>(invocationId);
3038
+ let callIndex = f32(_p5_randomCallIndex);
3039
+ _p5_randomCallIndex = _p5_randomCallIndex + 1;
3040
+ let s = fract(seed * 0.7548776662);
3041
+ return _p5_hash(vec3<f32>(
3042
+ id.x + s,
3043
+ id.y + callIndex * 0.5698402910,
3044
+ id.z + s + callIndex * 0.6180339887
3045
+ ));
3046
+ }
3047
+ `;
3048
+
2852
3049
  function internalError(errorMessage) {
2853
3050
  const prefixedMessage = `[p5.strands internal error]: ${errorMessage}`;
2854
3051
  throw new Error(prefixedMessage);
@@ -2978,6 +3175,9 @@ class StrandsNode {
2978
3175
  this.strandsContext = strandsContext;
2979
3176
  this.dimension = dimension;
2980
3177
  this.structProperties = null;
3178
+ // Schema for struct storage buffers (set by uniformStorage when buffer has a struct layout).
3179
+ // When set, buf.get(idx) returns a field proxy instead of a scalar StrandsNode.
3180
+ this._schema = null;
2981
3181
  this.isStrandsNode = true;
2982
3182
 
2983
3183
  // Store original identifier for varying variables
@@ -3131,11 +3331,61 @@ class StrandsNode {
3131
3331
 
3132
3332
  return this;
3133
3333
  }
3334
+
3335
+ get(index) {
3336
+ // Validate baseType is 'storage'
3337
+ const nodeData = getNodeDataFromID(this.strandsContext.dag, this.id);
3338
+ if (nodeData.baseType !== 'storage') {
3339
+ throw new Error('get() can only be used on storage buffers');
3340
+ }
3341
+
3342
+ // For struct storage, return a proxy with per-field getters/setters
3343
+ if (this._schema) {
3344
+ return createStructArrayElementProxy(this.strandsContext, this, index, this._schema);
3345
+ }
3346
+
3347
+ // Create array access node: buffer.get(index) -> buffer[index]
3348
+ const { id, dimension } = arrayAccessNode(
3349
+ this.strandsContext,
3350
+ this,
3351
+ index);
3352
+ return createStrandsNode(id, dimension, this.strandsContext);
3353
+ }
3354
+
3355
+ set(index, value) {
3356
+ // Validate baseType is 'storage' and has _originalIdentifier
3357
+ const nodeData = getNodeDataFromID(this.strandsContext.dag, this.id);
3358
+ if (nodeData.baseType !== 'storage') {
3359
+ throw new Error('set() can only be used on storage buffers');
3360
+ }
3361
+ if (!this._originalIdentifier) {
3362
+ throw new Error('set() can only be used on storage buffers with an identifier');
3363
+ }
3364
+
3365
+ // If value is a plain object (struct literal), expand to per-field assignments
3366
+ // e.g. buf[idx] = { position: pos, velocity: vel }
3367
+ // becomes buf[idx].position = pos; buf[idx].velocity = vel;
3368
+ if (value !== null && typeof value === 'object' && !value.isStrandsNode && this._schema) {
3369
+ const proxy = createStructArrayElementProxy(this.strandsContext, this, index, this._schema);
3370
+ for (const [fieldName, fieldValue] of Object.entries(value)) {
3371
+ proxy[fieldName] = fieldValue;
3372
+ }
3373
+ return this;
3374
+ }
3375
+
3376
+ // Create array assignment node: buffer.set(index, value) -> buffer[index] = value
3377
+ // This creates an ASSIGNMENT node and records it in the CFG basic block
3378
+ // CFG preserves sequential order, preventing reordering of assignments
3379
+ arrayAssignmentNode(this.strandsContext, this, index, value);
3380
+
3381
+ // Return this for chaining
3382
+ return this;
3383
+ }
3134
3384
  }
3135
3385
  function createStrandsNode(id, dimension, strandsContext, onRebind) {
3136
3386
  return new Proxy(
3137
3387
  new StrandsNode(id, dimension, strandsContext),
3138
- swizzleTrap(id, dimension, strandsContext)
3388
+ swizzleTrap(id, dimension, strandsContext, onRebind)
3139
3389
  );
3140
3390
  }
3141
3391
 
@@ -3625,6 +3875,14 @@ function swizzleTrap(id, dimension, strandsContext, onRebind) {
3625
3875
  );
3626
3876
 
3627
3877
  target.id = newID;
3878
+
3879
+ // If we swizzle assign on a struct component i.e.
3880
+ // inputs.position.rg = [1, 2]
3881
+ // The onRebind callback will update the structs components so that it refers to the new values,
3882
+ // and make a new ID for the struct with these new values
3883
+ if (typeof onRebind === 'function') {
3884
+ onRebind(newID);
3885
+ }
3628
3886
  return true;
3629
3887
  }
3630
3888
  return Reflect.set(...arguments);
@@ -3633,6 +3891,186 @@ function swizzleTrap(id, dimension, strandsContext, onRebind) {
3633
3891
  return trap;
3634
3892
  }
3635
3893
 
3894
+ function arrayAccessNode(strandsContext, bufferNode, indexNode, accessMode) {
3895
+ const { dag, cfg } = strandsContext;
3896
+
3897
+ // Ensure index is a StrandsNode
3898
+ let index;
3899
+ if (indexNode instanceof StrandsNode) {
3900
+ index = indexNode;
3901
+ } else {
3902
+ const { id, dimension } = primitiveConstructorNode(
3903
+ strandsContext,
3904
+ { baseType: BaseType.INT, dimension: 1 },
3905
+ indexNode
3906
+ );
3907
+ index = createStrandsNode(id, dimension, strandsContext);
3908
+ }
3909
+
3910
+ // Array access returns a single float
3911
+ const nodeData = createNodeData({
3912
+ nodeType: NodeType.OPERATION,
3913
+ opCode: OpCode.Binary.ARRAY_ACCESS,
3914
+ dependsOn: [bufferNode.id, index.id],
3915
+ dimension: 1,
3916
+ baseType: BaseType.FLOAT});
3917
+
3918
+ const id = getOrCreateNode(dag, nodeData);
3919
+ recordInBasicBlock(cfg, cfg.currentBlock, id);
3920
+
3921
+ return { id, dimension: 1 };
3922
+ }
3923
+
3924
+ function createStructArrayElementProxy(strandsContext, bufferNode, indexNode, schema) {
3925
+ const { dag, cfg } = strandsContext;
3926
+
3927
+ // Ensure index is a StrandsNode
3928
+ let index;
3929
+ if (indexNode instanceof StrandsNode) {
3930
+ index = indexNode;
3931
+ } else {
3932
+ const { id, dimension } = primitiveConstructorNode(
3933
+ strandsContext,
3934
+ { baseType: BaseType.INT, dimension: 1 },
3935
+ indexNode
3936
+ );
3937
+ index = createStrandsNode(id, dimension, strandsContext);
3938
+ }
3939
+
3940
+ // Create a plain object with getters/setters for each struct field.
3941
+ // When read, a field creates an ARRAY_ACCESS IR node with the field name encoded
3942
+ // in the identifier slot. When written, an ASSIGNMENT IR node is recorded in the CFG.
3943
+ const proxy = {};
3944
+
3945
+ for (const field of schema.fields) {
3946
+ Object.defineProperty(proxy, field.name, {
3947
+ get() {
3948
+ // Encode field name in identifier so WGSL backend can emit buf[idx].field
3949
+ const nodeData = createNodeData({
3950
+ nodeType: NodeType.OPERATION,
3951
+ opCode: OpCode.Binary.ARRAY_ACCESS,
3952
+ dependsOn: [bufferNode.id, index.id],
3953
+ dimension: field.dim,
3954
+ baseType: BaseType.FLOAT,
3955
+ identifier: field.name,
3956
+ });
3957
+ const id = getOrCreateNode(dag, nodeData);
3958
+ recordInBasicBlock(cfg, cfg.currentBlock, id);
3959
+ // When a swizzle assignment fires (e.g. buf[i].vel.y *= -1), onRebind
3960
+ // receives the new vector ID and writes it back to the buffer field,
3961
+ // equivalent to buf[i].vel = newVec.
3962
+ const onRebind = (newFieldID) => {
3963
+ const accessData = createNodeData({
3964
+ nodeType: NodeType.OPERATION,
3965
+ opCode: OpCode.Binary.ARRAY_ACCESS,
3966
+ dependsOn: [bufferNode.id, index.id],
3967
+ dimension: field.dim,
3968
+ baseType: BaseType.FLOAT,
3969
+ identifier: field.name,
3970
+ });
3971
+ const accessID = getOrCreateNode(dag, accessData);
3972
+ const assignData = createNodeData({
3973
+ nodeType: NodeType.ASSIGNMENT,
3974
+ dependsOn: [accessID, newFieldID],
3975
+ phiBlocks: [],
3976
+ });
3977
+ const assignID = getOrCreateNode(dag, assignData);
3978
+ recordInBasicBlock(cfg, cfg.currentBlock, assignID);
3979
+ };
3980
+ return createStrandsNode(id, field.dim, strandsContext, onRebind);
3981
+ },
3982
+ set(val) {
3983
+ // Create access node as assignment target (field name in identifier)
3984
+ const accessData = createNodeData({
3985
+ nodeType: NodeType.OPERATION,
3986
+ opCode: OpCode.Binary.ARRAY_ACCESS,
3987
+ dependsOn: [bufferNode.id, index.id],
3988
+ dimension: field.dim,
3989
+ baseType: BaseType.FLOAT,
3990
+ identifier: field.name,
3991
+ });
3992
+ const accessID = getOrCreateNode(dag, accessData);
3993
+
3994
+ let valueID;
3995
+ if (val?.isStrandsNode) {
3996
+ valueID = val.id;
3997
+ } else {
3998
+ const { id } = primitiveConstructorNode(
3999
+ strandsContext,
4000
+ { baseType: BaseType.FLOAT, dimension: field.dim },
4001
+ val
4002
+ );
4003
+ valueID = id;
4004
+ }
4005
+
4006
+ const assignData = createNodeData({
4007
+ nodeType: NodeType.ASSIGNMENT,
4008
+ dependsOn: [accessID, valueID],
4009
+ phiBlocks: [],
4010
+ });
4011
+ const assignID = getOrCreateNode(dag, assignData);
4012
+ recordInBasicBlock(cfg, cfg.currentBlock, assignID);
4013
+ },
4014
+ configurable: true,
4015
+ });
4016
+ }
4017
+
4018
+ return proxy;
4019
+ }
4020
+
4021
+ function arrayAssignmentNode(strandsContext, bufferNode, indexNode, valueNode) {
4022
+ const { dag, cfg } = strandsContext;
4023
+
4024
+ // Ensure index is a StrandsNode
4025
+ let index;
4026
+ if (indexNode instanceof StrandsNode) {
4027
+ index = indexNode;
4028
+ } else {
4029
+ const { id, dimension } = primitiveConstructorNode(
4030
+ strandsContext,
4031
+ { baseType: BaseType.INT, dimension: 1 },
4032
+ indexNode
4033
+ );
4034
+ index = createStrandsNode(id, dimension, strandsContext);
4035
+ }
4036
+
4037
+ // Ensure value is a StrandsNode
4038
+ let value;
4039
+ if (valueNode instanceof StrandsNode) {
4040
+ value = valueNode;
4041
+ } else {
4042
+ const { id, dimension } = primitiveConstructorNode(
4043
+ strandsContext,
4044
+ { baseType: BaseType.FLOAT, dimension: 1 },
4045
+ valueNode
4046
+ );
4047
+ value = createStrandsNode(id, dimension, strandsContext);
4048
+ }
4049
+
4050
+ // Create array access node as the assignment target
4051
+ const arrayAccessData = createNodeData({
4052
+ nodeType: NodeType.OPERATION,
4053
+ opCode: OpCode.Binary.ARRAY_ACCESS,
4054
+ dependsOn: [bufferNode.id, index.id],
4055
+ dimension: 1,
4056
+ baseType: BaseType.FLOAT
4057
+ });
4058
+ const arrayAccessID = getOrCreateNode(dag, arrayAccessData);
4059
+
4060
+ // Create assignment node: buffer[index] = value
4061
+ const assignmentData = createNodeData({
4062
+ nodeType: NodeType.ASSIGNMENT,
4063
+ dependsOn: [arrayAccessID, value.id],
4064
+ phiBlocks: []
4065
+ });
4066
+ const assignmentID = getOrCreateNode(dag, assignmentData);
4067
+
4068
+ // CRITICAL: Record in CFG to preserve sequential ordering
4069
+ recordInBasicBlock(cfg, cfg.currentBlock, assignmentID);
4070
+
4071
+ return { id: assignmentID };
4072
+ }
4073
+
3636
4074
  function shouldCreateTemp(dag, nodeID) {
3637
4075
  const nodeType = dag.nodeTypes[nodeID];
3638
4076
  if (nodeType !== NodeType.OPERATION) return false;
@@ -3834,10 +4272,10 @@ const wgslBackend = {
3834
4272
  // Add texture and sampler bindings for sampler2D uniforms to both vertex and fragment declarations
3835
4273
  if (!strandsContext.renderer || !strandsContext.baseShader) return;
3836
4274
 
3837
- // Get the next available binding index from the renderer
3838
4275
  let bindingIndex = strandsContext.renderer.getNextBindingIndex({
3839
- vert: strandsContext.baseShader.vertSrc(),
3840
- frag: strandsContext.baseShader.fragSrc(),
4276
+ vert: strandsContext.baseShader._vertSrc,
4277
+ frag: strandsContext.baseShader._fragSrc,
4278
+ compute: strandsContext.baseShader._computeSrc,
3841
4279
  });
3842
4280
 
3843
4281
  for (const {name, typeInfo} of strandsContext.uniforms) {
@@ -3854,6 +4292,38 @@ const wgslBackend = {
3854
4292
  }
3855
4293
  }
3856
4294
  },
4295
+ addStorageBufferBindingsToDeclarations(strandsContext) {
4296
+ if (!strandsContext.renderer || !strandsContext.baseShader) return;
4297
+
4298
+ const isComputeShader = strandsContext.baseShader.shaderType === 'compute';
4299
+ let bindingIndex = strandsContext.renderer.getNextBindingIndex({
4300
+ vert: strandsContext.baseShader._vertSrc,
4301
+ frag: strandsContext.baseShader._fragSrc,
4302
+ compute: strandsContext.baseShader._computeSrc,
4303
+ });
4304
+
4305
+ for (const {name, typeInfo} of strandsContext.uniforms) {
4306
+ if (typeInfo.baseType === 'storage') {
4307
+ const accessMode = isComputeShader ? 'read_write' : 'read';
4308
+ let declaration;
4309
+ if (typeInfo.schema) {
4310
+ const structTypeName = `${name}Element`;
4311
+ declaration = `struct ${structTypeName} ${typeInfo.schema.structBody}\n@group(0) @binding(${bindingIndex}) var<storage, ${accessMode}> ${name}: array<${structTypeName}>;`;
4312
+ } else {
4313
+ declaration = `@group(0) @binding(${bindingIndex}) var<storage, ${accessMode}> ${name}: array<f32>;`;
4314
+ }
4315
+
4316
+ if (isComputeShader) {
4317
+ strandsContext.computeDeclarations.add(declaration);
4318
+ } else {
4319
+ strandsContext.vertexDeclarations.add(declaration);
4320
+ strandsContext.fragmentDeclarations.add(declaration);
4321
+ }
4322
+
4323
+ bindingIndex += 1;
4324
+ }
4325
+ }
4326
+ },
3857
4327
  getTypeName(baseType, dimension) {
3858
4328
  const primitiveTypeName = TypeNames[baseType + dimension];
3859
4329
  if (!primitiveTypeName) {
@@ -3861,6 +4331,19 @@ const wgslBackend = {
3861
4331
  }
3862
4332
  return primitiveTypeName;
3863
4333
  },
4334
+ getNoiseShaderSnippet() {
4335
+ return noiseWGSL;
4336
+ },
4337
+ getRandomFragmentShaderSnippet() {
4338
+ return randomWGSL;
4339
+ },
4340
+ getRandomVertexShaderSnippet() {
4341
+ return randomVertWGSL;
4342
+ },
4343
+ getRandomComputeShaderSnippet() {
4344
+ return randomComputeWGSL;
4345
+ },
4346
+
3864
4347
  generateHookUniformKey(name, typeInfo) {
3865
4348
  // For sampler2D types, we don't add them to the uniform struct,
3866
4349
  // but we still need them in the shader's hooks object so that
@@ -3868,6 +4351,11 @@ const wgslBackend = {
3868
4351
  if (typeInfo.baseType === 'sampler2D') {
3869
4352
  return `${name}: sampler2D`; // Signal that this should not be added to uniform struct
3870
4353
  }
4354
+ // For storage buffers, we don't add them to the uniform struct
4355
+ // Instead, they become separate storage buffer bindings
4356
+ if (typeInfo.baseType === 'storage') {
4357
+ return null; // Signal that this should not be added to uniform struct
4358
+ }
3871
4359
  return `${name}: ${this.getTypeName(typeInfo.baseType, typeInfo.dimension)}`;
3872
4360
  },
3873
4361
  generateVaryingVariable(varName, typeInfo) {
@@ -3894,9 +4382,13 @@ const wgslBackend = {
3894
4382
  // Generate just a semicolon (unless suppressed)
3895
4383
  generationContext.write(semicolon);
3896
4384
  } else if (node.statementType === StatementType.EARLY_RETURN) {
3897
- const exprNodeID = node.dependsOn[0];
3898
- const expr = this.generateExpression(generationContext, dag, exprNodeID);
3899
- generationContext.write(`return ${expr}${semicolon}`);
4385
+ if (node.dependsOn && node.dependsOn.length > 0) {
4386
+ const exprNodeID = node.dependsOn[0];
4387
+ const expr = this.generateExpression(generationContext, dag, exprNodeID);
4388
+ generationContext.write(`return ${expr}${semicolon}`);
4389
+ } else {
4390
+ generationContext.write(`return${semicolon}`);
4391
+ }
3900
4392
  }
3901
4393
  },
3902
4394
  generateAssignment(generationContext, dag, nodeID) {
@@ -3908,6 +4400,17 @@ const wgslBackend = {
3908
4400
  const targetNode = getNodeDataFromID(dag, targetNodeID);
3909
4401
  const semicolon = generationContext.suppressSemicolon ? '' : ';';
3910
4402
 
4403
+ // Check if target is an array access (storage buffer assignment)
4404
+ if (targetNode.opCode === OpCode.Binary.ARRAY_ACCESS) {
4405
+ const [bufferID, indexID] = targetNode.dependsOn;
4406
+ const bufferExpr = this.generateExpression(generationContext, dag, bufferID);
4407
+ const indexExpr = this.generateExpression(generationContext, dag, indexID);
4408
+ const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID);
4409
+ const fieldSuffix = targetNode.identifier ? `.${targetNode.identifier}` : '';
4410
+ generationContext.write(`${bufferExpr}[i32(${indexExpr})]${fieldSuffix} = ${sourceExpr}${semicolon}`);
4411
+ return;
4412
+ }
4413
+
3911
4414
  // Check if target is a swizzle assignment
3912
4415
  if (targetNode.opCode === OpCode.Unary.SWIZZLE) {
3913
4416
  const parentID = targetNode.dependsOn[0];
@@ -3965,6 +4468,10 @@ const wgslBackend = {
3965
4468
  return `var ${tmp}: ${typeName} = ${expr};`;
3966
4469
  },
3967
4470
  generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType) {
4471
+ if (!returnType) {
4472
+ generationContext.write('return;');
4473
+ return;
4474
+ }
3968
4475
  const dag = strandsContext.dag;
3969
4476
  const rootNode = getNodeDataFromID(dag, rootNodeID);
3970
4477
  if (isStructType(returnType)) {
@@ -4005,9 +4512,15 @@ const wgslBackend = {
4005
4512
  }
4006
4513
  }
4007
4514
 
4008
- // Check if this is a uniform variable (but not a texture)
4515
+ // Detect instanceID usage in fragment context and rewrite to varying name
4516
+ if (node.identifier === this.instanceIdReference() && generationContext.shaderContext === 'fragment') {
4517
+ generationContext.strandsContext._instanceIDUsedInFragment = true;
4518
+ return INSTANCE_ID_VARYING_NAME;
4519
+ }
4520
+
4521
+ // Check if this is a uniform variable (but not a texture or storage buffer)
4009
4522
  const uniform = generationContext.strandsContext?.uniforms?.find(uniform => uniform.name === node.identifier);
4010
- if (uniform && uniform.typeInfo.baseType !== 'sampler2D') {
4523
+ if (uniform && uniform.typeInfo.baseType !== 'sampler2D' && uniform.typeInfo.baseType !== 'storage') {
4011
4524
  return `hooks.${node.identifier}`;
4012
4525
  }
4013
4526
 
@@ -4026,6 +4539,13 @@ const wgslBackend = {
4026
4539
  const deps = node.dependsOn.map((dep) => this.generateExpression(generationContext, dag, dep));
4027
4540
  return `${T}(${deps.join(', ')})`;
4028
4541
  }
4542
+ if (node.opCode === OpCode.Nary.TERNARY) {
4543
+ const [condID, trueID, falseID] = node.dependsOn;
4544
+ const cond = this.generateExpression(generationContext, dag, condID);
4545
+ const trueExpr = this.generateExpression(generationContext, dag, trueID);
4546
+ const falseExpr = this.generateExpression(generationContext, dag, falseID);
4547
+ return `select(${falseExpr}, ${trueExpr}, ${cond})`;
4548
+ }
4029
4549
  if (node.opCode === OpCode.Nary.FUNCTION_CALL) {
4030
4550
  // Convert mod() function calls to % operator in WGSL
4031
4551
  if (node.identifier === 'mod' && node.dependsOn.length === 2) {
@@ -4047,6 +4567,18 @@ const wgslBackend = {
4047
4567
  }
4048
4568
 
4049
4569
  const functionArgs = node.dependsOn.map(arg =>this.generateExpression(generationContext, dag, arg));
4570
+
4571
+ if (node.identifier === 'random') {
4572
+ const ctx = generationContext.shaderContext;
4573
+ if (ctx === 'fragment') {
4574
+ functionArgs.push('_p5FragPos.xy');
4575
+ } else if (ctx === 'vertex') {
4576
+ functionArgs.push('f32(_p5VertexId)');
4577
+ } else if (ctx === 'compute') {
4578
+ functionArgs.push('_p5GlobalId');
4579
+ }
4580
+ }
4581
+
4050
4582
  return `${node.identifier}(${functionArgs.join(', ')})`;
4051
4583
  }
4052
4584
  if (node.opCode === OpCode.Binary.MEMBER_ACCESS) {
@@ -4060,6 +4592,13 @@ const wgslBackend = {
4060
4592
  const parentExpr = this.generateExpression(generationContext, dag, parentID);
4061
4593
  return `${parentExpr}.${node.swizzle}`;
4062
4594
  }
4595
+ if (node.opCode === OpCode.Binary.ARRAY_ACCESS) {
4596
+ const [bufferID, indexID] = node.dependsOn;
4597
+ const bufferExpr = this.generateExpression(generationContext, dag, bufferID);
4598
+ const indexExpr = this.generateExpression(generationContext, dag, indexID);
4599
+ const fieldSuffix = node.identifier ? `.${node.identifier}` : '';
4600
+ return `${bufferExpr}[i32(${indexExpr})]${fieldSuffix}`;
4601
+ }
4063
4602
  if (node.dependsOn.length === 2) {
4064
4603
  const [lID, rID] = node.dependsOn;
4065
4604
  const left = this.generateExpression(generationContext, dag, lID);
@@ -4133,12 +4672,25 @@ const wgslBackend = {
4133
4672
  const samplerVariable = variableNode(strandsContext, { baseType: BaseType.SAMPLER, dimension: 1 }, samplerIdentifier);
4134
4673
  const samplerNode = createStrandsNode(samplerVariable.id, samplerVariable.dimension, strandsContext);
4135
4674
 
4136
- // Create the augmented args: [texture, sampler, coords]
4137
- const augmentedArgs = [textureArg, samplerNode, coordsArg];
4138
-
4139
- const { id, dimension } = functionCallNode(strandsContext, 'textureSample', augmentedArgs, {
4675
+ // Create a LOD literal node (0.0) so we can use textureSampleLevel instead
4676
+ // of textureSample. textureSample doesn't let you use uniform values in control
4677
+ // flow, whereas textureSampleLevel does. While we don't have mipmaps, we don't
4678
+ // miss out.
4679
+ // TODO: if we *do* add mipmap support, update this logic -- we'd need to hoist
4680
+ // the texture lookup out of the control flow.
4681
+ const lodLiteral = scalarLiteralNode(
4682
+ strandsContext,
4683
+ { dimension: 1, baseType: BaseType.FLOAT },
4684
+ 0.0
4685
+ );
4686
+ const lodNode = createStrandsNode(lodLiteral.id, lodLiteral.dimension, strandsContext);
4687
+
4688
+ // Create the augmented args: [texture, sampler, coords, lod]
4689
+ const augmentedArgs = [textureArg, samplerNode, coordsArg, lodNode];
4690
+
4691
+ const { id, dimension } = functionCallNode(strandsContext, 'textureSampleLevel', augmentedArgs, {
4140
4692
  overloads: [{
4141
- params: [DataType.sampler2D, DataType.sampler, DataType.float2],
4693
+ params: [DataType.sampler2D, DataType.sampler, DataType.float2, DataType.float1],
4142
4694
  returnType: DataType.float4
4143
4695
  }]
4144
4696
  });
@@ -4148,114 +4700,11 @@ const wgslBackend = {
4148
4700
  instanceIdReference() {
4149
4701
  return 'instanceID';
4150
4702
  },
4151
- };
4152
-
4153
- // Based on https://github.com/stegu/webgl-noise/blob/22434e04d7753f7e949e8d724ab3da2864c17a0f/src/noise3D.glsl
4154
- // MIT licensed, adapted for p5.strands and converted to WGSL
4155
-
4156
- var noiseWGSL = `fn mod289Vec3(x: vec3<f32>) -> vec3<f32> {
4157
- return x - floor(x * (1.0 / 289.0)) * 289.0;
4158
- }
4159
-
4160
- fn mod289Vec4(x: vec4<f32>) -> vec4<f32> {
4161
- return x - floor(x * (1.0 / 289.0)) * 289.0;
4162
- }
4163
-
4164
- fn permute(x: vec4<f32>) -> vec4<f32> {
4165
- return mod289Vec4(((x*34.0)+10.0)*x);
4166
- }
4167
-
4168
- fn taylorInvSqrt(r: vec4<f32>) -> vec4<f32> {
4169
- return vec4<f32>(1.79284291400159) - vec4<f32>(0.85373472095314) * r;
4170
- }
4171
-
4172
- fn baseNoise(v: vec3<f32>) -> f32 {
4173
- let C = vec2<f32>(1.0/6.0, 1.0/3.0);
4174
- let D = vec4<f32>(0.0, 0.5, 1.0, 2.0);
4175
-
4176
- // First corner
4177
- var i = floor(v + dot(v, C.yyy));
4178
- let x0 = v - i + dot(i, C.xxx);
4179
-
4180
- // Other corners
4181
- let g = step(x0.yzx, x0.xyz);
4182
- let l = vec3<f32>(1.0) - g;
4183
- let i1 = min(g.xyz, l.zxy);
4184
- let i2 = max(g.xyz, l.zxy);
4185
-
4186
- // x0 = x0 - 0.0 + 0.0 * C.xxx;
4187
- // x1 = x0 - i1 + 1.0 * C.xxx;
4188
- // x2 = x0 - i2 + 2.0 * C.xxx;
4189
- // x3 = x0 - 1.0 + 3.0 * C.xxx;
4190
- let x1 = x0 - i1 + C.xxx;
4191
- let x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
4192
- let x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y
4193
-
4194
- // Permutations
4195
- i = mod289Vec3(i);
4196
- let p = permute( permute( permute(
4197
- i.z + vec4<f32>(0.0, i1.z, i2.z, 1.0 ))
4198
- + i.y + vec4<f32>(0.0, i1.y, i2.y, 1.0 ))
4199
- + i.x + vec4<f32>(0.0, i1.x, i2.x, 1.0 ));
4200
-
4201
- // Gradients: 7x7 points over a square, mapped onto an octahedron.
4202
- // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
4203
- let n_ = 0.142857142857; // 1.0/7.0
4204
- let ns = n_ * D.wyz - D.xzx;
4205
-
4206
- let j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7)
4207
-
4208
- let x_ = floor(j * ns.z);
4209
- let y_ = floor(j - 7.0 * x_ ); // mod(j,N)
4210
-
4211
- let x = x_ *ns.x + ns.yyyy;
4212
- let y = y_ *ns.x + ns.yyyy;
4213
- let h = vec4<f32>(1.0) - abs(x) - abs(y);
4214
-
4215
- let b0 = vec4<f32>( x.xy, y.xy );
4216
- let b1 = vec4<f32>( x.zw, y.zw );
4217
-
4218
- //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
4219
- //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
4220
- let s0 = floor(b0)*2.0 + vec4<f32>(1.0);
4221
- let s1 = floor(b1)*2.0 + vec4<f32>(1.0);
4222
- let sh = -step(h, vec4<f32>(0.0));
4223
-
4224
- let a0 = b0.xzyw + s0.xzyw*sh.xxyy;
4225
- let a1 = b1.xzyw + s1.xzyw*sh.zzww;
4226
-
4227
- let p0 = vec3<f32>(a0.xy, h.x);
4228
- let p1 = vec3<f32>(a0.zw, h.y);
4229
- let p2 = vec3<f32>(a1.xy, h.z);
4230
- let p3 = vec3<f32>(a1.zw, h.w);
4231
-
4232
- //Normalise gradients
4233
- let norm = taylorInvSqrt(vec4<f32>(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
4234
- let p0_norm = p0 * norm.x;
4235
- let p1_norm = p1 * norm.y;
4236
- let p2_norm = p2 * norm.z;
4237
- let p3_norm = p3 * norm.w;
4238
-
4239
- // Mix final noise value
4240
- var m = max(vec4<f32>(0.5) - vec4<f32>(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), vec4<f32>(0.0));
4241
- m = m * m;
4242
- return 105.0 * dot( m*m, vec4<f32>( dot(p0_norm,x0), dot(p1_norm,x1),
4243
- dot(p2_norm,x2), dot(p3_norm,x3) ) );
4244
- }
4245
-
4246
- fn noise(st: vec3<f32>, octaves: i32, ampFalloff: f32) -> f32 {
4247
- var result = 0.0;
4248
- var amplitude = 1.0;
4249
- var frequency = 1.0;
4250
4703
 
4251
- for (var i = 0; i < 8; i++) {
4252
- if (i >= octaves) { break; }
4253
- result += amplitude * baseNoise(st * frequency);
4254
- frequency *= 2.0;
4255
- amplitude *= ampFalloff;
4256
- }
4257
- return (result + 1.0) * 0.5;
4258
- }`;
4704
+ generateInstanceIDVarying() {
4705
+ return { name: INSTANCE_ID_VARYING_NAME, declaration: `${INSTANCE_ID_VARYING_NAME}: i32`, source: 'i32(instanceID)', interpolation: 'flat' };
4706
+ },
4707
+ };
4259
4708
 
4260
4709
  const filterUniforms = `
4261
4710
  // Group 0: Filter Properties
@@ -4567,6 +5016,44 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
4567
5016
  }
4568
5017
  `;
4569
5018
 
5019
+ const baseComputeShader = `
5020
+ struct ComputeUniforms {
5021
+ uTotalCount: vec3<i32>,
5022
+ uPhysicalCount: vec3<i32>,
5023
+ }
5024
+ @group(0) @binding(0) var<uniform> uniforms: ComputeUniforms;
5025
+
5026
+ @compute @workgroup_size(8, 8, 1)
5027
+ fn main(
5028
+ @builtin(global_invocation_id) globalId: vec3<u32>,
5029
+ @builtin(local_invocation_id) localId: vec3<u32>,
5030
+ @builtin(workgroup_id) workgroupId: vec3<u32>,
5031
+ @builtin(local_invocation_index) localIndex: u32
5032
+ ) {
5033
+ let totalIterations = u32(uniforms.uTotalCount.x) * u32(uniforms.uTotalCount.y) * u32(uniforms.uTotalCount.z);
5034
+ let physicalId = globalId.x + globalId.y * (u32(uniforms.uPhysicalCount.x)) + globalId.z * (u32(uniforms.uPhysicalCount.x) * u32(uniforms.uPhysicalCount.y));
5035
+
5036
+ if (physicalId >= totalIterations) {
5037
+ return;
5038
+ }
5039
+
5040
+ var index = vec3<i32>(0);
5041
+ index.x = i32(physicalId % u32(uniforms.uTotalCount.x));
5042
+ let remainingY = physicalId / u32(uniforms.uTotalCount.x);
5043
+ index.y = i32(remainingY % u32(uniforms.uTotalCount.y));
5044
+ index.z = i32(remainingY / u32(uniforms.uTotalCount.y));
5045
+
5046
+ HOOK_iteration(index);
5047
+ }
5048
+ `;
5049
+
5050
+ /**
5051
+ * @module 3D
5052
+ * @submodule p5.strands
5053
+ * @for p5
5054
+ */
5055
+
5056
+
4570
5057
  const FRAME_STATE = {
4571
5058
  PENDING: 0,
4572
5059
  UNPROMOTED: 1,
@@ -4588,6 +5075,228 @@ function rendererWebGPU(p5, fn) {
4588
5075
  RGBA,
4589
5076
  } = p5;
4590
5077
 
5078
+ class StorageBuffer {
5079
+ constructor(buffer, size, renderer, schema = null) {
5080
+ this._isStorageBuffer = true;
5081
+ this.buffer = buffer;
5082
+ this.size = size;
5083
+ this._renderer = renderer;
5084
+ this._schema = schema;
5085
+ }
5086
+
5087
+ /**
5088
+ * Updates the data in the buffer with new values. The new data must be in
5089
+ * the same format as the data originally passed to
5090
+ * <a href="#/p5/createStorage">`createStorage()`</a>.
5091
+ *
5092
+ * ```js example
5093
+ * let particles;
5094
+ * let computeShader;
5095
+ * let displayShader;
5096
+ * let instance;
5097
+ * const numParticles = 100;
5098
+ *
5099
+ * async function setup() {
5100
+ * await createCanvas(100, 100, WEBGPU);
5101
+ * particles = createStorage(makeParticles(width / 2, height / 2));
5102
+ * computeShader = buildComputeShader(simulate);
5103
+ * displayShader = buildMaterialShader(display);
5104
+ * instance = buildGeometry(drawParticle);
5105
+ * describe('100 orange particles shooting outward.');
5106
+ * }
5107
+ *
5108
+ * function makeParticles(x, y) {
5109
+ * let data = [];
5110
+ * for (let i = 0; i < numParticles; i++) {
5111
+ * let angle = (i / numParticles) * TWO_PI;
5112
+ * let speed = random(0.5, 2);
5113
+ * data.push({
5114
+ * position: createVector(x, y),
5115
+ * velocity: createVector(cos(angle) * speed, sin(angle) * speed),
5116
+ * });
5117
+ * }
5118
+ * return data;
5119
+ * }
5120
+ *
5121
+ * function drawParticle() {
5122
+ * sphere(2);
5123
+ * }
5124
+ *
5125
+ * function simulate() {
5126
+ * let data = uniformStorage(particles);
5127
+ * let idx = index.x;
5128
+ * data[idx].position = data[idx].position + data[idx].velocity;
5129
+ * }
5130
+ *
5131
+ * function display() {
5132
+ * let data = uniformStorage(particles);
5133
+ * worldInputs.begin();
5134
+ * let pos = data[instanceID()].position;
5135
+ * worldInputs.position.xy += pos - [width / 2, height / 2];
5136
+ * worldInputs.end();
5137
+ * }
5138
+ *
5139
+ * function draw() {
5140
+ * background(30);
5141
+ * if (frameCount % 60 === 0) {
5142
+ * particles.update(makeParticles(random(width), random(height)));
5143
+ * }
5144
+ * compute(computeShader, numParticles);
5145
+ * noStroke();
5146
+ * fill(255, 200, 50);
5147
+ * shader(displayShader);
5148
+ * model(instance, numParticles);
5149
+ * }
5150
+ * ```
5151
+ *
5152
+ * @method update
5153
+ * @for p5.StorageBuffer
5154
+ * @beta
5155
+ * @webgpu
5156
+ * @webgpuOnly
5157
+ * @param {Number[]|Float32Array|Object[]} data The new data to write into the buffer.
5158
+ */
5159
+ update(data) {
5160
+ const device = this._renderer.device;
5161
+
5162
+ if (this._schema !== null) {
5163
+ // Buffer was created with a struct array
5164
+ if (
5165
+ !Array.isArray(data) ||
5166
+ data.length === 0 ||
5167
+ typeof data[0] !== 'object' ||
5168
+ Array.isArray(data[0])
5169
+ ) {
5170
+ throw new Error(
5171
+ 'update() expects an array of objects matching the original struct format'
5172
+ );
5173
+ }
5174
+
5175
+ const newSchema = this._renderer._inferStructSchema(data[0]);
5176
+ if (newSchema.structBody !== this._schema.structBody) {
5177
+ throw new Error(
5178
+ `update() data structure doesn't match the original.\n` +
5179
+ ` Expected: ${this._schema.structBody}\n` +
5180
+ ` Got: ${newSchema.structBody}`
5181
+ );
5182
+ }
5183
+
5184
+ const packed = this._renderer._packStructArray(data, this._schema);
5185
+ if (packed.byteLength > this.size) {
5186
+ throw new Error(
5187
+ `update() data (${packed.byteLength} bytes) exceeds buffer size (${this.size} bytes)`
5188
+ );
5189
+ }
5190
+ device.queue.writeBuffer(this.buffer, 0, packed);
5191
+ } else {
5192
+ // Buffer was created with a float array
5193
+ let floatData;
5194
+ if (data instanceof Float32Array) {
5195
+ floatData = data;
5196
+ } else if (Array.isArray(data)) {
5197
+ floatData = new Float32Array(data);
5198
+ } else {
5199
+ throw new Error(
5200
+ 'update() expects a Float32Array or array of numbers for this buffer'
5201
+ );
5202
+ }
5203
+
5204
+ if (floatData.byteLength > this.size) {
5205
+ throw new Error(
5206
+ `update() data (${floatData.byteLength} bytes) exceeds buffer size (${this.size} bytes)`
5207
+ );
5208
+ }
5209
+ device.queue.writeBuffer(this.buffer, 0, floatData);
5210
+ }
5211
+ }
5212
+
5213
+ /**
5214
+ * Reads data from a storage buffer back into JavaScript.
5215
+ *
5216
+ * Copies data from the GPU to the CPU using a temporary buffer,
5217
+ * so it must be awaited. Returns a `Float32Array` for number
5218
+ * buffers, or an array of plain objects for struct buffers.
5219
+ *
5220
+ * Note: This is a GPU -> CPU read, so calling it often (like every frame)
5221
+ * can be slow.
5222
+ *
5223
+ * ```js example
5224
+ * let data;
5225
+ * let computeShader;
5226
+ *
5227
+ * async function setup() {
5228
+ * await createCanvas(100, 100, WEBGPU);
5229
+ *
5230
+ * data = createStorage(new Float32Array([1, 2, 3, 4]));
5231
+ * computeShader = buildComputeShader(doubleValues);
5232
+ * compute(computeShader, 4);
5233
+ *
5234
+ * let result = await data.read();
5235
+ * // result is Float32Array [2, 4, 6, 8]
5236
+ * for (let i = 0; i < result.length; i++) {
5237
+ * print(result[i]);
5238
+ * }
5239
+ * describe('Prints the values 2, 4, 6, 8 to the console.');
5240
+ * }
5241
+ *
5242
+ * function doubleValues() {
5243
+ * let d = uniformStorage(data);
5244
+ * let idx = index.x;
5245
+ * d[idx] = d[idx] * 2;
5246
+ * }
5247
+ * ```
5248
+ *
5249
+ * @method read
5250
+ * @for p5.StorageBuffer
5251
+ * @beta
5252
+ * @webgpu
5253
+ * @webgpuOnly
5254
+ * @returns {Promise<Float32Array|Object[]>}
5255
+ */
5256
+ async read() {
5257
+ const device = this._renderer.device;
5258
+ this._renderer.flushDraw();
5259
+
5260
+ const stagingBuffer = device.createBuffer({
5261
+ size: this.size,
5262
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
5263
+ });
5264
+
5265
+ const commandEncoder = device.createCommandEncoder();
5266
+ commandEncoder.copyBufferToBuffer(this.buffer, 0, stagingBuffer, 0, this.size);
5267
+ device.queue.submit([commandEncoder.finish()]);
5268
+
5269
+ await stagingBuffer.mapAsync(GPUMapMode.READ, 0, this.size);
5270
+ const mappedRange = stagingBuffer.getMappedRange(0, this.size);
5271
+
5272
+ // Copy before unmapping because mapped memory becomes invalid after unmap
5273
+ const rawCopy = new Float32Array(mappedRange.byteLength / 4);
5274
+ rawCopy.set(new Float32Array(mappedRange));
5275
+
5276
+ stagingBuffer.unmap();
5277
+ stagingBuffer.destroy();
5278
+
5279
+ if (this._schema !== null) {
5280
+ return this._renderer._unpackStructArray(rawCopy, this._schema);
5281
+ }
5282
+ return rawCopy;
5283
+ }
5284
+ }
5285
+
5286
+ /**
5287
+ * A block of data that shaders can read from, and compute shaders can also
5288
+ * write to. This is only available in WebGPU mode.
5289
+ *
5290
+ * Note: <a href="#/p5/createStorage">`createStorage()`</a> is the recommended
5291
+ * way to create an instance of this class.
5292
+ *
5293
+ * @class p5.StorageBuffer
5294
+ * @beta
5295
+ * @webgpu
5296
+ * @webgpuOnly
5297
+ */
5298
+ p5.StorageBuffer = StorageBuffer;
5299
+
4591
5300
  class RendererWebGPU extends Renderer3D {
4592
5301
  constructor(pInst, w, h, isMainCanvas, elt) {
4593
5302
  super(pInst, w, h, isMainCanvas, elt);
@@ -4639,6 +5348,9 @@ function rendererWebGPU(p5, fn) {
4639
5348
  // Retired buffers to destroy at end of frame
4640
5349
  this._retiredBuffers = [];
4641
5350
 
5351
+ // Storage buffers for compute shaders
5352
+ this._storageBuffers = new Set();
5353
+
4642
5354
  // 2D canvas for pixel reading fallback
4643
5355
  this._pixelReadCanvas = null;
4644
5356
  this._pixelReadCtx = null;
@@ -4715,7 +5427,7 @@ function rendererWebGPU(p5, fn) {
4715
5427
  }
4716
5428
  if (this._pInst._webgpuAttributes[key] !== value) {
4717
5429
  //changing value of previously altered attribute
4718
- this._webgpuAttributes[key] = value;
5430
+ this._pInst._webgpuAttributes[key] = value;
4719
5431
  unchanged = false;
4720
5432
  }
4721
5433
  //setting all attributes with some change
@@ -4849,9 +5561,21 @@ function rendererWebGPU(p5, fn) {
4849
5561
  const _b = args[2] || 0;
4850
5562
  const _a = args[3] || 0;
4851
5563
 
4852
- // If PENDING and no custom framebuffer, clear means stay UNPROMOTED
4853
- if (this._frameState === FRAME_STATE.PENDING && !this.activeFramebuffer()) {
4854
- this._frameState = FRAME_STATE.UNPROMOTED;
5564
+ // If PENDING and no custom framebuffer, clear means stay UNPROMOTED.
5565
+ // However, if we are still in setup (frameCount == 0), we must promote
5566
+ // so that mainFramebuffer gets the cleared content. This ensures that if
5567
+ // draw() later promotes without a copy, it starts from the correct state
5568
+ // rather than a stale mainFramebuffer.
5569
+ // Note: a mid-draw-loop transition from UNPROMOTED back to PROMOTED
5570
+ // (i.e. calling background() some frames but not others) will still
5571
+ // lose intermediate UNPROMOTED frame content.
5572
+ if (this._frameState !== FRAME_STATE.PROMOTED && !this.activeFramebuffer()) {
5573
+ if (this._pInst.frameCount > 0) {
5574
+ this._frameState = FRAME_STATE.UNPROMOTED;
5575
+ } else {
5576
+ this._promoteToFramebufferWithoutCopy();
5577
+ // clear() then targets mainFramebuffer via activeFramebuffer()
5578
+ }
4855
5579
  }
4856
5580
 
4857
5581
  this._finishActiveRenderPass();
@@ -5054,7 +5778,8 @@ function rendererWebGPU(p5, fn) {
5054
5778
  return 4; // Cap at 4 for broader compatibility
5055
5779
  }
5056
5780
 
5057
- _shaderOptions({ mode }) {
5781
+ _shaderOptions({ mode, compute, workgroupSize }) {
5782
+ if (compute) return { compute: true, workgroupSize };
5058
5783
  const activeFramebuffer = this.activeFramebuffer();
5059
5784
  const format = activeFramebuffer ?
5060
5785
  this._getWebGPUColorFormat(activeFramebuffer) :
@@ -5065,9 +5790,9 @@ function rendererWebGPU(p5, fn) {
5065
5790
  1; // No MSAA needed when blitting already-antialiased textures to canvas
5066
5791
  const sampleCount = this._getValidSampleCount(requestedSampleCount);
5067
5792
 
5068
- const depthFormat = activeFramebuffer && activeFramebuffer.useDepth ?
5069
- this._getWebGPUDepthFormat(activeFramebuffer) :
5070
- this.depthFormat;
5793
+ const depthFormat = activeFramebuffer
5794
+ ? (activeFramebuffer.useDepth ? this._getWebGPUDepthFormat(activeFramebuffer) : undefined)
5795
+ : this.depthFormat;
5071
5796
 
5072
5797
  const drawTarget = this.drawTarget();
5073
5798
  const clipping = this._clipping;
@@ -5095,6 +5820,31 @@ function rendererWebGPU(p5, fn) {
5095
5820
  _initShader(shader) {
5096
5821
  const device = this.device;
5097
5822
 
5823
+ if (shader.shaderType === 'compute') {
5824
+ // Compute shader initialization
5825
+ shader.computeModule = device.createShaderModule({ code: shader.computeSrc() });
5826
+ shader._computePipelineCache = null;
5827
+ shader._workgroupSize = null;
5828
+
5829
+ // Create compute pipeline (deferred until first compute() call)
5830
+ shader.getPipeline = ({ workgroupSize }) => {
5831
+ if (!shader._computePipelineCache) {
5832
+ shader._computePipelineCache = device.createComputePipeline({
5833
+ layout: shader._pipelineLayout,
5834
+ compute: {
5835
+ module: shader.computeModule,
5836
+ entryPoint: 'main'
5837
+ }
5838
+ });
5839
+ shader._workgroupSize = workgroupSize;
5840
+ }
5841
+ return shader._computePipelineCache;
5842
+ };
5843
+
5844
+ return;
5845
+ }
5846
+
5847
+ // Render shader initialization
5098
5848
  shader.vertModule = device.createShaderModule({ code: shader.vertSrc() });
5099
5849
  shader.fragModule = device.createShaderModule({ code: shader.fragSrc() });
5100
5850
 
@@ -5119,25 +5869,27 @@ function rendererWebGPU(p5, fn) {
5119
5869
  },
5120
5870
  primitive: { topology },
5121
5871
  multisample: { count: sampleCount },
5122
- depthStencil: {
5123
- format: depthFormat,
5124
- depthWriteEnabled: !clipping,
5125
- depthCompare: 'less-equal',
5126
- stencilFront: {
5127
- compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
5128
- failOp: 'keep',
5129
- depthFailOp: 'keep',
5130
- passOp: clipping ? 'replace' : 'keep',
5131
- },
5132
- stencilBack: {
5133
- compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
5134
- failOp: 'keep',
5135
- depthFailOp: 'keep',
5136
- passOp: clipping ? 'replace' : 'keep',
5872
+ ...(depthFormat ? {
5873
+ depthStencil: {
5874
+ format: depthFormat,
5875
+ depthWriteEnabled: !clipping,
5876
+ depthCompare: 'less-equal',
5877
+ stencilFront: {
5878
+ compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
5879
+ failOp: 'keep',
5880
+ depthFailOp: 'keep',
5881
+ passOp: clipping ? 'replace' : 'keep',
5882
+ },
5883
+ stencilBack: {
5884
+ compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
5885
+ failOp: 'keep',
5886
+ depthFailOp: 'keep',
5887
+ passOp: clipping ? 'replace' : 'keep',
5888
+ },
5889
+ stencilReadMask: 0xFF,
5890
+ stencilWriteMask: clipping ? 0xFF : 0x00,
5137
5891
  },
5138
- stencilReadMask: 0xFF,
5139
- stencilWriteMask: clipping ? 0xFF : 0x00,
5140
- },
5892
+ } : {}),
5141
5893
  });
5142
5894
  shader._pipelineCache.set(key, pipeline);
5143
5895
  }
@@ -5193,7 +5945,9 @@ function rendererWebGPU(p5, fn) {
5193
5945
  entries.push({
5194
5946
  bufferGroup,
5195
5947
  binding: bufferGroup.binding,
5196
- visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
5948
+ visibility: shader.shaderType === 'compute'
5949
+ ? GPUShaderStage.COMPUTE
5950
+ : GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
5197
5951
  buffer: { type: 'uniform', hasDynamicOffset: bufferGroup.dynamic },
5198
5952
  });
5199
5953
  structEntries.set(bufferGroup.group, entries);
@@ -5227,6 +5981,24 @@ function rendererWebGPU(p5, fn) {
5227
5981
  groupEntries.set(group, entries);
5228
5982
  }
5229
5983
 
5984
+ // Add storage buffer bindings
5985
+ for (const storage of shader._storageBuffers || []) {
5986
+ const group = storage.group;
5987
+ const entries = groupEntries.get(group) || [];
5988
+
5989
+ entries.push({
5990
+ binding: storage.binding,
5991
+ visibility: storage.visibility,
5992
+ buffer: {
5993
+ type: storage.accessMode === 'read' ? 'read-only-storage' : 'storage'
5994
+ },
5995
+ storage: storage,
5996
+ });
5997
+
5998
+ entries.sort((a, b) => a.binding - b.binding);
5999
+ groupEntries.set(group, entries);
6000
+ }
6001
+
5230
6002
  // Create layouts and bind groups
5231
6003
  const groupEntriesArr = [];
5232
6004
  for (const [group, entries] of groupEntries) {
@@ -5245,6 +6017,7 @@ function rendererWebGPU(p5, fn) {
5245
6017
  shader._pipelineLayout = this.device.createPipelineLayout({
5246
6018
  bindGroupLayouts: shader._bindGroupLayouts,
5247
6019
  });
6020
+ shader._compiled = true;
5248
6021
  }
5249
6022
 
5250
6023
  _getBlendState(mode) {
@@ -5491,8 +6264,11 @@ function rendererWebGPU(p5, fn) {
5491
6264
 
5492
6265
  _resetBuffersBeforeDraw() {
5493
6266
  this._finishActiveRenderPass();
6267
+
5494
6268
  // Set state to PENDING - we'll decide on first draw
5495
- this._frameState = FRAME_STATE.PENDING;
6269
+ if (this._pInst.frameCount > 0) {
6270
+ this._frameState = FRAME_STATE.PENDING;
6271
+ }
5496
6272
 
5497
6273
  // Clear depth buffer but DON'T start any render pass yet
5498
6274
  const activeFramebuffer = this.activeFramebuffer();
@@ -5603,6 +6379,8 @@ function rendererWebGPU(p5, fn) {
5603
6379
  // once we're drawing to the framebuffer, because normally
5604
6380
  // those are reset.
5605
6381
  const savedModelMatrix = this.states.uModelMatrix.copy();
6382
+ this.states.uModelMatrix.set(this.states.uModelMatrix.copy());
6383
+ this.states.uModelMatrix.reset();
5606
6384
  this.mainFramebuffer.defaultCamera.set(this.states.curCamera);
5607
6385
 
5608
6386
  this.mainFramebuffer.begin();
@@ -5610,7 +6388,12 @@ function rendererWebGPU(p5, fn) {
5610
6388
  this.states.uModelMatrix.set(savedModelMatrix);
5611
6389
  }
5612
6390
 
5613
- _promoteToFramebufferWithoutCopy() {
6391
+ _promoteToFramebufferWithoutCopy() {
6392
+ // Already promoted this frame
6393
+ if (this._frameState === FRAME_STATE.PROMOTED) {
6394
+ return;
6395
+ }
6396
+
5614
6397
  // Ensure mainFramebuffer matches canvas size
5615
6398
  if (this.mainFramebuffer.width !== this.width ||
5616
6399
  this.mainFramebuffer.height !== this.height) {
@@ -5625,6 +6408,8 @@ function rendererWebGPU(p5, fn) {
5625
6408
 
5626
6409
  // Preserve transformation state
5627
6410
  const savedModelMatrix = this.states.uModelMatrix.copy();
6411
+ this.states.uModelMatrix.set(this.states.uModelMatrix.copy());
6412
+ this.states.uModelMatrix.reset();
5628
6413
  this.mainFramebuffer.defaultCamera.set(this.states.curCamera);
5629
6414
 
5630
6415
  // Begin rendering to mainFramebuffer
@@ -5938,7 +6723,6 @@ function rendererWebGPU(p5, fn) {
5938
6723
  }
5939
6724
  this.flushDraw();
5940
6725
 
5941
- // this._pInst.background('red');
5942
6726
  this._pInst.push();
5943
6727
  this.states.setValue('enableLighting', false);
5944
6728
  this.states.setValue('activeImageLight', null);
@@ -6003,25 +6787,9 @@ function rendererWebGPU(p5, fn) {
6003
6787
 
6004
6788
  this._beginActiveRenderPass();
6005
6789
  const passEncoder = this.activeRenderPass;
6006
- const currentShader = this._curShader;
6007
- const shaderOptions = this._shaderOptions({ mode });
6008
- if (this.activeShader !== currentShader || this._shaderOptionsDifferent(shaderOptions)) {
6009
- passEncoder.setPipeline(currentShader.getPipeline(shaderOptions));
6010
- }
6011
- this.activeShader = currentShader;
6012
- this.activeShaderOptions = shaderOptions;
6013
6790
 
6014
- // Set stencil reference value for clipping
6015
- const drawTarget = this.drawTarget();
6016
- if (drawTarget._isClipApplied && !this._clipping) {
6017
- // When using the clip mask, test against reference value 0 (background)
6018
- // WebGL uses NOTEQUAL with ref 0, so fragments pass where stencil != 0
6019
- // In WebGPU with 'not-equal', we need ref 0 to pass where stencil != 0
6020
- passEncoder.setStencilReference(0);
6021
- } else if (this._clipping) {
6022
- // When writing to the clip mask, write reference value 1
6023
- passEncoder.setStencilReference(1);
6024
- }
6791
+ const currentShader = this._curShader;
6792
+ this.setupShaderBindGroups(currentShader, passEncoder, { mode, buffers });
6025
6793
  // Bind vertex buffers
6026
6794
  for (const buffer of currentShader._vertexBuffers || this._getVertexBuffers(currentShader)) {
6027
6795
  const location = currentShader.attributes[buffer.attr].location;
@@ -6029,6 +6797,58 @@ function rendererWebGPU(p5, fn) {
6029
6797
  passEncoder.setVertexBuffer(location, gpuBuffer, 0);
6030
6798
  }
6031
6799
 
6800
+ if (currentShader.shaderType === "fill") {
6801
+ // Bind index buffer and issue draw
6802
+ if (buffers.indexBuffer) {
6803
+ const indexFormat = buffers.indexFormat || "uint16";
6804
+ passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
6805
+ passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
6806
+ } else {
6807
+ passEncoder.draw(geometry.vertices.length, count, 0, 0);
6808
+ }
6809
+ } else if (currentShader.shaderType === "text") {
6810
+ if (!buffers.indexBuffer) {
6811
+ throw new Error("Text geometry must have an index buffer");
6812
+ }
6813
+ const indexFormat = buffers.indexFormat || "uint16";
6814
+ passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
6815
+ passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
6816
+ }
6817
+
6818
+ if (buffers.lineVerticesBuffer && currentShader.shaderType === "stroke") {
6819
+ passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0);
6820
+ }
6821
+
6822
+ // Mark that we have pending draws that need submission
6823
+ this._hasPendingDraws = true;
6824
+ }
6825
+
6826
+ setupShaderBindGroups(currentShader, passEncoder, shaderOptionsParams) {
6827
+ const shaderOptions = this._shaderOptions(shaderOptionsParams);
6828
+ if (
6829
+ shaderOptions.compute ||
6830
+ this.activeShader !== currentShader ||
6831
+ this._shaderOptionsDifferent(shaderOptions)
6832
+ ) {
6833
+ passEncoder.setPipeline(currentShader.getPipeline(shaderOptions));
6834
+ }
6835
+ if (!shaderOptions.compute) {
6836
+ this.activeShader = currentShader;
6837
+ this.activeShaderOptions = shaderOptions;
6838
+
6839
+ // Set stencil reference value for clipping
6840
+ const drawTarget = this.drawTarget();
6841
+ if (drawTarget._isClipApplied && !this._clipping) {
6842
+ // When using the clip mask, test against reference value 0 (background)
6843
+ // WebGL uses NOTEQUAL with ref 0, so fragments pass where stencil != 0
6844
+ // In WebGPU with 'not-equal', we need ref 0 to pass where stencil != 0
6845
+ passEncoder.setStencilReference(0);
6846
+ } else if (this._clipping) {
6847
+ // When writing to the clip mask, write reference value 1
6848
+ passEncoder.setStencilReference(1);
6849
+ }
6850
+ }
6851
+
6032
6852
  for (const bufferGroup of currentShader._uniformBufferGroups) {
6033
6853
  if (bufferGroup.dynamic) {
6034
6854
  // Bind uniforms into a part of a big dynamic memory block because
@@ -6081,6 +6901,13 @@ function rendererWebGPU(p5, fn) {
6081
6901
  currentShader.buffersDirty.delete(key);
6082
6902
  }
6083
6903
  }
6904
+ for (const storage of currentShader._storageBuffers || []) {
6905
+ const key = storage.group * 1000 + storage.binding;
6906
+ if (currentShader.buffersDirty.has(key)) {
6907
+ currentShader._cachedBindGroup[storage.group] = undefined;
6908
+ currentShader.buffersDirty.delete(key);
6909
+ }
6910
+ }
6084
6911
 
6085
6912
  // Bind sampler/texture uniforms and uniform buffers
6086
6913
  for (const iter of currentShader._groupEntries) {
@@ -6110,6 +6937,19 @@ function rendererWebGPU(p5, fn) {
6110
6937
  : { buffer: uniformBufferInfo.buffer },
6111
6938
  });
6112
6939
  }
6940
+ } else if (entry.storage && !bindGroup) {
6941
+ // Storage buffer binding
6942
+ const uniform = currentShader.uniforms[entry.storage.name];
6943
+ if (!uniform || !uniform._cachedData || !uniform._cachedData._isStorageBuffer) {
6944
+ throw new Error(
6945
+ `Storage buffer "${entry.storage.name}" not set. ` +
6946
+ `Use shader.setUniform("${entry.storage.name}", storageBuffer)`
6947
+ );
6948
+ }
6949
+ bgEntries.push({
6950
+ binding: entry.binding,
6951
+ resource: { buffer: uniform._cachedData.buffer },
6952
+ });
6113
6953
  } else if (!bindGroup) {
6114
6954
  bgEntries.push({
6115
6955
  binding: entry.binding,
@@ -6143,84 +6983,71 @@ function rendererWebGPU(p5, fn) {
6143
6983
  );
6144
6984
  }
6145
6985
  }
6986
+ return passEncoder;
6987
+ }
6146
6988
 
6147
- if (currentShader.shaderType === "fill") {
6148
- // Bind index buffer and issue draw
6149
- if (buffers.indexBuffer) {
6150
- const indexFormat = buffers.indexFormat || "uint16";
6151
- passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
6152
- passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
6989
+ //////////////////////////////////////////////
6990
+ // SHADER
6991
+ //////////////////////////////////////////////
6992
+
6993
+ // Writes a single field's value into a Float32Array+DataView at (baseOffset + field.offset).
6994
+ //
6995
+ // Field interface (shared by uniform fields from _parseStruct and struct storage schema fields):
6996
+ // baseType: string - 'f32', 'i32', 'u32', etc.
6997
+ // size: number - byte size of the field
6998
+ // offset: number - byte offset of the field within its struct
6999
+ // packInPlace: bool - true for mat3, written with manual column padding
7000
+ //
7001
+ // value: number or number[] - the data to write
7002
+ _packField(field, value, floatView, dataView, baseOffset) {
7003
+ if (value === undefined) return;
7004
+
7005
+ // Duck typing instead of instanceof to avoid importing a separate
7006
+ // copy of the Color/Vector classes
7007
+ if (value?.isVector) {
7008
+ value = value.values.length !== value.dimensions ? value.values.slice(0, value.dimensions) : value.values;
7009
+ } else if (value?.isColor) {
7010
+ value = value._getRGBA([1, 1, 1, 1]);
7011
+ }
7012
+ const byteOffset = baseOffset + field.offset;
7013
+ if (field.baseType === 'u32') {
7014
+ if (field.size === 4) {
7015
+ dataView.setUint32(byteOffset, value, true);
6153
7016
  } else {
6154
- passEncoder.draw(geometry.vertices.length, count, 0, 0);
7017
+ for (let i = 0; i < value.length; i++) {
7018
+ dataView.setUint32(byteOffset + i * 4, value[i], true);
7019
+ }
6155
7020
  }
6156
- } else if (currentShader.shaderType === "text") {
6157
- if (!buffers.indexBuffer) {
6158
- throw new Error("Text geometry must have an index buffer");
7021
+ } else if (field.baseType === 'i32') {
7022
+ if (field.size === 4) {
7023
+ dataView.setInt32(byteOffset, value, true);
7024
+ } else {
7025
+ for (let i = 0; i < value.length; i++) {
7026
+ dataView.setInt32(byteOffset + i * 4, value[i], true);
7027
+ }
6159
7028
  }
6160
- const indexFormat = buffers.indexFormat || "uint16";
6161
- passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
6162
- passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
6163
- }
6164
-
6165
- if (buffers.lineVerticesBuffer && currentShader.shaderType === "stroke") {
6166
- passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0);
7029
+ } else if (field.packInPlace) {
7030
+ // In-place packing for mat3: write directly to buffer with padding
7031
+ const base = byteOffset / 4;
7032
+ floatView[base + 0] = value[0]; floatView[base + 1] = value[1]; floatView[base + 2] = value[2];
7033
+ floatView[base + 4] = value[3]; floatView[base + 5] = value[4]; floatView[base + 6] = value[5];
7034
+ floatView[base + 8] = value[6]; floatView[base + 9] = value[7]; floatView[base + 10] = value[8];
7035
+ } else if (field.size === 4) {
7036
+ floatView.set([value], byteOffset / 4);
7037
+ } else {
7038
+ floatView.set(value, byteOffset / 4);
6167
7039
  }
6168
-
6169
- // Mark that we have pending draws that need submission
6170
- this._hasPendingDraws = true;
6171
7040
  }
6172
7041
 
6173
- //////////////////////////////////////////////
6174
- // SHADER
6175
- //////////////////////////////////////////////
6176
-
6177
7042
  _packUniformGroup(shader, groupUniforms, bufferInfo) {
6178
7043
  // Pack a single group's uniforms into a buffer
6179
7044
  const data = bufferInfo.data;
6180
7045
  const dataView = bufferInfo.dataView;
6181
-
6182
7046
  const offset = bufferInfo.offset || 0;
6183
7047
  for (const uniform of groupUniforms) {
6184
7048
  const fullUniform = shader.uniforms[uniform.name];
6185
7049
  if (!fullUniform || fullUniform.isSampler) continue;
6186
- const uniformData = fullUniform._mappedData;
6187
-
6188
- if (fullUniform.baseType === 'u32') {
6189
- if (fullUniform.size === 4) {
6190
- dataView.setUint32(offset + fullUniform.offset, uniformData, true);
6191
- } else {
6192
- for (let i = 0; i < uniformData.length; i++) {
6193
- dataView.setUint32(offset + fullUniform.offset + i * 4, uniformData[i], true);
6194
- }
6195
- }
6196
- } else if (fullUniform.baseType === 'i32') {
6197
- if (fullUniform.size === 4) {
6198
- dataView.setInt32(offset + fullUniform.offset, uniformData, true);
6199
- } else {
6200
- for (let i = 0; i < uniformData.length; i++) {
6201
- dataView.setInt32(offset + fullUniform.offset + i * 4, uniformData[i], true);
6202
- }
6203
- }
6204
- } else if (fullUniform.packInPlace) {
6205
- // In-place packing for mat3: write directly to buffer with padding
6206
- const baseOffset = (offset + fullUniform.offset) / 4;
6207
- // Column 0
6208
- data[baseOffset + 0] = uniformData[0];
6209
- data[baseOffset + 1] = uniformData[1];
6210
- data[baseOffset + 2] = uniformData[2];
6211
- // Column 1
6212
- data[baseOffset + 4] = uniformData[3];
6213
- data[baseOffset + 5] = uniformData[4];
6214
- data[baseOffset + 6] = uniformData[5];
6215
- // Column 2
6216
- data[baseOffset + 8] = uniformData[6];
6217
- data[baseOffset + 9] = uniformData[7];
6218
- data[baseOffset + 10] = uniformData[8];
6219
- } else if (fullUniform.size === 4) {
6220
- data.set([uniformData], (offset + fullUniform.offset) / 4);
6221
- } else if (uniformData !== undefined) {
6222
- data.set(uniformData, (offset + fullUniform.offset) / 4);
6223
- }
7050
+ this._packField(fullUniform, fullUniform._mappedData, data, dataView, offset);
6224
7051
  }
6225
7052
  }
6226
7053
 
@@ -6367,10 +7194,11 @@ function rendererWebGPU(p5, fn) {
6367
7194
  const uniformVarRegex = /@group\((\d+)\)\s+@binding\((\d+)\)\s+var<uniform>\s+(\w+)\s*:\s*(\w+);/g;
6368
7195
 
6369
7196
  let match;
6370
- while ((match = uniformVarRegex.exec(shader.vertSrc())) !== null) {
7197
+ const src = shader.shaderType === 'compute' ? shader.computeSrc() : shader.vertSrc();
7198
+ while ((match = uniformVarRegex.exec(src)) !== null) {
6371
7199
  const [_, groupNum, binding, varName, structType] = match;
6372
7200
  const bindingIndex = parseInt(binding);
6373
- const uniforms = this._parseStruct(shader.vertSrc(), structType);
7201
+ const uniforms = this._parseStruct(src, structType);
6374
7202
 
6375
7203
  uniformGroups.push({
6376
7204
  group: parseInt(groupNum),
@@ -6381,7 +7209,7 @@ function rendererWebGPU(p5, fn) {
6381
7209
  });
6382
7210
  }
6383
7211
 
6384
- if (uniformGroups.length === 0) {
7212
+ if (uniformGroups.length === 0 && shader.shaderType !== 'compute') {
6385
7213
  throw new Error('Expected at least one uniform struct bound to @group(0)');
6386
7214
  }
6387
7215
 
@@ -6408,6 +7236,10 @@ function rendererWebGPU(p5, fn) {
6408
7236
  // TODO: support other texture types
6409
7237
  const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d<f32>|sampler);/g;
6410
7238
 
7239
+ // Extract storage buffers
7240
+ const storageBuffers = {};
7241
+ const storageRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var<storage,\s*(read|read_write)>\s+(\w+)\s*:\s*array<\w+>/g;
7242
+
6411
7243
  // Track which bindings are taken by the struct properties we've parsed
6412
7244
  // (the rest should be textures/samplers)
6413
7245
  const structUniformBindings = {};
@@ -6417,8 +7249,11 @@ function rendererWebGPU(p5, fn) {
6417
7249
 
6418
7250
  for (const [src, visibility] of [
6419
7251
  [shader.vertSrc(), GPUShaderStage.VERTEX],
6420
- [shader.fragSrc(), GPUShaderStage.FRAGMENT]
7252
+ [shader.fragSrc(), GPUShaderStage.FRAGMENT],
7253
+ [shader.computeSrc ? shader.computeSrc() : null, GPUShaderStage.COMPUTE]
6421
7254
  ]) {
7255
+ if (!src) continue; // Skip if shader stage doesn't exist
7256
+
6422
7257
  let match;
6423
7258
  while ((match = samplerRegex.exec(src)) !== null) {
6424
7259
  const [_, group, binding, name, type] = match;
@@ -6453,21 +7288,51 @@ function rendererWebGPU(p5, fn) {
6453
7288
  samplerNode.textureSource = sampler;
6454
7289
  }
6455
7290
  }
7291
+
7292
+ // Parse storage buffers
7293
+ while ((match = storageRegex.exec(src)) !== null) {
7294
+ const [_, group, binding, accessMode, name] = match;
7295
+ const groupIndex = parseInt(group);
7296
+ const bindingIndex = parseInt(binding);
7297
+
7298
+ const key = `${groupIndex},${bindingIndex}`;
7299
+ const existing = storageBuffers[key];
7300
+ // If any stage uses read_write, the bind group layout must use read_write
7301
+ const finalAccessMode = (existing?.accessMode === 'read_write' || accessMode === 'read_write')
7302
+ ? 'read_write'
7303
+ : accessMode;
7304
+
7305
+ storageBuffers[key] = {
7306
+ visibility: (existing?.visibility || 0) | visibility,
7307
+ group: groupIndex,
7308
+ binding: bindingIndex,
7309
+ name,
7310
+ accessMode: finalAccessMode, // 'read' or 'read_write'
7311
+ isStorage: true,
7312
+ type: 'storage'
7313
+ };
7314
+ }
6456
7315
  }
6457
- return [...Object.values(allUniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers)];
7316
+
7317
+ // Store storage buffers on shader for later use
7318
+ shader._storageBuffers = Object.values(storageBuffers);
7319
+
7320
+ return [...Object.values(allUniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers), ...Object.values(storageBuffers)];
6458
7321
  }
6459
7322
 
6460
- getNextBindingIndex({ vert, frag }, group = 0) {
7323
+ getNextBindingIndex({ vert, frag, compute }, group = 0) {
6461
7324
  // Get the highest binding index in the specified group and return the next available
6462
- const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var(?:<uniform>)?\s+(\w+)\s*:\s*(texture_2d<f32>|sampler|uniform|\w+)/g;
7325
+ const bindingRegex = /@group\((\d+)\)\s*@binding\((\d+)\)/g;
6463
7326
  let maxBindingIndex = -1;
6464
7327
 
6465
- for (const [src, visibility] of [
6466
- [vert, GPUShaderStage.VERTEX],
6467
- [frag, GPUShaderStage.FRAGMENT]
6468
- ]) {
7328
+ const sources = [];
7329
+ if (vert) sources.push([vert, GPUShaderStage.VERTEX]);
7330
+ if (frag) sources.push([frag, GPUShaderStage.FRAGMENT]);
7331
+ if (compute) sources.push([compute, GPUShaderStage.COMPUTE]);
7332
+
7333
+ for (const [src, visibility] of sources) {
6469
7334
  let match;
6470
- while ((match = samplerRegex.exec(src)) !== null) {
7335
+ while ((match = bindingRegex.exec(src)) !== null) {
6471
7336
  const [_, groupIndex, bindingIndex] = match;
6472
7337
  if (parseInt(groupIndex) === group) {
6473
7338
  maxBindingIndex = Math.max(maxBindingIndex, parseInt(bindingIndex));
@@ -6482,7 +7347,7 @@ function rendererWebGPU(p5, fn) {
6482
7347
  if (uniform.isSampler) {
6483
7348
  uniform.texture =
6484
7349
  data instanceof Texture ? data : this.getTexture(data);
6485
- } else {
7350
+ } else if (!data?._isStorageBuffer) {
6486
7351
  uniform._mappedData = this._mapUniformData(uniform, uniform._cachedData);
6487
7352
  }
6488
7353
  shader.buffersDirty.add(uniform.group * 1000 + uniform.binding);
@@ -6589,7 +7454,7 @@ function rendererWebGPU(p5, fn) {
6589
7454
  rgb += components.emissive;
6590
7455
  return vec4<f32>(rgb, components.opacity);
6591
7456
  }`,
6592
- "vec4f getFinalColor": "(color: vec4<f32>) { return color; }",
7457
+ "vec4f getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
6593
7458
  "void afterFragment": "() {}",
6594
7459
  },
6595
7460
  }
@@ -6614,7 +7479,7 @@ function rendererWebGPU(p5, fn) {
6614
7479
  },
6615
7480
  fragment: {
6616
7481
  "void beforeFragment": "() {}",
6617
- "vec4<f32> getFinalColor": "(color: vec4<f32>) { return color; }",
7482
+ "vec4<f32> getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
6618
7483
  "void afterFragment": "() {}",
6619
7484
  },
6620
7485
  }
@@ -6640,7 +7505,7 @@ function rendererWebGPU(p5, fn) {
6640
7505
  fragment: {
6641
7506
  "void beforeFragment": "() {}",
6642
7507
  "Inputs getPixelInputs": "(inputs: Inputs) { return inputs; }",
6643
- "vec4<f32> getFinalColor": "(color: vec4<f32>) { return color; }",
7508
+ "vec4<f32> getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
6644
7509
  "bool shouldDiscard": "(outside: bool) { return outside; };",
6645
7510
  "void afterFragment": "() {}",
6646
7511
  },
@@ -6799,11 +7664,87 @@ function rendererWebGPU(p5, fn) {
6799
7664
  }
6800
7665
  );
6801
7666
 
6802
- let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment)\s*)?fn main[^{]+\{)/);
6803
- if (shaderType !== 'fragment') {
6804
- if (!main.match(/\@builtin\s*\(\s*instance_index\s*\)/)) {
6805
- main = main.replace(/\)\s*(->|\{)/, ', @builtin(instance_index) instanceID: u32) $1');
7667
+ let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment|compute)\s*(?:@workgroup_size\([^)]+\)\s*)?)?fn main[^{]+\{)/);
7668
+
7669
+ const getBuiltinParamName = (mainSrc, builtinName) => {
7670
+ const match = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`).exec(mainSrc);
7671
+ return match ? match[1] : null;
7672
+ };
7673
+
7674
+ const ensureBuiltinParam = (mainSrc, builtinName, fallbackName, typeName) => {
7675
+ const existingName = getBuiltinParamName(mainSrc, builtinName);
7676
+ if (existingName) {
7677
+ return { mainSrc, argName: existingName };
7678
+ }
7679
+
7680
+ const hasParams = /\(\s*\S/.test(mainSrc);
7681
+ const injectedMain = mainSrc.replace(
7682
+ /\)\s*(->|\{)/,
7683
+ `${hasParams ? ', ' : ''}@builtin(${builtinName}) ${fallbackName}: ${typeName}) $1`
7684
+ );
7685
+
7686
+ return { mainSrc: injectedMain, argName: fallbackName };
7687
+ };
7688
+
7689
+ const getMainStructParameter = (mainSrc) => {
7690
+ const match = /fn main\s*\(\s*(\w+)\s*:\s*(\w+)/.exec(mainSrc);
7691
+ if (!match) return null;
7692
+ return { inputName: match[1], structName: match[2] };
7693
+ };
7694
+
7695
+ const getStructBuiltinFieldName = (structName, builtinName) => {
7696
+ const structMatch = new RegExp(`struct\\s+${structName}\\s*\\{([^}]*)\\}`, 's').exec(preMain);
7697
+ if (!structMatch) return null;
7698
+ const fieldMatch = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`, 's').exec(structMatch[1]);
7699
+ return fieldMatch ? fieldMatch[1] : null;
7700
+ };
7701
+
7702
+ const appendHookParams = (params, additionalParams) => {
7703
+ if (additionalParams.length === 0) return params;
7704
+ const hasParams = !/^\(\s*\)$/.test(params);
7705
+ return `${params.slice(0, -1)}${hasParams ? ', ' : ''}${additionalParams.join(', ')})`;
7706
+ };
7707
+
7708
+ let hookExtraParams = [];
7709
+ let hookExtraArgs = [];
7710
+
7711
+ if (shaderType === 'vertex') {
7712
+ const ensuredInstance = ensureBuiltinParam(main, 'instance_index', 'instanceID', 'u32');
7713
+ main = ensuredInstance.mainSrc;
7714
+
7715
+ const ensuredVertex = ensureBuiltinParam(main, 'vertex_index', '_p5VertexId', 'u32');
7716
+ main = ensuredVertex.mainSrc;
7717
+
7718
+ hookExtraParams = ['instanceID: u32', '_p5VertexId: u32'];
7719
+ hookExtraArgs = [ensuredInstance.argName, ensuredVertex.argName];
7720
+ } else if (shaderType === 'fragment') {
7721
+ const directPositionArg = getBuiltinParamName(main, 'position');
7722
+ let fragmentPositionArg = directPositionArg;
7723
+
7724
+ if (!fragmentPositionArg) {
7725
+ const mainStructParam = getMainStructParameter(main);
7726
+ if (mainStructParam) {
7727
+ const positionField = getStructBuiltinFieldName(mainStructParam.structName, 'position');
7728
+ if (positionField) {
7729
+ fragmentPositionArg = `${mainStructParam.inputName}.${positionField}`;
7730
+ }
7731
+ }
6806
7732
  }
7733
+
7734
+ if (!fragmentPositionArg) {
7735
+ const ensuredPosition = ensureBuiltinParam(main, 'position', '_p5FragPos', 'vec4<f32>');
7736
+ main = ensuredPosition.mainSrc;
7737
+ fragmentPositionArg = ensuredPosition.argName;
7738
+ }
7739
+
7740
+ hookExtraParams = ['_p5FragPos: vec4<f32>'];
7741
+ hookExtraArgs = [fragmentPositionArg];
7742
+ } else if (shaderType === 'compute') {
7743
+ const ensuredGlobalId = ensureBuiltinParam(main, 'global_invocation_id', '_p5GlobalId', 'vec3<u32>');
7744
+ main = ensuredGlobalId.mainSrc;
7745
+
7746
+ hookExtraParams = ['_p5GlobalId: vec3<u32>'];
7747
+ hookExtraArgs = [ensuredGlobalId.argName];
6807
7748
  }
6808
7749
 
6809
7750
  // Inject hook uniforms as a separate struct at a new binding
@@ -6823,6 +7764,7 @@ function rendererWebGPU(p5, fn) {
6823
7764
  const nextBinding = this.getNextBindingIndex({
6824
7765
  vert: shaderType === 'vertex' ? preMain + (shader.hooks.vertex?.declarations ?? '') + shader.hooks.declarations : shader._vertSrc,
6825
7766
  frag: shaderType === 'fragment' ? preMain + (shader.hooks.fragment?.declarations ?? '') + shader.hooks.declarations : shader._fragSrc,
7767
+ compute: shaderType === 'compute' ? preMain + (shader.hooks.compute?.declarations ?? '') + shader.hooks.declarations : shader._computeSrc,
6826
7768
  }, 0);
6827
7769
 
6828
7770
  // Create HookUniforms struct and binding
@@ -6833,8 +7775,14 @@ ${hookUniformFields}}
6833
7775
 
6834
7776
  @group(0) @binding(${nextBinding}) var<uniform> hooks: HookUniforms;
6835
7777
  `;
6836
- // Insert before the first @group binding
6837
- preMain = preMain.replace(/(@group\(0\)\s+@binding)/, `${hookUniformsDecl}\n$1`);
7778
+ // Insert before the first @group binding, or at the end if there are none
7779
+ const replaced = preMain.replace(/(@group\(0\)\s+@binding)/, `${hookUniformsDecl}\n$1`);
7780
+ if (replaced === preMain) {
7781
+ // No @group bindings found in base shader, append to preMain
7782
+ preMain = preMain + '\n' + hookUniformsDecl;
7783
+ } else {
7784
+ preMain = replaced;
7785
+ }
6838
7786
  }
6839
7787
 
6840
7788
  // Handle varying variables by injecting them into VertexOutput and FragmentInput structs
@@ -6900,10 +7848,9 @@ ${hookUniformFields}}
6900
7848
  initStatements += ` ${varName} = INPUT_VAR.${varName};\n`;
6901
7849
  }
6902
7850
 
6903
- // Find the input parameter name from the main function signature (anchored to start)
6904
- const inputMatch = main.match(/fn main\s*\((\w+):\s*\w+\)/);
6905
- if (inputMatch) {
6906
- const inputVarName = inputMatch[1];
7851
+ const mainStructParam = getMainStructParameter(main);
7852
+ if (mainStructParam) {
7853
+ const inputVarName = mainStructParam.inputName;
6907
7854
  initStatements = initStatements.replace(/INPUT_VAR/g, inputVarName);
6908
7855
  // Insert after the main function parameter but before any other code (anchored to start)
6909
7856
  postMain = initStatements + postMain;
@@ -6911,12 +7858,56 @@ ${hookUniformFields}}
6911
7858
  }
6912
7859
  }
6913
7860
 
7861
+ // Handle instanceID varying for fragment access
7862
+ if (shader.hooks.instanceIDVarying) {
7863
+ const { name, declaration, source, interpolation } = shader.hooks.instanceIDVarying;
7864
+ const nextLocIndex = this._getNextAvailableLocation(preMain, shaderType);
7865
+ const interpAttr = interpolation ? ` @interpolate(${interpolation})` : '';
7866
+ const [varName, varType] = declaration.split(':').map(s => s.trim());
7867
+ const structMember = `@location(${nextLocIndex})${interpAttr} ${declaration},`;
7868
+
7869
+ if (shaderType === 'vertex') {
7870
+ // Inject into VertexOutput struct
7871
+ preMain = preMain.replace(
7872
+ /struct\s+VertexOutput\s+\{([^}]*)\}/,
7873
+ (match, body) => `struct VertexOutput {${body}\n${structMember}}`
7874
+ );
7875
+ // Add private global
7876
+ preMain += `var<private> ${declaration};\n`;
7877
+ // Assign from built-in instanceID at start of main()
7878
+ postMain = `\n ${varName} = ${source};\n` + postMain;
7879
+ // Copy to output struct before return
7880
+ const returnMatch = postMain.match(/return\s+(\w+)\s*;/);
7881
+ if (returnMatch) {
7882
+ const outputVarName = returnMatch[1];
7883
+ postMain = postMain.replace(
7884
+ /(return\s+\w+\s*;)/g,
7885
+ `${outputVarName}.${varName} = ${varName};\n $1`
7886
+ );
7887
+ }
7888
+ } else if (shaderType === 'fragment') {
7889
+ // Inject into FragmentInput struct
7890
+ preMain = preMain.replace(
7891
+ /struct\s+FragmentInput\s+\{([^}]*)\}/,
7892
+ (match, body) => `struct FragmentInput {${body}\n${structMember}}`
7893
+ );
7894
+ // Add private global
7895
+ preMain += `var<private> ${declaration};\n`;
7896
+ // Initialize from input struct at start of main()
7897
+ const mainStructParam = getMainStructParameter(main);
7898
+ if (mainStructParam) {
7899
+ const inputVarName = mainStructParam.inputName;
7900
+ postMain = `\n ${varName} = ${inputVarName}.${varName};\n` + postMain;
7901
+ }
7902
+ }
7903
+ }
7904
+
6914
7905
  let hooks = '';
6915
7906
  let defines = '';
6916
7907
  if (shader.hooks.declarations) {
6917
7908
  hooks += shader.hooks.declarations + '\n';
6918
7909
  }
6919
- if (shader.hooks[shaderType].declarations) {
7910
+ if (shader.hooks[shaderType] && shader.hooks[shaderType].declarations) {
6920
7911
  hooks += shader.hooks[shaderType].declarations + '\n';
6921
7912
  }
6922
7913
  for (const hookDef in shader.hooks.helpers) {
@@ -6940,11 +7931,7 @@ ${hookUniformFields}}
6940
7931
 
6941
7932
  let [_, params, body] = /^(\([^\)]*\))((?:.|\n)*)$/.exec(shader.hooks[shaderType][hookDef]);
6942
7933
 
6943
- if (shaderType !== 'fragment') {
6944
- // Splice the instance ID in as a final parameter to every WGSL hook function
6945
- let hasParams = !!params.match(/^\(\s*\S+.*\)$/);
6946
- params = params.slice(0, -1) + (hasParams ? ', ' : '') + 'instanceID: u32)';
6947
- }
7934
+ params = appendHookParams(params, hookExtraParams);
6948
7935
 
6949
7936
  if (hookType === 'void') {
6950
7937
  hooks += `fn HOOK_${hookName}${params}${body}\n`;
@@ -6953,40 +7940,45 @@ ${hookUniformFields}}
6953
7940
  }
6954
7941
  }
6955
7942
 
6956
- // Add the instance ID as a final parameter to each hook call
6957
- if (shaderType !== 'fragment') {
6958
- const addInstanceIDParam = (src) => {
6959
- let result = src;
6960
- let idx = 0;
6961
- let match;
6962
- do {
6963
- match = /HOOK_\w+\(/.exec(result.slice(idx));
6964
- if (match) {
6965
- idx += match.index + match[0].length - 1;
6966
- let nesting = 0;
6967
- let hasParams = false;
6968
- while (idx < result.length) {
6969
- if (result[idx] === '(') {
6970
- nesting++;
6971
- } else if (result[idx] === ')') {
6972
- nesting--;
6973
- } else if (result[idx].match(/\S/)) {
6974
- hasParams = true;
6975
- }
6976
- idx++;
6977
- if (nesting === 0) {
6978
- break;
6979
- }
7943
+ // Pass stage-specific builtins from main to each hook call.
7944
+ // Collect ALL HOOK_ calls (including nested ones) then insert
7945
+ // extra args from right to left so position shifts don't
7946
+ // invalidate earlier insertion points.
7947
+ if (hookExtraArgs.length > 0) {
7948
+ const addHookArgs = (src) => {
7949
+ const insertions = [];
7950
+ let searchIdx = 0;
7951
+ let m;
7952
+ while ((m = /HOOK_\w+\(/.exec(src.slice(searchIdx))) !== null) {
7953
+ const openParen = searchIdx + m.index + m[0].length - 1;
7954
+ let pos = openParen + 1;
7955
+ let nesting = 1;
7956
+ let hasParams = false;
7957
+ while (pos < src.length && nesting > 0) {
7958
+ if (src[pos] === '(') nesting++;
7959
+ else if (src[pos] === ')') {
7960
+ nesting--;
7961
+ if (nesting === 0) break;
7962
+ } else if (/\S/.test(src[pos])) {
7963
+ hasParams = true;
6980
7964
  }
6981
- const insertion = (hasParams ? ', ' : '') + 'instanceID';
6982
- result = result.slice(0, idx-1) + insertion + result.slice(idx-1);
6983
- idx += insertion.length;
7965
+ pos++;
6984
7966
  }
6985
- } while (match);
7967
+ insertions.push({ pos, hasParams });
7968
+ searchIdx = openParen + 1;
7969
+ }
7970
+
7971
+ insertions.sort((a, b) => b.pos - a.pos);
7972
+
7973
+ let result = src;
7974
+ for (const { pos, hasParams } of insertions) {
7975
+ const insertion = (hasParams ? ', ' : '') + hookExtraArgs.join(', ');
7976
+ result = result.slice(0, pos) + insertion + result.slice(pos);
7977
+ }
6986
7978
  return result;
6987
7979
  };
6988
- preMain = addInstanceIDParam(preMain);
6989
- postMain = addInstanceIDParam(postMain);
7980
+ preMain = addHookArgs(preMain);
7981
+ postMain = addHookArgs(postMain);
6990
7982
  }
6991
7983
 
6992
7984
  return preMain + '\n' + defines + hooks + main + postMain;
@@ -7045,6 +8037,10 @@ ${hookUniformFields}}
7045
8037
  body = shader.hooks.fragment[hookName];
7046
8038
  fullSrc = shader._fragSrc;
7047
8039
  }
8040
+ if (!body) {
8041
+ body = shader.hooks.compute[hookName];
8042
+ fullSrc = shader._computeSrc;
8043
+ }
7048
8044
  if (!body) {
7049
8045
  throw new Error(`Can't find hook ${hookName}!`);
7050
8046
  }
@@ -7176,7 +8172,7 @@ ${hookUniformFields}}
7176
8172
  }
7177
8173
 
7178
8174
  defaultFramebufferAntialias() {
7179
- return true;
8175
+ return this._pInst._webgpuAttributes?.antialias !== false;
7180
8176
  }
7181
8177
 
7182
8178
  supportsFramebufferAntialias() {
@@ -7369,6 +8365,267 @@ ${hookUniformFields}}
7369
8365
  };
7370
8366
  }
7371
8367
 
8368
+ // Maps a plain JS value to the WGSL type string that represents it in a struct.
8369
+ _jsValueToWgslType(value) {
8370
+ if (typeof value === 'number') return 'f32';
8371
+ // Duck typing instead of instanceof to avoid importing a separate
8372
+ // copy of the Color/Vector classes
8373
+ if (value?.isVector) {
8374
+ if (value.dimensions === 2) return 'vec2f';
8375
+ if (value.dimensions === 3) return 'vec3f';
8376
+ if (value.dimensions === 4) return 'vec4f';
8377
+ throw new Error(`Unsupported vector dimension ${value.dimensions} for struct storage field`);
8378
+ }
8379
+ if (value?.isColor) {
8380
+ return 'vec4f';
8381
+ }
8382
+ if (Array.isArray(value)) {
8383
+ if (value.length === 2) return 'vec2f';
8384
+ if (value.length === 3) return 'vec3f';
8385
+ if (value.length === 4) return 'vec4f';
8386
+ throw new Error(`Unsupported array length ${value.length} for struct storage field`);
8387
+ }
8388
+ throw new Error(`Unsupported value type ${typeof value} for struct storage field`);
8389
+ }
8390
+
8391
+ // Infers a struct schema from the first element of a struct array.
8392
+ //
8393
+ // Returns { fields, stride, structBody } where:
8394
+ // fields: field has the _packField interface (baseType, size, offset, packInPlace) plus:
8395
+ // name: string - JS property name
8396
+ // dim: number - float component count, used when creating StrandsNodes
8397
+ // structBody: everything inside the { ... } of a WGSL struct definition
8398
+ // stride: how many bytes are reserved for this struct in the buffer
8399
+ _inferStructSchema(firstElement) {
8400
+ const entries = Object.entries(firstElement);
8401
+
8402
+ if (!p5.disableFriendlyErrors) {
8403
+ for (const [name, value] of entries) {
8404
+ if (
8405
+ value !== null &&
8406
+ typeof value === 'object' &&
8407
+ !Array.isArray(value) &&
8408
+ // Duck typing instead of instanceof to avoid importing a separate
8409
+ // copy of the Color/Vector classes
8410
+ !value?.isVector &&
8411
+ !value?.isColor
8412
+ ) {
8413
+ p5._friendlyError(
8414
+ `The "${name}" property in your storage data contains a nested object. ` +
8415
+ `Make sure you only use properties with numbers, arrays of numbers, or p5.Vector.`,
8416
+ 'createStorage'
8417
+ );
8418
+ }
8419
+ }
8420
+ }
8421
+
8422
+ const fieldLines = entries.map(([name, value]) =>
8423
+ ` ${name}: ${this._jsValueToWgslType(value)},`
8424
+ ).join('\n');
8425
+ const structBody = `{\n${fieldLines}\n}`;
8426
+ const elements = this._parseStruct(`struct _Tmp ${structBody}`, '_Tmp');
8427
+
8428
+ let maxEnd = 0;
8429
+ let maxAlign = 1;
8430
+ const fields = entries.map(([name, value]) => {
8431
+ const el = elements[name];
8432
+ maxEnd = Math.max(maxEnd, el.offsetEnd);
8433
+ // Alignment for scalars/vectors: <=4 -> 4, <=8 -> 8, else 16
8434
+ const align = el.size <= 4 ? 4 : el.size <= 8 ? 8 : 16;
8435
+ maxAlign = Math.max(maxAlign, align);
8436
+ // Track original JS type for reconstruction during readback
8437
+ const kind = value?.isVector ? 'vector'
8438
+ : value?.isColor ? 'color'
8439
+ : undefined;
8440
+ return {
8441
+ name,
8442
+ baseType: el.baseType,
8443
+ size: el.size,
8444
+ offset: el.offset,
8445
+ packInPlace: el.packInPlace ?? false,
8446
+ dim: el.size / 4,
8447
+ kind,
8448
+ };
8449
+ });
8450
+
8451
+ const stride = Math.ceil(maxEnd / maxAlign) * maxAlign;
8452
+ return { fields, stride, structBody };
8453
+ }
8454
+
8455
+ // Packs an array of plain objects into a Float32Array using the given struct schema.
8456
+ // Reuses _packField so layout rules match uniform packing exactly.
8457
+ _packStructArray(data, schema) {
8458
+ const { fields, stride } = schema;
8459
+ const totalBytes = Math.max(data.length * stride, 16);
8460
+ const alignedBytes = Math.ceil(totalBytes / 16) * 16;
8461
+ const buffer = new ArrayBuffer(alignedBytes);
8462
+ const floatView = new Float32Array(buffer);
8463
+ const dataView = new DataView(buffer);
8464
+ for (let i = 0; i < data.length; i++) {
8465
+ const item = data[i];
8466
+ const baseOffset = i * stride;
8467
+ for (const field of fields) {
8468
+ this._packField(field, item[field.name], floatView, dataView, baseOffset);
8469
+ }
8470
+ }
8471
+ return floatView;
8472
+ }
8473
+
8474
+ // Inverse of _packStructArray reads packed buffer back into plain JS objects
8475
+ // using the same schema layout - fields, stride and offsets
8476
+ _unpackStructArray(floatView, schema) {
8477
+ const { fields, stride } = schema;
8478
+ const dataView = new DataView(floatView.buffer);
8479
+ const count = Math.floor(floatView.byteLength / stride);
8480
+ const result = [];
8481
+
8482
+ for (let i = 0; i < count; i++) {
8483
+ const item = {};
8484
+ const baseOffset = i * stride;
8485
+ for (const field of fields) {
8486
+ const byteOffset = baseOffset + field.offset;
8487
+ const n = field.size / 4;
8488
+
8489
+ if (field.baseType === 'u32') {
8490
+ if (n === 1) {
8491
+ item[field.name] = dataView.getUint32(byteOffset, true);
8492
+ } else {
8493
+ item[field.name] = Array.from({ length: n }, (_, j) =>
8494
+ dataView.getUint32(byteOffset + j * 4, true)
8495
+ );
8496
+ }
8497
+ } else if (field.baseType === 'i32') {
8498
+ if (n === 1) {
8499
+ item[field.name] = dataView.getInt32(byteOffset, true);
8500
+ } else {
8501
+ item[field.name] = Array.from({ length: n }, (_, j) =>
8502
+ dataView.getInt32(byteOffset + j * 4, true)
8503
+ );
8504
+ }
8505
+ } else {
8506
+ const idx = byteOffset / 4;
8507
+ if (n === 1) {
8508
+ item[field.name] = floatView[idx];
8509
+ } else {
8510
+ const values = Array.from(floatView.slice(idx, idx + n));
8511
+ if (field.kind === 'vector') {
8512
+ item[field.name] = this._pInst.createVector(...values);
8513
+ } else if (field.kind === 'color') {
8514
+ // Color was packed as normalized RGBA [0-1] via _getRGBA([1,1,1,1])
8515
+ // Scale back to the current colorMode range
8516
+ const maxes = this.states.colorMaxes[this.states.colorMode];
8517
+ item[field.name] = this._pInst.color(
8518
+ values[0] * maxes[0], values[1] * maxes[1],
8519
+ values[2] * maxes[2], values[3] * maxes[3]
8520
+ );
8521
+ } else {
8522
+ item[field.name] = values;
8523
+ }
8524
+ }
8525
+ }
8526
+ }
8527
+ result.push(item);
8528
+ }
8529
+
8530
+ return result;
8531
+ }
8532
+
8533
+ createStorage(dataOrCount) {
8534
+ const device = this.device;
8535
+
8536
+ // Struct array: an array of plain objects
8537
+ if (Array.isArray(dataOrCount) && dataOrCount.length > 0 &&
8538
+ typeof dataOrCount[0] === 'object' && !Array.isArray(dataOrCount[0])) {
8539
+ if (!p5.disableFriendlyErrors && dataOrCount.length > 1) {
8540
+ const firstKeys = Object.keys(dataOrCount[0]);
8541
+ let warned = false;
8542
+ for (let i = 1; i < dataOrCount.length; i++) {
8543
+ const el = dataOrCount[i];
8544
+ const elKeys = Object.keys(el);
8545
+ const sameKeys = firstKeys.length === elKeys.length &&
8546
+ firstKeys.every((k, j) => k === elKeys[j]);
8547
+ if (!sameKeys) {
8548
+ p5._friendlyError(
8549
+ `Element ${i} has different fields than element 0. ` +
8550
+ `All elements should have the same properties.`,
8551
+ 'createStorage'
8552
+ );
8553
+ break;
8554
+ }
8555
+ for (const key of firstKeys) {
8556
+ const firstType = this._jsValueToWgslType(dataOrCount[0][key]);
8557
+ const elType = this._jsValueToWgslType(el[key]);
8558
+ if (firstType !== elType) {
8559
+ p5._friendlyError(
8560
+ `The "${key}" property of element ${i} has type ${elType} ` +
8561
+ `but element 0 has type ${firstType}. Proporties should have the same type across all elements.`,
8562
+ 'createStorage'
8563
+ );
8564
+ warned = true;
8565
+ break;
8566
+ }
8567
+ }
8568
+ if (warned) break;
8569
+ }
8570
+ }
8571
+ const schema = this._inferStructSchema(dataOrCount[0]);
8572
+ const packed = this._packStructArray(dataOrCount, schema);
8573
+ const size = packed.byteLength;
8574
+ const buffer = device.createBuffer({
8575
+ size,
8576
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
8577
+ mappedAtCreation: true,
8578
+ });
8579
+ new Float32Array(buffer.getMappedRange()).set(packed);
8580
+ buffer.unmap();
8581
+ const storageBuffer = new StorageBuffer(buffer, size, this, schema);
8582
+ this._storageBuffers.add(storageBuffer);
8583
+ return storageBuffer;
8584
+ }
8585
+
8586
+ // Determine buffer size and initial data
8587
+ let size, initialData;
8588
+ if (typeof dataOrCount === 'number') {
8589
+ // createStorage(count) - zero-initialized
8590
+ size = dataOrCount * 4; // floats are 4 bytes
8591
+ initialData = new Float32Array(dataOrCount);
8592
+ } else {
8593
+ // createStorage(array) - from data
8594
+ if (dataOrCount instanceof Float32Array) {
8595
+ initialData = dataOrCount;
8596
+ } else if (Array.isArray(dataOrCount)) {
8597
+ initialData = new Float32Array(dataOrCount);
8598
+ } else {
8599
+ throw new Error('createStorage expects a number or array/Float32Array');
8600
+ }
8601
+ size = initialData.byteLength;
8602
+ }
8603
+
8604
+ // Align to 16 bytes (WGSL storage buffer alignment requirement)
8605
+ size = Math.ceil(size / 16) * 16;
8606
+
8607
+ // Create storage buffer with STORAGE | COPY_DST | COPY_SRC usage
8608
+ const buffer = device.createBuffer({
8609
+ size,
8610
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
8611
+ mappedAtCreation: initialData.length > 0
8612
+ });
8613
+
8614
+ // Write initial data if provided
8615
+ if (initialData.length > 0) {
8616
+ const mapping = new Float32Array(buffer.getMappedRange());
8617
+ mapping.set(initialData);
8618
+ buffer.unmap();
8619
+ }
8620
+
8621
+ const storageBuffer = new StorageBuffer(buffer, size, this);
8622
+
8623
+ // Track for cleanup
8624
+ this._storageBuffers.add(storageBuffer);
8625
+
8626
+ return storageBuffer;
8627
+ }
8628
+
7372
8629
  _getWebGPUColorFormat(framebuffer) {
7373
8630
  if (framebuffer.format === FLOAT) {
7374
8631
  return framebuffer.channels === RGBA ? 'rgba32float' : 'rgba32float';
@@ -7665,10 +8922,6 @@ ${hookUniformFields}}
7665
8922
  return super.filter(...args);
7666
8923
  }
7667
8924
 
7668
- getNoiseShaderSnippet() {
7669
- return noiseWGSL;
7670
- }
7671
-
7672
8925
 
7673
8926
  baseFilterShader() {
7674
8927
  if (!this._baseFilterShader) {
@@ -7692,6 +8945,21 @@ ${hookUniformFields}}
7692
8945
  return this._baseFilterShader;
7693
8946
  }
7694
8947
 
8948
+ baseComputeShader() {
8949
+ if (!this._baseComputeShader) {
8950
+ this._baseComputeShader = new Shader(
8951
+ this,
8952
+ baseComputeShader,
8953
+ {
8954
+ compute: {
8955
+ 'void iteration': '(index: vec3<i32>) {}',
8956
+ },
8957
+ }
8958
+ );
8959
+ }
8960
+ return this._baseComputeShader;
8961
+ }
8962
+
7695
8963
  /*
7696
8964
  * WebGPU-specific implementation of imageLight shader creation
7697
8965
  */
@@ -7791,6 +9059,69 @@ ${hookUniformFields}}
7791
9059
  glDataType: dataType || 'uint8'
7792
9060
  };
7793
9061
  }
9062
+
9063
+ compute(shader, x, y = 1, z = 1) {
9064
+ if (shader.shaderType !== 'compute') {
9065
+ throw new Error('compute() can only be called with a compute shader');
9066
+ }
9067
+
9068
+ this._finishActiveRenderPass();
9069
+
9070
+ // Ensure shader is initialized and finalized
9071
+ if (!shader._compiled) {
9072
+ shader.init();
9073
+ }
9074
+
9075
+ // Set default uniforms
9076
+ shader.setDefaultUniforms();
9077
+ shader.setUniform('uTotalCount', [x, y, z]);
9078
+
9079
+ // Calculate optimal workgroup size (8x8x1 = 64 threads per workgroup)
9080
+ const WORKGROUP_SIZE_X = 8;
9081
+ const WORKGROUP_SIZE_Y = 8;
9082
+ const WORKGROUP_SIZE_Z = 1;
9083
+
9084
+ // auto spreading: if any dimension is too large or for performance optimization,
9085
+ // spread total iteration count across dimensions
9086
+ const totalIterations = x * y * z;
9087
+ const MAX_THREADS_PER_DIM = 65535 * 8;
9088
+
9089
+ let px = x;
9090
+ let py = y;
9091
+ let pz = z;
9092
+
9093
+ // we spread if we exceed GPU limits OR if it involves a large 1D dispatch
9094
+ const exceedsLimits = x > MAX_THREADS_PER_DIM || y > MAX_THREADS_PER_DIM || z > MAX_THREADS_PER_DIM;
9095
+ const isLarge1D = totalIterations > 1024 && y === 1 && z === 1;
9096
+
9097
+ if (exceedsLimits || isLarge1D) {
9098
+ // Always use 2D square spreading (√N × √N).
9099
+ // Benchmarks showed 2D square equals or outperforms 3D cube at every
9100
+ // scale tested, with simpler index reconstruction in the shader.
9101
+ px = Math.ceil(Math.sqrt(totalIterations));
9102
+ py = Math.ceil(totalIterations / px);
9103
+ pz = 1;
9104
+ }
9105
+
9106
+ shader.setUniform('uPhysicalCount', [px, py, pz]);
9107
+
9108
+ const workgroupCountX = Math.ceil(px / WORKGROUP_SIZE_X);
9109
+ const workgroupCountY = Math.ceil(py / WORKGROUP_SIZE_Y);
9110
+ const workgroupCountZ = Math.ceil(pz / WORKGROUP_SIZE_Z);
9111
+
9112
+ const commandEncoder = this.device.createCommandEncoder();
9113
+ const passEncoder = commandEncoder.beginComputePass();
9114
+ this.setupShaderBindGroups(shader, passEncoder, {
9115
+ compute: true,
9116
+ workgroupSize: [WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y, WORKGROUP_SIZE_Z],
9117
+ });
9118
+
9119
+ // Dispatch compute workgroups
9120
+ passEncoder.dispatchWorkgroups(workgroupCountX, workgroupCountY, workgroupCountZ);
9121
+
9122
+ passEncoder.end();
9123
+ this.device.queue.submit([commandEncoder.finish()]);
9124
+ }
7794
9125
  }
7795
9126
 
7796
9127
  p5.RendererWebGPU = RendererWebGPU;
@@ -7801,6 +9132,7 @@ ${hookUniformFields}}
7801
9132
  fn.setAttributes = async function (key, value) {
7802
9133
  return this._renderer._setAttributes(key, value);
7803
9134
  };
9135
+
7804
9136
  }
7805
9137
 
7806
9138
  if (typeof p5 !== "undefined") {