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