p5 2.2.3 → 2.3.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accessibility/color_namer.js +9 -11
- package/dist/accessibility/describe.js +0 -1
- package/dist/accessibility/gridOutput.js +0 -1
- package/dist/accessibility/index.js +9 -10
- package/dist/accessibility/outputs.js +0 -1
- package/dist/accessibility/textOutput.js +0 -1
- package/dist/app.js +11 -10
- package/dist/app.node.js +122 -0
- package/dist/color/color_conversion.js +9 -11
- package/dist/color/creating_reading.js +1 -1
- package/dist/color/index.js +2 -2
- package/dist/color/p5.Color.js +1 -1
- package/dist/color/setting.js +25 -12
- package/dist/{constants-BdTiYOQI.js → constants-0wkVUfqa.js} +2 -2
- package/dist/core/States.js +1 -1
- package/dist/core/constants.js +1 -1
- package/dist/core/environment.js +28 -29
- package/dist/core/filterShaders.js +1 -1
- package/dist/core/friendly_errors/fes_core.js +9 -8
- package/dist/core/friendly_errors/file_errors.js +1 -2
- package/dist/core/friendly_errors/index.js +1 -1
- package/dist/core/friendly_errors/param_validator.js +737 -640
- package/dist/core/friendly_errors/sketch_verifier.js +1 -1
- package/dist/core/friendly_errors/stacktrace.js +0 -1
- package/dist/core/helpers.js +3 -4
- package/dist/core/init.js +24 -21
- package/dist/core/internationalization.js +1 -1
- package/dist/core/legacy.js +9 -11
- package/dist/core/main.js +9 -10
- package/dist/core/p5.Graphics.js +5 -5
- package/dist/core/p5.Renderer.js +3 -3
- package/dist/core/p5.Renderer2D.js +9 -10
- package/dist/core/p5.Renderer3D.js +5 -5
- package/dist/core/rendering.js +5 -5
- package/dist/core/structure.js +0 -1
- package/dist/core/transform.js +7 -16
- package/dist/{creating_reading-C7hu6sg1.js → creating_reading-DLkHH80h.js} +11 -8
- package/dist/data/local_storage.js +0 -1
- package/dist/dom/dom.js +2 -3
- package/dist/dom/index.js +2 -2
- package/dist/dom/p5.Element.js +2 -2
- package/dist/dom/p5.MediaElement.js +2 -2
- package/dist/events/acceleration.js +5 -3
- package/dist/events/keyboard.js +0 -1
- package/dist/events/pointer.js +0 -2
- package/dist/image/const.js +1 -1
- package/dist/image/filterRenderer2D.js +19 -12
- package/dist/image/image.js +5 -5
- package/dist/image/index.js +5 -5
- package/dist/image/loading_displaying.js +5 -5
- package/dist/image/p5.Image.js +3 -3
- package/dist/image/pixels.js +0 -1
- package/dist/io/files.js +5 -5
- package/dist/io/index.js +5 -5
- package/dist/io/p5.Table.js +0 -1
- package/dist/io/p5.TableRow.js +0 -1
- package/dist/io/p5.XML.js +0 -1
- package/dist/{ir_builders-Cd6rU9Vm.js → ir_builders-C2ebb6Lu.js} +234 -1
- package/dist/{main-H_nu4eDs.js → main-D2kqeMXM.js} +107 -136
- package/dist/math/Matrices/Matrix.js +1 -1
- package/dist/math/Matrices/MatrixNumjs.js +1 -1
- package/dist/math/calculation.js +0 -1
- package/dist/math/index.js +3 -1
- package/dist/math/math.js +3 -17
- package/dist/math/noise.js +0 -1
- package/dist/math/p5.Matrix.js +1 -2
- package/dist/math/p5.Vector.js +237 -279
- package/dist/math/patch-vector.js +75 -0
- package/dist/math/random.js +0 -1
- package/dist/math/trigonometry.js +3 -4
- package/dist/{p5.Renderer-BmD2P6Wv.js → p5.Renderer-CQI8PO1F.js} +31 -24
- package/dist/{rendering-CC8JNTwG.js → rendering-ltTIxpF2.js} +732 -44
- package/dist/shape/2d_primitives.js +1 -4
- package/dist/shape/attributes.js +43 -8
- package/dist/shape/curves.js +0 -1
- package/dist/shape/custom_shapes.js +260 -5
- package/dist/shape/index.js +2 -2
- package/dist/shape/vertex.js +0 -2
- package/dist/strands/ir_builders.js +1 -1
- package/dist/strands/ir_types.js +5 -1
- package/dist/strands/p5.strands.js +286 -31
- package/dist/strands/strands_api.js +179 -8
- package/dist/strands/strands_codegen.js +26 -8
- package/dist/strands/strands_conditionals.js +1 -1
- package/dist/strands/strands_for.js +1 -1
- package/dist/strands/strands_node.js +1 -1
- package/dist/strands/strands_ternary.js +56 -0
- package/dist/strands/strands_transpiler.js +416 -251
- package/dist/strands_glslBackend-i-ReKgZo.js +423 -0
- package/dist/type/index.js +3 -3
- package/dist/type/lib/Typr.js +1 -1
- package/dist/type/p5.Font.js +3 -3
- package/dist/type/textCore.js +31 -24
- package/dist/utilities/conversion.js +0 -1
- package/dist/utilities/time_date.js +0 -1
- package/dist/utilities/utility_functions.js +0 -1
- package/dist/webgl/3d_primitives.js +5 -5
- package/dist/webgl/GeometryBuilder.js +1 -1
- package/dist/webgl/ShapeBuilder.js +26 -1
- package/dist/webgl/enums.js +1 -1
- package/dist/webgl/index.js +8 -9
- package/dist/webgl/interaction.js +8 -4
- package/dist/webgl/light.js +5 -5
- package/dist/webgl/loading.js +60 -21
- package/dist/webgl/material.js +5 -5
- package/dist/webgl/p5.Camera.js +5 -5
- package/dist/webgl/p5.Framebuffer.js +5 -5
- package/dist/webgl/p5.Geometry.js +3 -5
- package/dist/webgl/p5.Quat.js +1 -1
- package/dist/webgl/p5.RendererGL.js +17 -21
- package/dist/webgl/p5.Shader.js +129 -36
- package/dist/webgl/p5.Texture.js +5 -5
- package/dist/webgl/strands_glslBackend.js +5 -386
- package/dist/webgl/text.js +5 -5
- package/dist/webgl/utils.js +5 -5
- package/dist/webgl2Compatibility-DA7DLMuq.js +7 -0
- package/dist/webgpu/index.js +7 -3
- package/dist/webgpu/p5.RendererWebGPU.js +1146 -180
- package/dist/webgpu/shaders/color.js +1 -1
- package/dist/webgpu/shaders/compute.js +32 -0
- package/dist/webgpu/shaders/functions/randomComputeWGSL.js +31 -0
- package/dist/webgpu/shaders/functions/randomVertWGSL.js +30 -0
- package/dist/webgpu/shaders/functions/randomWGSL.js +30 -0
- package/dist/webgpu/shaders/line.js +1 -1
- package/dist/webgpu/shaders/material.js +3 -3
- package/dist/webgpu/strands_wgslBackend.js +137 -15
- package/lib/p5.esm.js +4092 -1950
- package/lib/p5.esm.min.js +1 -1
- package/lib/p5.js +4092 -1950
- package/lib/p5.min.js +1 -1
- package/lib/p5.webgpu.esm.js +1748 -306
- package/lib/p5.webgpu.js +1747 -305
- package/lib/p5.webgpu.min.js +1 -1
- package/package.json +6 -1
- package/types/global.d.ts +4182 -2441
- package/types/p5.d.ts +2776 -1675
- 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.
|
|
17
|
+
const VERSION = '2.3.0-rc.1';
|
|
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
|
-
|
|
2502
|
-
|
|
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.
|
|
3843
|
-
frag: strandsContext.baseShader.
|
|
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
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
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
|
-
//
|
|
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
|
|
4140
|
-
|
|
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, '
|
|
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
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
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,338 @@ 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
|
+
* Updates a single element in the buffer at a given index. Use this
|
|
5290
|
+
* when only a small number of elements need to change. If you need to
|
|
5291
|
+
* replace all the data at once, use
|
|
5292
|
+
* <a href="#/p5.StorageBuffer/update">`update()`</a> instead.
|
|
5293
|
+
*
|
|
5294
|
+
* ```js
|
|
5295
|
+
* let buf;
|
|
5296
|
+
*
|
|
5297
|
+
* async function setup() {
|
|
5298
|
+
* await createCanvas(100, 100, WEBGPU);
|
|
5299
|
+
*
|
|
5300
|
+
* // Float buffer: update one value by index
|
|
5301
|
+
* buf = createStorage(new Float32Array([1, 2, 3, 4]));
|
|
5302
|
+
* buf.set(2, 9.5); // only index 2 changes → [1, 2, 9.5, 4]
|
|
5303
|
+
*
|
|
5304
|
+
* let result = await buf.read();
|
|
5305
|
+
* print(result[2]); // 9.5
|
|
5306
|
+
* describe('Prints 9.5 to the console.');
|
|
5307
|
+
* }
|
|
5308
|
+
* ```
|
|
5309
|
+
*
|
|
5310
|
+
* ```js
|
|
5311
|
+
* let particles;
|
|
5312
|
+
* const numParticles = 100;
|
|
5313
|
+
*
|
|
5314
|
+
* async function setup() {
|
|
5315
|
+
* await createCanvas(100, 100, WEBGPU);
|
|
5316
|
+
* particles = createStorage(makeParticles());
|
|
5317
|
+
*
|
|
5318
|
+
* // Replace particle 42 without touching the others
|
|
5319
|
+
* particles.set(42, {
|
|
5320
|
+
* position: createVector(0, 0),
|
|
5321
|
+
* velocity: createVector(1, 0),
|
|
5322
|
+
* });
|
|
5323
|
+
*
|
|
5324
|
+
* // Read back to confirm the update
|
|
5325
|
+
* let result = await particles.read();
|
|
5326
|
+
* print(result[42].position.x, result[42].position.y); // 0, 0
|
|
5327
|
+
* describe('Prints the position of particle 42 after updating it.');
|
|
5328
|
+
* }
|
|
5329
|
+
*
|
|
5330
|
+
* function makeParticles() {
|
|
5331
|
+
* let data = [];
|
|
5332
|
+
* for (let i = 0; i < numParticles; i++) {
|
|
5333
|
+
* data.push({
|
|
5334
|
+
* position: createVector(random(width), random(height)),
|
|
5335
|
+
* velocity: createVector(random(-1, 1), random(-1, 1)),
|
|
5336
|
+
* });
|
|
5337
|
+
* }
|
|
5338
|
+
* return data;
|
|
5339
|
+
* }
|
|
5340
|
+
* ```
|
|
5341
|
+
*
|
|
5342
|
+
* @method set
|
|
5343
|
+
* @for p5.StorageBuffer
|
|
5344
|
+
* @beta
|
|
5345
|
+
* @webgpu
|
|
5346
|
+
* @webgpuOnly
|
|
5347
|
+
* @param {Number} index The zero-based index of the element to update.
|
|
5348
|
+
* @param {Number|Object} value The new value. Pass a number for float
|
|
5349
|
+
* buffers, or a plain object matching the original struct layout for
|
|
5350
|
+
* struct buffers.
|
|
5351
|
+
*/
|
|
5352
|
+
set(index, value) {
|
|
5353
|
+
const device = this._renderer.device;
|
|
5354
|
+
|
|
5355
|
+
if (this._schema !== null) {
|
|
5356
|
+
// buffer was created with an array of structs
|
|
5357
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
5358
|
+
throw new Error(
|
|
5359
|
+
'set() expects a plain object matching the original struct format for this buffer'
|
|
5360
|
+
);
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5363
|
+
const { stride } = this._schema;
|
|
5364
|
+
const byteOffset = index * stride;
|
|
5365
|
+
|
|
5366
|
+
if (byteOffset + stride > this.size) {
|
|
5367
|
+
throw new Error(
|
|
5368
|
+
`set() index ${index} is out of bounds for this buffer ` +
|
|
5369
|
+
`(buffer holds ${Math.floor(this.size / stride)} elements)`
|
|
5370
|
+
);
|
|
5371
|
+
}
|
|
5372
|
+
|
|
5373
|
+
// pack just this one element using the same logic as update()
|
|
5374
|
+
const packed = this._renderer._packStructArray([value], this._schema);
|
|
5375
|
+
// use packed.buffer (ArrayBuffer) so the size arg is always in bytes
|
|
5376
|
+
device.queue.writeBuffer(this.buffer, byteOffset, packed.buffer, 0, stride);
|
|
5377
|
+
} else {
|
|
5378
|
+
// buffer was created with a float array
|
|
5379
|
+
if (typeof value !== 'number') {
|
|
5380
|
+
throw new Error(
|
|
5381
|
+
'set() expects a number for this float buffer'
|
|
5382
|
+
);
|
|
5383
|
+
}
|
|
5384
|
+
|
|
5385
|
+
const byteOffset = index * 4;
|
|
5386
|
+
|
|
5387
|
+
if (byteOffset + 4 > this.size) {
|
|
5388
|
+
throw new Error(
|
|
5389
|
+
`set() index ${index} is out of bounds for this buffer ` +
|
|
5390
|
+
`(buffer holds ${Math.floor(this.size / 4)} floats)`
|
|
5391
|
+
);
|
|
5392
|
+
}
|
|
5393
|
+
|
|
5394
|
+
device.queue.writeBuffer(this.buffer, byteOffset, new Float32Array([value]));
|
|
5395
|
+
}
|
|
5396
|
+
}
|
|
5397
|
+
}
|
|
5398
|
+
|
|
5399
|
+
/**
|
|
5400
|
+
* A block of data that shaders can read from, and compute shaders can also
|
|
5401
|
+
* write to. This is only available in WebGPU mode.
|
|
5402
|
+
*
|
|
5403
|
+
* Note: <a href="#/p5/createStorage">`createStorage()`</a> is the recommended
|
|
5404
|
+
* way to create an instance of this class.
|
|
5405
|
+
*
|
|
5406
|
+
* @class p5.StorageBuffer
|
|
5407
|
+
* @beta
|
|
5408
|
+
* @webgpu
|
|
5409
|
+
* @webgpuOnly
|
|
5410
|
+
*/
|
|
5411
|
+
p5.StorageBuffer = StorageBuffer;
|
|
5412
|
+
|
|
4594
5413
|
class RendererWebGPU extends Renderer3D {
|
|
4595
5414
|
constructor(pInst, w, h, isMainCanvas, elt) {
|
|
4596
5415
|
super(pInst, w, h, isMainCanvas, elt);
|
|
@@ -4642,6 +5461,9 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
4642
5461
|
// Retired buffers to destroy at end of frame
|
|
4643
5462
|
this._retiredBuffers = [];
|
|
4644
5463
|
|
|
5464
|
+
// Storage buffers for compute shaders
|
|
5465
|
+
this._storageBuffers = new Set();
|
|
5466
|
+
|
|
4645
5467
|
// 2D canvas for pixel reading fallback
|
|
4646
5468
|
this._pixelReadCanvas = null;
|
|
4647
5469
|
this._pixelReadCtx = null;
|
|
@@ -4718,7 +5540,7 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
4718
5540
|
}
|
|
4719
5541
|
if (this._pInst._webgpuAttributes[key] !== value) {
|
|
4720
5542
|
//changing value of previously altered attribute
|
|
4721
|
-
this._webgpuAttributes[key] = value;
|
|
5543
|
+
this._pInst._webgpuAttributes[key] = value;
|
|
4722
5544
|
unchanged = false;
|
|
4723
5545
|
}
|
|
4724
5546
|
//setting all attributes with some change
|
|
@@ -4852,9 +5674,21 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
4852
5674
|
const _b = args[2] || 0;
|
|
4853
5675
|
const _a = args[3] || 0;
|
|
4854
5676
|
|
|
4855
|
-
// If PENDING and no custom framebuffer, clear means stay UNPROMOTED
|
|
4856
|
-
if
|
|
4857
|
-
|
|
5677
|
+
// If PENDING and no custom framebuffer, clear means stay UNPROMOTED.
|
|
5678
|
+
// However, if we are still in setup (frameCount == 0), we must promote
|
|
5679
|
+
// so that mainFramebuffer gets the cleared content. This ensures that if
|
|
5680
|
+
// draw() later promotes without a copy, it starts from the correct state
|
|
5681
|
+
// rather than a stale mainFramebuffer.
|
|
5682
|
+
// Note: a mid-draw-loop transition from UNPROMOTED back to PROMOTED
|
|
5683
|
+
// (i.e. calling background() some frames but not others) will still
|
|
5684
|
+
// lose intermediate UNPROMOTED frame content.
|
|
5685
|
+
if (this._frameState !== FRAME_STATE.PROMOTED && !this.activeFramebuffer()) {
|
|
5686
|
+
if (this._pInst.frameCount > 0) {
|
|
5687
|
+
this._frameState = FRAME_STATE.UNPROMOTED;
|
|
5688
|
+
} else {
|
|
5689
|
+
this._promoteToFramebufferWithoutCopy();
|
|
5690
|
+
// clear() then targets mainFramebuffer via activeFramebuffer()
|
|
5691
|
+
}
|
|
4858
5692
|
}
|
|
4859
5693
|
|
|
4860
5694
|
this._finishActiveRenderPass();
|
|
@@ -5057,7 +5891,8 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
5057
5891
|
return 4; // Cap at 4 for broader compatibility
|
|
5058
5892
|
}
|
|
5059
5893
|
|
|
5060
|
-
_shaderOptions({ mode }) {
|
|
5894
|
+
_shaderOptions({ mode, compute, workgroupSize }) {
|
|
5895
|
+
if (compute) return { compute: true, workgroupSize };
|
|
5061
5896
|
const activeFramebuffer = this.activeFramebuffer();
|
|
5062
5897
|
const format = activeFramebuffer ?
|
|
5063
5898
|
this._getWebGPUColorFormat(activeFramebuffer) :
|
|
@@ -5068,9 +5903,9 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
5068
5903
|
1; // No MSAA needed when blitting already-antialiased textures to canvas
|
|
5069
5904
|
const sampleCount = this._getValidSampleCount(requestedSampleCount);
|
|
5070
5905
|
|
|
5071
|
-
const depthFormat = activeFramebuffer
|
|
5072
|
-
this._getWebGPUDepthFormat(activeFramebuffer) :
|
|
5073
|
-
this.depthFormat;
|
|
5906
|
+
const depthFormat = activeFramebuffer
|
|
5907
|
+
? (activeFramebuffer.useDepth ? this._getWebGPUDepthFormat(activeFramebuffer) : undefined)
|
|
5908
|
+
: this.depthFormat;
|
|
5074
5909
|
|
|
5075
5910
|
const drawTarget = this.drawTarget();
|
|
5076
5911
|
const clipping = this._clipping;
|
|
@@ -5098,6 +5933,31 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
5098
5933
|
_initShader(shader) {
|
|
5099
5934
|
const device = this.device;
|
|
5100
5935
|
|
|
5936
|
+
if (shader.shaderType === 'compute') {
|
|
5937
|
+
// Compute shader initialization
|
|
5938
|
+
shader.computeModule = device.createShaderModule({ code: shader.computeSrc() });
|
|
5939
|
+
shader._computePipelineCache = null;
|
|
5940
|
+
shader._workgroupSize = null;
|
|
5941
|
+
|
|
5942
|
+
// Create compute pipeline (deferred until first compute() call)
|
|
5943
|
+
shader.getPipeline = ({ workgroupSize }) => {
|
|
5944
|
+
if (!shader._computePipelineCache) {
|
|
5945
|
+
shader._computePipelineCache = device.createComputePipeline({
|
|
5946
|
+
layout: shader._pipelineLayout,
|
|
5947
|
+
compute: {
|
|
5948
|
+
module: shader.computeModule,
|
|
5949
|
+
entryPoint: 'main'
|
|
5950
|
+
}
|
|
5951
|
+
});
|
|
5952
|
+
shader._workgroupSize = workgroupSize;
|
|
5953
|
+
}
|
|
5954
|
+
return shader._computePipelineCache;
|
|
5955
|
+
};
|
|
5956
|
+
|
|
5957
|
+
return;
|
|
5958
|
+
}
|
|
5959
|
+
|
|
5960
|
+
// Render shader initialization
|
|
5101
5961
|
shader.vertModule = device.createShaderModule({ code: shader.vertSrc() });
|
|
5102
5962
|
shader.fragModule = device.createShaderModule({ code: shader.fragSrc() });
|
|
5103
5963
|
|
|
@@ -5122,25 +5982,27 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
5122
5982
|
},
|
|
5123
5983
|
primitive: { topology },
|
|
5124
5984
|
multisample: { count: sampleCount },
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5985
|
+
...(depthFormat ? {
|
|
5986
|
+
depthStencil: {
|
|
5987
|
+
format: depthFormat,
|
|
5988
|
+
depthWriteEnabled: !clipping,
|
|
5989
|
+
depthCompare: 'less-equal',
|
|
5990
|
+
stencilFront: {
|
|
5991
|
+
compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
|
|
5992
|
+
failOp: 'keep',
|
|
5993
|
+
depthFailOp: 'keep',
|
|
5994
|
+
passOp: clipping ? 'replace' : 'keep',
|
|
5995
|
+
},
|
|
5996
|
+
stencilBack: {
|
|
5997
|
+
compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
|
|
5998
|
+
failOp: 'keep',
|
|
5999
|
+
depthFailOp: 'keep',
|
|
6000
|
+
passOp: clipping ? 'replace' : 'keep',
|
|
6001
|
+
},
|
|
6002
|
+
stencilReadMask: 0xFF,
|
|
6003
|
+
stencilWriteMask: clipping ? 0xFF : 0x00,
|
|
5134
6004
|
},
|
|
5135
|
-
|
|
5136
|
-
compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
|
|
5137
|
-
failOp: 'keep',
|
|
5138
|
-
depthFailOp: 'keep',
|
|
5139
|
-
passOp: clipping ? 'replace' : 'keep',
|
|
5140
|
-
},
|
|
5141
|
-
stencilReadMask: 0xFF,
|
|
5142
|
-
stencilWriteMask: clipping ? 0xFF : 0x00,
|
|
5143
|
-
},
|
|
6005
|
+
} : {}),
|
|
5144
6006
|
});
|
|
5145
6007
|
shader._pipelineCache.set(key, pipeline);
|
|
5146
6008
|
}
|
|
@@ -5196,7 +6058,9 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
5196
6058
|
entries.push({
|
|
5197
6059
|
bufferGroup,
|
|
5198
6060
|
binding: bufferGroup.binding,
|
|
5199
|
-
visibility:
|
|
6061
|
+
visibility: shader.shaderType === 'compute'
|
|
6062
|
+
? GPUShaderStage.COMPUTE
|
|
6063
|
+
: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
5200
6064
|
buffer: { type: 'uniform', hasDynamicOffset: bufferGroup.dynamic },
|
|
5201
6065
|
});
|
|
5202
6066
|
structEntries.set(bufferGroup.group, entries);
|
|
@@ -5230,6 +6094,24 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
5230
6094
|
groupEntries.set(group, entries);
|
|
5231
6095
|
}
|
|
5232
6096
|
|
|
6097
|
+
// Add storage buffer bindings
|
|
6098
|
+
for (const storage of shader._storageBuffers || []) {
|
|
6099
|
+
const group = storage.group;
|
|
6100
|
+
const entries = groupEntries.get(group) || [];
|
|
6101
|
+
|
|
6102
|
+
entries.push({
|
|
6103
|
+
binding: storage.binding,
|
|
6104
|
+
visibility: storage.visibility,
|
|
6105
|
+
buffer: {
|
|
6106
|
+
type: storage.accessMode === 'read' ? 'read-only-storage' : 'storage'
|
|
6107
|
+
},
|
|
6108
|
+
storage: storage,
|
|
6109
|
+
});
|
|
6110
|
+
|
|
6111
|
+
entries.sort((a, b) => a.binding - b.binding);
|
|
6112
|
+
groupEntries.set(group, entries);
|
|
6113
|
+
}
|
|
6114
|
+
|
|
5233
6115
|
// Create layouts and bind groups
|
|
5234
6116
|
const groupEntriesArr = [];
|
|
5235
6117
|
for (const [group, entries] of groupEntries) {
|
|
@@ -5248,6 +6130,7 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
5248
6130
|
shader._pipelineLayout = this.device.createPipelineLayout({
|
|
5249
6131
|
bindGroupLayouts: shader._bindGroupLayouts,
|
|
5250
6132
|
});
|
|
6133
|
+
shader._compiled = true;
|
|
5251
6134
|
}
|
|
5252
6135
|
|
|
5253
6136
|
_getBlendState(mode) {
|
|
@@ -5494,8 +6377,11 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
5494
6377
|
|
|
5495
6378
|
_resetBuffersBeforeDraw() {
|
|
5496
6379
|
this._finishActiveRenderPass();
|
|
6380
|
+
|
|
5497
6381
|
// Set state to PENDING - we'll decide on first draw
|
|
5498
|
-
this.
|
|
6382
|
+
if (this._pInst.frameCount > 0) {
|
|
6383
|
+
this._frameState = FRAME_STATE.PENDING;
|
|
6384
|
+
}
|
|
5499
6385
|
|
|
5500
6386
|
// Clear depth buffer but DON'T start any render pass yet
|
|
5501
6387
|
const activeFramebuffer = this.activeFramebuffer();
|
|
@@ -5606,6 +6492,8 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
5606
6492
|
// once we're drawing to the framebuffer, because normally
|
|
5607
6493
|
// those are reset.
|
|
5608
6494
|
const savedModelMatrix = this.states.uModelMatrix.copy();
|
|
6495
|
+
this.states.uModelMatrix.set(this.states.uModelMatrix.copy());
|
|
6496
|
+
this.states.uModelMatrix.reset();
|
|
5609
6497
|
this.mainFramebuffer.defaultCamera.set(this.states.curCamera);
|
|
5610
6498
|
|
|
5611
6499
|
this.mainFramebuffer.begin();
|
|
@@ -5613,7 +6501,12 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
5613
6501
|
this.states.uModelMatrix.set(savedModelMatrix);
|
|
5614
6502
|
}
|
|
5615
6503
|
|
|
5616
|
-
_promoteToFramebufferWithoutCopy() {
|
|
6504
|
+
_promoteToFramebufferWithoutCopy() {
|
|
6505
|
+
// Already promoted this frame
|
|
6506
|
+
if (this._frameState === FRAME_STATE.PROMOTED) {
|
|
6507
|
+
return;
|
|
6508
|
+
}
|
|
6509
|
+
|
|
5617
6510
|
// Ensure mainFramebuffer matches canvas size
|
|
5618
6511
|
if (this.mainFramebuffer.width !== this.width ||
|
|
5619
6512
|
this.mainFramebuffer.height !== this.height) {
|
|
@@ -5628,6 +6521,8 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
5628
6521
|
|
|
5629
6522
|
// Preserve transformation state
|
|
5630
6523
|
const savedModelMatrix = this.states.uModelMatrix.copy();
|
|
6524
|
+
this.states.uModelMatrix.set(this.states.uModelMatrix.copy());
|
|
6525
|
+
this.states.uModelMatrix.reset();
|
|
5631
6526
|
this.mainFramebuffer.defaultCamera.set(this.states.curCamera);
|
|
5632
6527
|
|
|
5633
6528
|
// Begin rendering to mainFramebuffer
|
|
@@ -5941,7 +6836,6 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
5941
6836
|
}
|
|
5942
6837
|
this.flushDraw();
|
|
5943
6838
|
|
|
5944
|
-
// this._pInst.background('red');
|
|
5945
6839
|
this._pInst.push();
|
|
5946
6840
|
this.states.setValue('enableLighting', false);
|
|
5947
6841
|
this.states.setValue('activeImageLight', null);
|
|
@@ -6006,25 +6900,9 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
6006
6900
|
|
|
6007
6901
|
this._beginActiveRenderPass();
|
|
6008
6902
|
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
6903
|
|
|
6017
|
-
|
|
6018
|
-
|
|
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
|
-
}
|
|
6904
|
+
const currentShader = this._curShader;
|
|
6905
|
+
this.setupShaderBindGroups(currentShader, passEncoder, { mode, buffers });
|
|
6028
6906
|
// Bind vertex buffers
|
|
6029
6907
|
for (const buffer of currentShader._vertexBuffers || this._getVertexBuffers(currentShader)) {
|
|
6030
6908
|
const location = currentShader.attributes[buffer.attr].location;
|
|
@@ -6032,6 +6910,58 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
6032
6910
|
passEncoder.setVertexBuffer(location, gpuBuffer, 0);
|
|
6033
6911
|
}
|
|
6034
6912
|
|
|
6913
|
+
if (currentShader.shaderType === "fill") {
|
|
6914
|
+
// Bind index buffer and issue draw
|
|
6915
|
+
if (buffers.indexBuffer) {
|
|
6916
|
+
const indexFormat = buffers.indexFormat || "uint16";
|
|
6917
|
+
passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
|
|
6918
|
+
passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
|
|
6919
|
+
} else {
|
|
6920
|
+
passEncoder.draw(geometry.vertices.length, count, 0, 0);
|
|
6921
|
+
}
|
|
6922
|
+
} else if (currentShader.shaderType === "text") {
|
|
6923
|
+
if (!buffers.indexBuffer) {
|
|
6924
|
+
throw new Error("Text geometry must have an index buffer");
|
|
6925
|
+
}
|
|
6926
|
+
const indexFormat = buffers.indexFormat || "uint16";
|
|
6927
|
+
passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
|
|
6928
|
+
passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
|
|
6929
|
+
}
|
|
6930
|
+
|
|
6931
|
+
if (buffers.lineVerticesBuffer && currentShader.shaderType === "stroke") {
|
|
6932
|
+
passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0);
|
|
6933
|
+
}
|
|
6934
|
+
|
|
6935
|
+
// Mark that we have pending draws that need submission
|
|
6936
|
+
this._hasPendingDraws = true;
|
|
6937
|
+
}
|
|
6938
|
+
|
|
6939
|
+
setupShaderBindGroups(currentShader, passEncoder, shaderOptionsParams) {
|
|
6940
|
+
const shaderOptions = this._shaderOptions(shaderOptionsParams);
|
|
6941
|
+
if (
|
|
6942
|
+
shaderOptions.compute ||
|
|
6943
|
+
this.activeShader !== currentShader ||
|
|
6944
|
+
this._shaderOptionsDifferent(shaderOptions)
|
|
6945
|
+
) {
|
|
6946
|
+
passEncoder.setPipeline(currentShader.getPipeline(shaderOptions));
|
|
6947
|
+
}
|
|
6948
|
+
if (!shaderOptions.compute) {
|
|
6949
|
+
this.activeShader = currentShader;
|
|
6950
|
+
this.activeShaderOptions = shaderOptions;
|
|
6951
|
+
|
|
6952
|
+
// Set stencil reference value for clipping
|
|
6953
|
+
const drawTarget = this.drawTarget();
|
|
6954
|
+
if (drawTarget._isClipApplied && !this._clipping) {
|
|
6955
|
+
// When using the clip mask, test against reference value 0 (background)
|
|
6956
|
+
// WebGL uses NOTEQUAL with ref 0, so fragments pass where stencil != 0
|
|
6957
|
+
// In WebGPU with 'not-equal', we need ref 0 to pass where stencil != 0
|
|
6958
|
+
passEncoder.setStencilReference(0);
|
|
6959
|
+
} else if (this._clipping) {
|
|
6960
|
+
// When writing to the clip mask, write reference value 1
|
|
6961
|
+
passEncoder.setStencilReference(1);
|
|
6962
|
+
}
|
|
6963
|
+
}
|
|
6964
|
+
|
|
6035
6965
|
for (const bufferGroup of currentShader._uniformBufferGroups) {
|
|
6036
6966
|
if (bufferGroup.dynamic) {
|
|
6037
6967
|
// Bind uniforms into a part of a big dynamic memory block because
|
|
@@ -6084,6 +7014,13 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
6084
7014
|
currentShader.buffersDirty.delete(key);
|
|
6085
7015
|
}
|
|
6086
7016
|
}
|
|
7017
|
+
for (const storage of currentShader._storageBuffers || []) {
|
|
7018
|
+
const key = storage.group * 1000 + storage.binding;
|
|
7019
|
+
if (currentShader.buffersDirty.has(key)) {
|
|
7020
|
+
currentShader._cachedBindGroup[storage.group] = undefined;
|
|
7021
|
+
currentShader.buffersDirty.delete(key);
|
|
7022
|
+
}
|
|
7023
|
+
}
|
|
6087
7024
|
|
|
6088
7025
|
// Bind sampler/texture uniforms and uniform buffers
|
|
6089
7026
|
for (const iter of currentShader._groupEntries) {
|
|
@@ -6113,6 +7050,19 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
6113
7050
|
: { buffer: uniformBufferInfo.buffer },
|
|
6114
7051
|
});
|
|
6115
7052
|
}
|
|
7053
|
+
} else if (entry.storage && !bindGroup) {
|
|
7054
|
+
// Storage buffer binding
|
|
7055
|
+
const uniform = currentShader.uniforms[entry.storage.name];
|
|
7056
|
+
if (!uniform || !uniform._cachedData || !uniform._cachedData._isStorageBuffer) {
|
|
7057
|
+
throw new Error(
|
|
7058
|
+
`Storage buffer "${entry.storage.name}" not set. ` +
|
|
7059
|
+
`Use shader.setUniform("${entry.storage.name}", storageBuffer)`
|
|
7060
|
+
);
|
|
7061
|
+
}
|
|
7062
|
+
bgEntries.push({
|
|
7063
|
+
binding: entry.binding,
|
|
7064
|
+
resource: { buffer: uniform._cachedData.buffer },
|
|
7065
|
+
});
|
|
6116
7066
|
} else if (!bindGroup) {
|
|
6117
7067
|
bgEntries.push({
|
|
6118
7068
|
binding: entry.binding,
|
|
@@ -6146,84 +7096,71 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
6146
7096
|
);
|
|
6147
7097
|
}
|
|
6148
7098
|
}
|
|
7099
|
+
return passEncoder;
|
|
7100
|
+
}
|
|
6149
7101
|
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
7102
|
+
//////////////////////////////////////////////
|
|
7103
|
+
// SHADER
|
|
7104
|
+
//////////////////////////////////////////////
|
|
7105
|
+
|
|
7106
|
+
// Writes a single field's value into a Float32Array+DataView at (baseOffset + field.offset).
|
|
7107
|
+
//
|
|
7108
|
+
// Field interface (shared by uniform fields from _parseStruct and struct storage schema fields):
|
|
7109
|
+
// baseType: string - 'f32', 'i32', 'u32', etc.
|
|
7110
|
+
// size: number - byte size of the field
|
|
7111
|
+
// offset: number - byte offset of the field within its struct
|
|
7112
|
+
// packInPlace: bool - true for mat3, written with manual column padding
|
|
7113
|
+
//
|
|
7114
|
+
// value: number or number[] - the data to write
|
|
7115
|
+
_packField(field, value, floatView, dataView, baseOffset) {
|
|
7116
|
+
if (value === undefined) return;
|
|
7117
|
+
|
|
7118
|
+
// Duck typing instead of instanceof to avoid importing a separate
|
|
7119
|
+
// copy of the Color/Vector classes
|
|
7120
|
+
if (value?.isVector) {
|
|
7121
|
+
value = value.values.length !== value.dimensions ? value.values.slice(0, value.dimensions) : value.values;
|
|
7122
|
+
} else if (value?.isColor) {
|
|
7123
|
+
value = value._getRGBA([1, 1, 1, 1]);
|
|
7124
|
+
}
|
|
7125
|
+
const byteOffset = baseOffset + field.offset;
|
|
7126
|
+
if (field.baseType === 'u32') {
|
|
7127
|
+
if (field.size === 4) {
|
|
7128
|
+
dataView.setUint32(byteOffset, value, true);
|
|
6156
7129
|
} else {
|
|
6157
|
-
|
|
7130
|
+
for (let i = 0; i < value.length; i++) {
|
|
7131
|
+
dataView.setUint32(byteOffset + i * 4, value[i], true);
|
|
7132
|
+
}
|
|
6158
7133
|
}
|
|
6159
|
-
} else if (
|
|
6160
|
-
if (
|
|
6161
|
-
|
|
7134
|
+
} else if (field.baseType === 'i32') {
|
|
7135
|
+
if (field.size === 4) {
|
|
7136
|
+
dataView.setInt32(byteOffset, value, true);
|
|
7137
|
+
} else {
|
|
7138
|
+
for (let i = 0; i < value.length; i++) {
|
|
7139
|
+
dataView.setInt32(byteOffset + i * 4, value[i], true);
|
|
7140
|
+
}
|
|
6162
7141
|
}
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
7142
|
+
} else if (field.packInPlace) {
|
|
7143
|
+
// In-place packing for mat3: write directly to buffer with padding
|
|
7144
|
+
const base = byteOffset / 4;
|
|
7145
|
+
floatView[base + 0] = value[0]; floatView[base + 1] = value[1]; floatView[base + 2] = value[2];
|
|
7146
|
+
floatView[base + 4] = value[3]; floatView[base + 5] = value[4]; floatView[base + 6] = value[5];
|
|
7147
|
+
floatView[base + 8] = value[6]; floatView[base + 9] = value[7]; floatView[base + 10] = value[8];
|
|
7148
|
+
} else if (field.size === 4) {
|
|
7149
|
+
floatView.set([value], byteOffset / 4);
|
|
7150
|
+
} else {
|
|
7151
|
+
floatView.set(value, byteOffset / 4);
|
|
6170
7152
|
}
|
|
6171
|
-
|
|
6172
|
-
// Mark that we have pending draws that need submission
|
|
6173
|
-
this._hasPendingDraws = true;
|
|
6174
7153
|
}
|
|
6175
7154
|
|
|
6176
|
-
//////////////////////////////////////////////
|
|
6177
|
-
// SHADER
|
|
6178
|
-
//////////////////////////////////////////////
|
|
6179
|
-
|
|
6180
7155
|
_packUniformGroup(shader, groupUniforms, bufferInfo) {
|
|
6181
7156
|
// Pack a single group's uniforms into a buffer
|
|
6182
7157
|
const data = bufferInfo.data;
|
|
6183
7158
|
const dataView = bufferInfo.dataView;
|
|
6184
|
-
|
|
6185
7159
|
const offset = bufferInfo.offset || 0;
|
|
6186
7160
|
for (const uniform of groupUniforms) {
|
|
6187
7161
|
const fullUniform = shader.uniforms[uniform.name];
|
|
6188
7162
|
if (!fullUniform || fullUniform.isSampler) continue;
|
|
6189
|
-
|
|
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
|
-
}
|
|
7163
|
+
this._packField(fullUniform, fullUniform._mappedData, data, dataView, offset);
|
|
6227
7164
|
}
|
|
6228
7165
|
}
|
|
6229
7166
|
|
|
@@ -6370,10 +7307,11 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
6370
7307
|
const uniformVarRegex = /@group\((\d+)\)\s+@binding\((\d+)\)\s+var<uniform>\s+(\w+)\s*:\s*(\w+);/g;
|
|
6371
7308
|
|
|
6372
7309
|
let match;
|
|
6373
|
-
|
|
7310
|
+
const src = shader.shaderType === 'compute' ? shader.computeSrc() : shader.vertSrc();
|
|
7311
|
+
while ((match = uniformVarRegex.exec(src)) !== null) {
|
|
6374
7312
|
const [_, groupNum, binding, varName, structType] = match;
|
|
6375
7313
|
const bindingIndex = parseInt(binding);
|
|
6376
|
-
const uniforms = this._parseStruct(
|
|
7314
|
+
const uniforms = this._parseStruct(src, structType);
|
|
6377
7315
|
|
|
6378
7316
|
uniformGroups.push({
|
|
6379
7317
|
group: parseInt(groupNum),
|
|
@@ -6384,7 +7322,7 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
6384
7322
|
});
|
|
6385
7323
|
}
|
|
6386
7324
|
|
|
6387
|
-
if (uniformGroups.length === 0) {
|
|
7325
|
+
if (uniformGroups.length === 0 && shader.shaderType !== 'compute') {
|
|
6388
7326
|
throw new Error('Expected at least one uniform struct bound to @group(0)');
|
|
6389
7327
|
}
|
|
6390
7328
|
|
|
@@ -6411,6 +7349,10 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
6411
7349
|
// TODO: support other texture types
|
|
6412
7350
|
const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d<f32>|sampler);/g;
|
|
6413
7351
|
|
|
7352
|
+
// Extract storage buffers
|
|
7353
|
+
const storageBuffers = {};
|
|
7354
|
+
const storageRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var<storage,\s*(read|read_write)>\s+(\w+)\s*:\s*array<\w+>/g;
|
|
7355
|
+
|
|
6414
7356
|
// Track which bindings are taken by the struct properties we've parsed
|
|
6415
7357
|
// (the rest should be textures/samplers)
|
|
6416
7358
|
const structUniformBindings = {};
|
|
@@ -6420,8 +7362,11 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
6420
7362
|
|
|
6421
7363
|
for (const [src, visibility] of [
|
|
6422
7364
|
[shader.vertSrc(), GPUShaderStage.VERTEX],
|
|
6423
|
-
[shader.fragSrc(), GPUShaderStage.FRAGMENT]
|
|
7365
|
+
[shader.fragSrc(), GPUShaderStage.FRAGMENT],
|
|
7366
|
+
[shader.computeSrc ? shader.computeSrc() : null, GPUShaderStage.COMPUTE]
|
|
6424
7367
|
]) {
|
|
7368
|
+
if (!src) continue; // Skip if shader stage doesn't exist
|
|
7369
|
+
|
|
6425
7370
|
let match;
|
|
6426
7371
|
while ((match = samplerRegex.exec(src)) !== null) {
|
|
6427
7372
|
const [_, group, binding, name, type] = match;
|
|
@@ -6456,21 +7401,51 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
6456
7401
|
samplerNode.textureSource = sampler;
|
|
6457
7402
|
}
|
|
6458
7403
|
}
|
|
7404
|
+
|
|
7405
|
+
// Parse storage buffers
|
|
7406
|
+
while ((match = storageRegex.exec(src)) !== null) {
|
|
7407
|
+
const [_, group, binding, accessMode, name] = match;
|
|
7408
|
+
const groupIndex = parseInt(group);
|
|
7409
|
+
const bindingIndex = parseInt(binding);
|
|
7410
|
+
|
|
7411
|
+
const key = `${groupIndex},${bindingIndex}`;
|
|
7412
|
+
const existing = storageBuffers[key];
|
|
7413
|
+
// If any stage uses read_write, the bind group layout must use read_write
|
|
7414
|
+
const finalAccessMode = (existing?.accessMode === 'read_write' || accessMode === 'read_write')
|
|
7415
|
+
? 'read_write'
|
|
7416
|
+
: accessMode;
|
|
7417
|
+
|
|
7418
|
+
storageBuffers[key] = {
|
|
7419
|
+
visibility: (existing?.visibility || 0) | visibility,
|
|
7420
|
+
group: groupIndex,
|
|
7421
|
+
binding: bindingIndex,
|
|
7422
|
+
name,
|
|
7423
|
+
accessMode: finalAccessMode, // 'read' or 'read_write'
|
|
7424
|
+
isStorage: true,
|
|
7425
|
+
type: 'storage'
|
|
7426
|
+
};
|
|
7427
|
+
}
|
|
6459
7428
|
}
|
|
6460
|
-
|
|
7429
|
+
|
|
7430
|
+
// Store storage buffers on shader for later use
|
|
7431
|
+
shader._storageBuffers = Object.values(storageBuffers);
|
|
7432
|
+
|
|
7433
|
+
return [...Object.values(allUniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers), ...Object.values(storageBuffers)];
|
|
6461
7434
|
}
|
|
6462
7435
|
|
|
6463
|
-
getNextBindingIndex({ vert, frag }, group = 0) {
|
|
7436
|
+
getNextBindingIndex({ vert, frag, compute }, group = 0) {
|
|
6464
7437
|
// Get the highest binding index in the specified group and return the next available
|
|
6465
|
-
const
|
|
7438
|
+
const bindingRegex = /@group\((\d+)\)\s*@binding\((\d+)\)/g;
|
|
6466
7439
|
let maxBindingIndex = -1;
|
|
6467
7440
|
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
])
|
|
7441
|
+
const sources = [];
|
|
7442
|
+
if (vert) sources.push([vert, GPUShaderStage.VERTEX]);
|
|
7443
|
+
if (frag) sources.push([frag, GPUShaderStage.FRAGMENT]);
|
|
7444
|
+
if (compute) sources.push([compute, GPUShaderStage.COMPUTE]);
|
|
7445
|
+
|
|
7446
|
+
for (const [src, visibility] of sources) {
|
|
6472
7447
|
let match;
|
|
6473
|
-
while ((match =
|
|
7448
|
+
while ((match = bindingRegex.exec(src)) !== null) {
|
|
6474
7449
|
const [_, groupIndex, bindingIndex] = match;
|
|
6475
7450
|
if (parseInt(groupIndex) === group) {
|
|
6476
7451
|
maxBindingIndex = Math.max(maxBindingIndex, parseInt(bindingIndex));
|
|
@@ -6485,7 +7460,7 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
6485
7460
|
if (uniform.isSampler) {
|
|
6486
7461
|
uniform.texture =
|
|
6487
7462
|
data instanceof Texture ? data : this.getTexture(data);
|
|
6488
|
-
} else {
|
|
7463
|
+
} else if (!data?._isStorageBuffer) {
|
|
6489
7464
|
uniform._mappedData = this._mapUniformData(uniform, uniform._cachedData);
|
|
6490
7465
|
}
|
|
6491
7466
|
shader.buffersDirty.add(uniform.group * 1000 + uniform.binding);
|
|
@@ -6592,7 +7567,7 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
6592
7567
|
rgb += components.emissive;
|
|
6593
7568
|
return vec4<f32>(rgb, components.opacity);
|
|
6594
7569
|
}`,
|
|
6595
|
-
"vec4f getFinalColor": "(color: vec4<f32>) { return color; }",
|
|
7570
|
+
"vec4f getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
|
|
6596
7571
|
"void afterFragment": "() {}",
|
|
6597
7572
|
},
|
|
6598
7573
|
}
|
|
@@ -6617,7 +7592,7 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
6617
7592
|
},
|
|
6618
7593
|
fragment: {
|
|
6619
7594
|
"void beforeFragment": "() {}",
|
|
6620
|
-
"vec4<f32> getFinalColor": "(color: vec4<f32>) { return color; }",
|
|
7595
|
+
"vec4<f32> getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
|
|
6621
7596
|
"void afterFragment": "() {}",
|
|
6622
7597
|
},
|
|
6623
7598
|
}
|
|
@@ -6643,7 +7618,7 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
6643
7618
|
fragment: {
|
|
6644
7619
|
"void beforeFragment": "() {}",
|
|
6645
7620
|
"Inputs getPixelInputs": "(inputs: Inputs) { return inputs; }",
|
|
6646
|
-
"vec4<f32> getFinalColor": "(color: vec4<f32>) { return color; }",
|
|
7621
|
+
"vec4<f32> getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
|
|
6647
7622
|
"bool shouldDiscard": "(outside: bool) { return outside; };",
|
|
6648
7623
|
"void afterFragment": "() {}",
|
|
6649
7624
|
},
|
|
@@ -6802,11 +7777,87 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
6802
7777
|
}
|
|
6803
7778
|
);
|
|
6804
7779
|
|
|
6805
|
-
let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment)\s*)?fn main[^{]+\{)/);
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
7780
|
+
let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment|compute)\s*(?:@workgroup_size\([^)]+\)\s*)?)?fn main[^{]+\{)/);
|
|
7781
|
+
|
|
7782
|
+
const getBuiltinParamName = (mainSrc, builtinName) => {
|
|
7783
|
+
const match = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`).exec(mainSrc);
|
|
7784
|
+
return match ? match[1] : null;
|
|
7785
|
+
};
|
|
7786
|
+
|
|
7787
|
+
const ensureBuiltinParam = (mainSrc, builtinName, fallbackName, typeName) => {
|
|
7788
|
+
const existingName = getBuiltinParamName(mainSrc, builtinName);
|
|
7789
|
+
if (existingName) {
|
|
7790
|
+
return { mainSrc, argName: existingName };
|
|
7791
|
+
}
|
|
7792
|
+
|
|
7793
|
+
const hasParams = /\(\s*\S/.test(mainSrc);
|
|
7794
|
+
const injectedMain = mainSrc.replace(
|
|
7795
|
+
/\)\s*(->|\{)/,
|
|
7796
|
+
`${hasParams ? ', ' : ''}@builtin(${builtinName}) ${fallbackName}: ${typeName}) $1`
|
|
7797
|
+
);
|
|
7798
|
+
|
|
7799
|
+
return { mainSrc: injectedMain, argName: fallbackName };
|
|
7800
|
+
};
|
|
7801
|
+
|
|
7802
|
+
const getMainStructParameter = (mainSrc) => {
|
|
7803
|
+
const match = /fn main\s*\(\s*(\w+)\s*:\s*(\w+)/.exec(mainSrc);
|
|
7804
|
+
if (!match) return null;
|
|
7805
|
+
return { inputName: match[1], structName: match[2] };
|
|
7806
|
+
};
|
|
7807
|
+
|
|
7808
|
+
const getStructBuiltinFieldName = (structName, builtinName) => {
|
|
7809
|
+
const structMatch = new RegExp(`struct\\s+${structName}\\s*\\{([^}]*)\\}`, 's').exec(preMain);
|
|
7810
|
+
if (!structMatch) return null;
|
|
7811
|
+
const fieldMatch = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`, 's').exec(structMatch[1]);
|
|
7812
|
+
return fieldMatch ? fieldMatch[1] : null;
|
|
7813
|
+
};
|
|
7814
|
+
|
|
7815
|
+
const appendHookParams = (params, additionalParams) => {
|
|
7816
|
+
if (additionalParams.length === 0) return params;
|
|
7817
|
+
const hasParams = !/^\(\s*\)$/.test(params);
|
|
7818
|
+
return `${params.slice(0, -1)}${hasParams ? ', ' : ''}${additionalParams.join(', ')})`;
|
|
7819
|
+
};
|
|
7820
|
+
|
|
7821
|
+
let hookExtraParams = [];
|
|
7822
|
+
let hookExtraArgs = [];
|
|
7823
|
+
|
|
7824
|
+
if (shaderType === 'vertex') {
|
|
7825
|
+
const ensuredInstance = ensureBuiltinParam(main, 'instance_index', 'instanceID', 'u32');
|
|
7826
|
+
main = ensuredInstance.mainSrc;
|
|
7827
|
+
|
|
7828
|
+
const ensuredVertex = ensureBuiltinParam(main, 'vertex_index', '_p5VertexId', 'u32');
|
|
7829
|
+
main = ensuredVertex.mainSrc;
|
|
7830
|
+
|
|
7831
|
+
hookExtraParams = ['instanceID: u32', '_p5VertexId: u32'];
|
|
7832
|
+
hookExtraArgs = [ensuredInstance.argName, ensuredVertex.argName];
|
|
7833
|
+
} else if (shaderType === 'fragment') {
|
|
7834
|
+
const directPositionArg = getBuiltinParamName(main, 'position');
|
|
7835
|
+
let fragmentPositionArg = directPositionArg;
|
|
7836
|
+
|
|
7837
|
+
if (!fragmentPositionArg) {
|
|
7838
|
+
const mainStructParam = getMainStructParameter(main);
|
|
7839
|
+
if (mainStructParam) {
|
|
7840
|
+
const positionField = getStructBuiltinFieldName(mainStructParam.structName, 'position');
|
|
7841
|
+
if (positionField) {
|
|
7842
|
+
fragmentPositionArg = `${mainStructParam.inputName}.${positionField}`;
|
|
7843
|
+
}
|
|
7844
|
+
}
|
|
6809
7845
|
}
|
|
7846
|
+
|
|
7847
|
+
if (!fragmentPositionArg) {
|
|
7848
|
+
const ensuredPosition = ensureBuiltinParam(main, 'position', '_p5FragPos', 'vec4<f32>');
|
|
7849
|
+
main = ensuredPosition.mainSrc;
|
|
7850
|
+
fragmentPositionArg = ensuredPosition.argName;
|
|
7851
|
+
}
|
|
7852
|
+
|
|
7853
|
+
hookExtraParams = ['_p5FragPos: vec4<f32>'];
|
|
7854
|
+
hookExtraArgs = [fragmentPositionArg];
|
|
7855
|
+
} else if (shaderType === 'compute') {
|
|
7856
|
+
const ensuredGlobalId = ensureBuiltinParam(main, 'global_invocation_id', '_p5GlobalId', 'vec3<u32>');
|
|
7857
|
+
main = ensuredGlobalId.mainSrc;
|
|
7858
|
+
|
|
7859
|
+
hookExtraParams = ['_p5GlobalId: vec3<u32>'];
|
|
7860
|
+
hookExtraArgs = [ensuredGlobalId.argName];
|
|
6810
7861
|
}
|
|
6811
7862
|
|
|
6812
7863
|
// Inject hook uniforms as a separate struct at a new binding
|
|
@@ -6826,6 +7877,7 @@ fn main(input: FragmentInput) -> @location(0) vec4<f32> {
|
|
|
6826
7877
|
const nextBinding = this.getNextBindingIndex({
|
|
6827
7878
|
vert: shaderType === 'vertex' ? preMain + (shader.hooks.vertex?.declarations ?? '') + shader.hooks.declarations : shader._vertSrc,
|
|
6828
7879
|
frag: shaderType === 'fragment' ? preMain + (shader.hooks.fragment?.declarations ?? '') + shader.hooks.declarations : shader._fragSrc,
|
|
7880
|
+
compute: shaderType === 'compute' ? preMain + (shader.hooks.compute?.declarations ?? '') + shader.hooks.declarations : shader._computeSrc,
|
|
6829
7881
|
}, 0);
|
|
6830
7882
|
|
|
6831
7883
|
// Create HookUniforms struct and binding
|
|
@@ -6836,8 +7888,14 @@ ${hookUniformFields}}
|
|
|
6836
7888
|
|
|
6837
7889
|
@group(0) @binding(${nextBinding}) var<uniform> hooks: HookUniforms;
|
|
6838
7890
|
`;
|
|
6839
|
-
// Insert before the first @group binding
|
|
6840
|
-
|
|
7891
|
+
// Insert before the first @group binding, or at the end if there are none
|
|
7892
|
+
const replaced = preMain.replace(/(@group\(0\)\s+@binding)/, `${hookUniformsDecl}\n$1`);
|
|
7893
|
+
if (replaced === preMain) {
|
|
7894
|
+
// No @group bindings found in base shader, append to preMain
|
|
7895
|
+
preMain = preMain + '\n' + hookUniformsDecl;
|
|
7896
|
+
} else {
|
|
7897
|
+
preMain = replaced;
|
|
7898
|
+
}
|
|
6841
7899
|
}
|
|
6842
7900
|
|
|
6843
7901
|
// Handle varying variables by injecting them into VertexOutput and FragmentInput structs
|
|
@@ -6903,10 +7961,9 @@ ${hookUniformFields}}
|
|
|
6903
7961
|
initStatements += ` ${varName} = INPUT_VAR.${varName};\n`;
|
|
6904
7962
|
}
|
|
6905
7963
|
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
const inputVarName = inputMatch[1];
|
|
7964
|
+
const mainStructParam = getMainStructParameter(main);
|
|
7965
|
+
if (mainStructParam) {
|
|
7966
|
+
const inputVarName = mainStructParam.inputName;
|
|
6910
7967
|
initStatements = initStatements.replace(/INPUT_VAR/g, inputVarName);
|
|
6911
7968
|
// Insert after the main function parameter but before any other code (anchored to start)
|
|
6912
7969
|
postMain = initStatements + postMain;
|
|
@@ -6914,12 +7971,56 @@ ${hookUniformFields}}
|
|
|
6914
7971
|
}
|
|
6915
7972
|
}
|
|
6916
7973
|
|
|
7974
|
+
// Handle instanceID varying for fragment access
|
|
7975
|
+
if (shader.hooks.instanceIDVarying) {
|
|
7976
|
+
const { name, declaration, source, interpolation } = shader.hooks.instanceIDVarying;
|
|
7977
|
+
const nextLocIndex = this._getNextAvailableLocation(preMain, shaderType);
|
|
7978
|
+
const interpAttr = interpolation ? ` @interpolate(${interpolation})` : '';
|
|
7979
|
+
const [varName, varType] = declaration.split(':').map(s => s.trim());
|
|
7980
|
+
const structMember = `@location(${nextLocIndex})${interpAttr} ${declaration},`;
|
|
7981
|
+
|
|
7982
|
+
if (shaderType === 'vertex') {
|
|
7983
|
+
// Inject into VertexOutput struct
|
|
7984
|
+
preMain = preMain.replace(
|
|
7985
|
+
/struct\s+VertexOutput\s+\{([^}]*)\}/,
|
|
7986
|
+
(match, body) => `struct VertexOutput {${body}\n${structMember}}`
|
|
7987
|
+
);
|
|
7988
|
+
// Add private global
|
|
7989
|
+
preMain += `var<private> ${declaration};\n`;
|
|
7990
|
+
// Assign from built-in instanceID at start of main()
|
|
7991
|
+
postMain = `\n ${varName} = ${source};\n` + postMain;
|
|
7992
|
+
// Copy to output struct before return
|
|
7993
|
+
const returnMatch = postMain.match(/return\s+(\w+)\s*;/);
|
|
7994
|
+
if (returnMatch) {
|
|
7995
|
+
const outputVarName = returnMatch[1];
|
|
7996
|
+
postMain = postMain.replace(
|
|
7997
|
+
/(return\s+\w+\s*;)/g,
|
|
7998
|
+
`${outputVarName}.${varName} = ${varName};\n $1`
|
|
7999
|
+
);
|
|
8000
|
+
}
|
|
8001
|
+
} else if (shaderType === 'fragment') {
|
|
8002
|
+
// Inject into FragmentInput struct
|
|
8003
|
+
preMain = preMain.replace(
|
|
8004
|
+
/struct\s+FragmentInput\s+\{([^}]*)\}/,
|
|
8005
|
+
(match, body) => `struct FragmentInput {${body}\n${structMember}}`
|
|
8006
|
+
);
|
|
8007
|
+
// Add private global
|
|
8008
|
+
preMain += `var<private> ${declaration};\n`;
|
|
8009
|
+
// Initialize from input struct at start of main()
|
|
8010
|
+
const mainStructParam = getMainStructParameter(main);
|
|
8011
|
+
if (mainStructParam) {
|
|
8012
|
+
const inputVarName = mainStructParam.inputName;
|
|
8013
|
+
postMain = `\n ${varName} = ${inputVarName}.${varName};\n` + postMain;
|
|
8014
|
+
}
|
|
8015
|
+
}
|
|
8016
|
+
}
|
|
8017
|
+
|
|
6917
8018
|
let hooks = '';
|
|
6918
8019
|
let defines = '';
|
|
6919
8020
|
if (shader.hooks.declarations) {
|
|
6920
8021
|
hooks += shader.hooks.declarations + '\n';
|
|
6921
8022
|
}
|
|
6922
|
-
if (shader.hooks[shaderType].declarations) {
|
|
8023
|
+
if (shader.hooks[shaderType] && shader.hooks[shaderType].declarations) {
|
|
6923
8024
|
hooks += shader.hooks[shaderType].declarations + '\n';
|
|
6924
8025
|
}
|
|
6925
8026
|
for (const hookDef in shader.hooks.helpers) {
|
|
@@ -6943,11 +8044,7 @@ ${hookUniformFields}}
|
|
|
6943
8044
|
|
|
6944
8045
|
let [_, params, body] = /^(\([^\)]*\))((?:.|\n)*)$/.exec(shader.hooks[shaderType][hookDef]);
|
|
6945
8046
|
|
|
6946
|
-
|
|
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
|
-
}
|
|
8047
|
+
params = appendHookParams(params, hookExtraParams);
|
|
6951
8048
|
|
|
6952
8049
|
if (hookType === 'void') {
|
|
6953
8050
|
hooks += `fn HOOK_${hookName}${params}${body}\n`;
|
|
@@ -6956,40 +8053,45 @@ ${hookUniformFields}}
|
|
|
6956
8053
|
}
|
|
6957
8054
|
}
|
|
6958
8055
|
|
|
6959
|
-
//
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
if (nesting === 0) {
|
|
6981
|
-
break;
|
|
6982
|
-
}
|
|
8056
|
+
// Pass stage-specific builtins from main to each hook call.
|
|
8057
|
+
// Collect ALL HOOK_ calls (including nested ones) then insert
|
|
8058
|
+
// extra args from right to left so position shifts don't
|
|
8059
|
+
// invalidate earlier insertion points.
|
|
8060
|
+
if (hookExtraArgs.length > 0) {
|
|
8061
|
+
const addHookArgs = (src) => {
|
|
8062
|
+
const insertions = [];
|
|
8063
|
+
let searchIdx = 0;
|
|
8064
|
+
let m;
|
|
8065
|
+
while ((m = /HOOK_\w+\(/.exec(src.slice(searchIdx))) !== null) {
|
|
8066
|
+
const openParen = searchIdx + m.index + m[0].length - 1;
|
|
8067
|
+
let pos = openParen + 1;
|
|
8068
|
+
let nesting = 1;
|
|
8069
|
+
let hasParams = false;
|
|
8070
|
+
while (pos < src.length && nesting > 0) {
|
|
8071
|
+
if (src[pos] === '(') nesting++;
|
|
8072
|
+
else if (src[pos] === ')') {
|
|
8073
|
+
nesting--;
|
|
8074
|
+
if (nesting === 0) break;
|
|
8075
|
+
} else if (/\S/.test(src[pos])) {
|
|
8076
|
+
hasParams = true;
|
|
6983
8077
|
}
|
|
6984
|
-
|
|
6985
|
-
result = result.slice(0, idx-1) + insertion + result.slice(idx-1);
|
|
6986
|
-
idx += insertion.length;
|
|
8078
|
+
pos++;
|
|
6987
8079
|
}
|
|
6988
|
-
|
|
8080
|
+
insertions.push({ pos, hasParams });
|
|
8081
|
+
searchIdx = openParen + 1;
|
|
8082
|
+
}
|
|
8083
|
+
|
|
8084
|
+
insertions.sort((a, b) => b.pos - a.pos);
|
|
8085
|
+
|
|
8086
|
+
let result = src;
|
|
8087
|
+
for (const { pos, hasParams } of insertions) {
|
|
8088
|
+
const insertion = (hasParams ? ', ' : '') + hookExtraArgs.join(', ');
|
|
8089
|
+
result = result.slice(0, pos) + insertion + result.slice(pos);
|
|
8090
|
+
}
|
|
6989
8091
|
return result;
|
|
6990
8092
|
};
|
|
6991
|
-
preMain =
|
|
6992
|
-
postMain =
|
|
8093
|
+
preMain = addHookArgs(preMain);
|
|
8094
|
+
postMain = addHookArgs(postMain);
|
|
6993
8095
|
}
|
|
6994
8096
|
|
|
6995
8097
|
return preMain + '\n' + defines + hooks + main + postMain;
|
|
@@ -7048,6 +8150,10 @@ ${hookUniformFields}}
|
|
|
7048
8150
|
body = shader.hooks.fragment[hookName];
|
|
7049
8151
|
fullSrc = shader._fragSrc;
|
|
7050
8152
|
}
|
|
8153
|
+
if (!body) {
|
|
8154
|
+
body = shader.hooks.compute[hookName];
|
|
8155
|
+
fullSrc = shader._computeSrc;
|
|
8156
|
+
}
|
|
7051
8157
|
if (!body) {
|
|
7052
8158
|
throw new Error(`Can't find hook ${hookName}!`);
|
|
7053
8159
|
}
|
|
@@ -7179,7 +8285,7 @@ ${hookUniformFields}}
|
|
|
7179
8285
|
}
|
|
7180
8286
|
|
|
7181
8287
|
defaultFramebufferAntialias() {
|
|
7182
|
-
return
|
|
8288
|
+
return this._pInst._webgpuAttributes?.antialias !== false;
|
|
7183
8289
|
}
|
|
7184
8290
|
|
|
7185
8291
|
supportsFramebufferAntialias() {
|
|
@@ -7372,6 +8478,267 @@ ${hookUniformFields}}
|
|
|
7372
8478
|
};
|
|
7373
8479
|
}
|
|
7374
8480
|
|
|
8481
|
+
// Maps a plain JS value to the WGSL type string that represents it in a struct.
|
|
8482
|
+
_jsValueToWgslType(value) {
|
|
8483
|
+
if (typeof value === 'number') return 'f32';
|
|
8484
|
+
// Duck typing instead of instanceof to avoid importing a separate
|
|
8485
|
+
// copy of the Color/Vector classes
|
|
8486
|
+
if (value?.isVector) {
|
|
8487
|
+
if (value.dimensions === 2) return 'vec2f';
|
|
8488
|
+
if (value.dimensions === 3) return 'vec3f';
|
|
8489
|
+
if (value.dimensions === 4) return 'vec4f';
|
|
8490
|
+
throw new Error(`Unsupported vector dimension ${value.dimensions} for struct storage field`);
|
|
8491
|
+
}
|
|
8492
|
+
if (value?.isColor) {
|
|
8493
|
+
return 'vec4f';
|
|
8494
|
+
}
|
|
8495
|
+
if (Array.isArray(value)) {
|
|
8496
|
+
if (value.length === 2) return 'vec2f';
|
|
8497
|
+
if (value.length === 3) return 'vec3f';
|
|
8498
|
+
if (value.length === 4) return 'vec4f';
|
|
8499
|
+
throw new Error(`Unsupported array length ${value.length} for struct storage field`);
|
|
8500
|
+
}
|
|
8501
|
+
throw new Error(`Unsupported value type ${typeof value} for struct storage field`);
|
|
8502
|
+
}
|
|
8503
|
+
|
|
8504
|
+
// Infers a struct schema from the first element of a struct array.
|
|
8505
|
+
//
|
|
8506
|
+
// Returns { fields, stride, structBody } where:
|
|
8507
|
+
// fields: field has the _packField interface (baseType, size, offset, packInPlace) plus:
|
|
8508
|
+
// name: string - JS property name
|
|
8509
|
+
// dim: number - float component count, used when creating StrandsNodes
|
|
8510
|
+
// structBody: everything inside the { ... } of a WGSL struct definition
|
|
8511
|
+
// stride: how many bytes are reserved for this struct in the buffer
|
|
8512
|
+
_inferStructSchema(firstElement) {
|
|
8513
|
+
const entries = Object.entries(firstElement);
|
|
8514
|
+
|
|
8515
|
+
if (!p5.disableFriendlyErrors) {
|
|
8516
|
+
for (const [name, value] of entries) {
|
|
8517
|
+
if (
|
|
8518
|
+
value !== null &&
|
|
8519
|
+
typeof value === 'object' &&
|
|
8520
|
+
!Array.isArray(value) &&
|
|
8521
|
+
// Duck typing instead of instanceof to avoid importing a separate
|
|
8522
|
+
// copy of the Color/Vector classes
|
|
8523
|
+
!value?.isVector &&
|
|
8524
|
+
!value?.isColor
|
|
8525
|
+
) {
|
|
8526
|
+
p5._friendlyError(
|
|
8527
|
+
`The "${name}" property in your storage data contains a nested object. ` +
|
|
8528
|
+
`Make sure you only use properties with numbers, arrays of numbers, or p5.Vector.`,
|
|
8529
|
+
'createStorage'
|
|
8530
|
+
);
|
|
8531
|
+
}
|
|
8532
|
+
}
|
|
8533
|
+
}
|
|
8534
|
+
|
|
8535
|
+
const fieldLines = entries.map(([name, value]) =>
|
|
8536
|
+
` ${name}: ${this._jsValueToWgslType(value)},`
|
|
8537
|
+
).join('\n');
|
|
8538
|
+
const structBody = `{\n${fieldLines}\n}`;
|
|
8539
|
+
const elements = this._parseStruct(`struct _Tmp ${structBody}`, '_Tmp');
|
|
8540
|
+
|
|
8541
|
+
let maxEnd = 0;
|
|
8542
|
+
let maxAlign = 1;
|
|
8543
|
+
const fields = entries.map(([name, value]) => {
|
|
8544
|
+
const el = elements[name];
|
|
8545
|
+
maxEnd = Math.max(maxEnd, el.offsetEnd);
|
|
8546
|
+
// Alignment for scalars/vectors: <=4 -> 4, <=8 -> 8, else 16
|
|
8547
|
+
const align = el.size <= 4 ? 4 : el.size <= 8 ? 8 : 16;
|
|
8548
|
+
maxAlign = Math.max(maxAlign, align);
|
|
8549
|
+
// Track original JS type for reconstruction during readback
|
|
8550
|
+
const kind = value?.isVector ? 'vector'
|
|
8551
|
+
: value?.isColor ? 'color'
|
|
8552
|
+
: undefined;
|
|
8553
|
+
return {
|
|
8554
|
+
name,
|
|
8555
|
+
baseType: el.baseType,
|
|
8556
|
+
size: el.size,
|
|
8557
|
+
offset: el.offset,
|
|
8558
|
+
packInPlace: el.packInPlace ?? false,
|
|
8559
|
+
dim: el.size / 4,
|
|
8560
|
+
kind,
|
|
8561
|
+
};
|
|
8562
|
+
});
|
|
8563
|
+
|
|
8564
|
+
const stride = Math.ceil(maxEnd / maxAlign) * maxAlign;
|
|
8565
|
+
return { fields, stride, structBody };
|
|
8566
|
+
}
|
|
8567
|
+
|
|
8568
|
+
// Packs an array of plain objects into a Float32Array using the given struct schema.
|
|
8569
|
+
// Reuses _packField so layout rules match uniform packing exactly.
|
|
8570
|
+
_packStructArray(data, schema) {
|
|
8571
|
+
const { fields, stride } = schema;
|
|
8572
|
+
const totalBytes = Math.max(data.length * stride, 16);
|
|
8573
|
+
const alignedBytes = Math.ceil(totalBytes / 16) * 16;
|
|
8574
|
+
const buffer = new ArrayBuffer(alignedBytes);
|
|
8575
|
+
const floatView = new Float32Array(buffer);
|
|
8576
|
+
const dataView = new DataView(buffer);
|
|
8577
|
+
for (let i = 0; i < data.length; i++) {
|
|
8578
|
+
const item = data[i];
|
|
8579
|
+
const baseOffset = i * stride;
|
|
8580
|
+
for (const field of fields) {
|
|
8581
|
+
this._packField(field, item[field.name], floatView, dataView, baseOffset);
|
|
8582
|
+
}
|
|
8583
|
+
}
|
|
8584
|
+
return floatView;
|
|
8585
|
+
}
|
|
8586
|
+
|
|
8587
|
+
// Inverse of _packStructArray reads packed buffer back into plain JS objects
|
|
8588
|
+
// using the same schema layout - fields, stride and offsets
|
|
8589
|
+
_unpackStructArray(floatView, schema) {
|
|
8590
|
+
const { fields, stride } = schema;
|
|
8591
|
+
const dataView = new DataView(floatView.buffer);
|
|
8592
|
+
const count = Math.floor(floatView.byteLength / stride);
|
|
8593
|
+
const result = [];
|
|
8594
|
+
|
|
8595
|
+
for (let i = 0; i < count; i++) {
|
|
8596
|
+
const item = {};
|
|
8597
|
+
const baseOffset = i * stride;
|
|
8598
|
+
for (const field of fields) {
|
|
8599
|
+
const byteOffset = baseOffset + field.offset;
|
|
8600
|
+
const n = field.size / 4;
|
|
8601
|
+
|
|
8602
|
+
if (field.baseType === 'u32') {
|
|
8603
|
+
if (n === 1) {
|
|
8604
|
+
item[field.name] = dataView.getUint32(byteOffset, true);
|
|
8605
|
+
} else {
|
|
8606
|
+
item[field.name] = Array.from({ length: n }, (_, j) =>
|
|
8607
|
+
dataView.getUint32(byteOffset + j * 4, true)
|
|
8608
|
+
);
|
|
8609
|
+
}
|
|
8610
|
+
} else if (field.baseType === 'i32') {
|
|
8611
|
+
if (n === 1) {
|
|
8612
|
+
item[field.name] = dataView.getInt32(byteOffset, true);
|
|
8613
|
+
} else {
|
|
8614
|
+
item[field.name] = Array.from({ length: n }, (_, j) =>
|
|
8615
|
+
dataView.getInt32(byteOffset + j * 4, true)
|
|
8616
|
+
);
|
|
8617
|
+
}
|
|
8618
|
+
} else {
|
|
8619
|
+
const idx = byteOffset / 4;
|
|
8620
|
+
if (n === 1) {
|
|
8621
|
+
item[field.name] = floatView[idx];
|
|
8622
|
+
} else {
|
|
8623
|
+
const values = Array.from(floatView.slice(idx, idx + n));
|
|
8624
|
+
if (field.kind === 'vector') {
|
|
8625
|
+
item[field.name] = this._pInst.createVector(...values);
|
|
8626
|
+
} else if (field.kind === 'color') {
|
|
8627
|
+
// Color was packed as normalized RGBA [0-1] via _getRGBA([1,1,1,1])
|
|
8628
|
+
// Scale back to the current colorMode range
|
|
8629
|
+
const maxes = this.states.colorMaxes[this.states.colorMode];
|
|
8630
|
+
item[field.name] = this._pInst.color(
|
|
8631
|
+
values[0] * maxes[0], values[1] * maxes[1],
|
|
8632
|
+
values[2] * maxes[2], values[3] * maxes[3]
|
|
8633
|
+
);
|
|
8634
|
+
} else {
|
|
8635
|
+
item[field.name] = values;
|
|
8636
|
+
}
|
|
8637
|
+
}
|
|
8638
|
+
}
|
|
8639
|
+
}
|
|
8640
|
+
result.push(item);
|
|
8641
|
+
}
|
|
8642
|
+
|
|
8643
|
+
return result;
|
|
8644
|
+
}
|
|
8645
|
+
|
|
8646
|
+
createStorage(dataOrCount) {
|
|
8647
|
+
const device = this.device;
|
|
8648
|
+
|
|
8649
|
+
// Struct array: an array of plain objects
|
|
8650
|
+
if (Array.isArray(dataOrCount) && dataOrCount.length > 0 &&
|
|
8651
|
+
typeof dataOrCount[0] === 'object' && !Array.isArray(dataOrCount[0])) {
|
|
8652
|
+
if (!p5.disableFriendlyErrors && dataOrCount.length > 1) {
|
|
8653
|
+
const firstKeys = Object.keys(dataOrCount[0]);
|
|
8654
|
+
let warned = false;
|
|
8655
|
+
for (let i = 1; i < dataOrCount.length; i++) {
|
|
8656
|
+
const el = dataOrCount[i];
|
|
8657
|
+
const elKeys = Object.keys(el);
|
|
8658
|
+
const sameKeys = firstKeys.length === elKeys.length &&
|
|
8659
|
+
firstKeys.every((k, j) => k === elKeys[j]);
|
|
8660
|
+
if (!sameKeys) {
|
|
8661
|
+
p5._friendlyError(
|
|
8662
|
+
`Element ${i} has different fields than element 0. ` +
|
|
8663
|
+
`All elements should have the same properties.`,
|
|
8664
|
+
'createStorage'
|
|
8665
|
+
);
|
|
8666
|
+
break;
|
|
8667
|
+
}
|
|
8668
|
+
for (const key of firstKeys) {
|
|
8669
|
+
const firstType = this._jsValueToWgslType(dataOrCount[0][key]);
|
|
8670
|
+
const elType = this._jsValueToWgslType(el[key]);
|
|
8671
|
+
if (firstType !== elType) {
|
|
8672
|
+
p5._friendlyError(
|
|
8673
|
+
`The "${key}" property of element ${i} has type ${elType} ` +
|
|
8674
|
+
`but element 0 has type ${firstType}. Proporties should have the same type across all elements.`,
|
|
8675
|
+
'createStorage'
|
|
8676
|
+
);
|
|
8677
|
+
warned = true;
|
|
8678
|
+
break;
|
|
8679
|
+
}
|
|
8680
|
+
}
|
|
8681
|
+
if (warned) break;
|
|
8682
|
+
}
|
|
8683
|
+
}
|
|
8684
|
+
const schema = this._inferStructSchema(dataOrCount[0]);
|
|
8685
|
+
const packed = this._packStructArray(dataOrCount, schema);
|
|
8686
|
+
const size = packed.byteLength;
|
|
8687
|
+
const buffer = device.createBuffer({
|
|
8688
|
+
size,
|
|
8689
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
|
8690
|
+
mappedAtCreation: true,
|
|
8691
|
+
});
|
|
8692
|
+
new Float32Array(buffer.getMappedRange()).set(packed);
|
|
8693
|
+
buffer.unmap();
|
|
8694
|
+
const storageBuffer = new StorageBuffer(buffer, size, this, schema);
|
|
8695
|
+
this._storageBuffers.add(storageBuffer);
|
|
8696
|
+
return storageBuffer;
|
|
8697
|
+
}
|
|
8698
|
+
|
|
8699
|
+
// Determine buffer size and initial data
|
|
8700
|
+
let size, initialData;
|
|
8701
|
+
if (typeof dataOrCount === 'number') {
|
|
8702
|
+
// createStorage(count) - zero-initialized
|
|
8703
|
+
size = dataOrCount * 4; // floats are 4 bytes
|
|
8704
|
+
initialData = new Float32Array(dataOrCount);
|
|
8705
|
+
} else {
|
|
8706
|
+
// createStorage(array) - from data
|
|
8707
|
+
if (dataOrCount instanceof Float32Array) {
|
|
8708
|
+
initialData = dataOrCount;
|
|
8709
|
+
} else if (Array.isArray(dataOrCount)) {
|
|
8710
|
+
initialData = new Float32Array(dataOrCount);
|
|
8711
|
+
} else {
|
|
8712
|
+
throw new Error('createStorage expects a number or array/Float32Array');
|
|
8713
|
+
}
|
|
8714
|
+
size = initialData.byteLength;
|
|
8715
|
+
}
|
|
8716
|
+
|
|
8717
|
+
// Align to 16 bytes (WGSL storage buffer alignment requirement)
|
|
8718
|
+
size = Math.ceil(size / 16) * 16;
|
|
8719
|
+
|
|
8720
|
+
// Create storage buffer with STORAGE | COPY_DST | COPY_SRC usage
|
|
8721
|
+
const buffer = device.createBuffer({
|
|
8722
|
+
size,
|
|
8723
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
|
8724
|
+
mappedAtCreation: initialData.length > 0
|
|
8725
|
+
});
|
|
8726
|
+
|
|
8727
|
+
// Write initial data if provided
|
|
8728
|
+
if (initialData.length > 0) {
|
|
8729
|
+
const mapping = new Float32Array(buffer.getMappedRange());
|
|
8730
|
+
mapping.set(initialData);
|
|
8731
|
+
buffer.unmap();
|
|
8732
|
+
}
|
|
8733
|
+
|
|
8734
|
+
const storageBuffer = new StorageBuffer(buffer, size, this);
|
|
8735
|
+
|
|
8736
|
+
// Track for cleanup
|
|
8737
|
+
this._storageBuffers.add(storageBuffer);
|
|
8738
|
+
|
|
8739
|
+
return storageBuffer;
|
|
8740
|
+
}
|
|
8741
|
+
|
|
7375
8742
|
_getWebGPUColorFormat(framebuffer) {
|
|
7376
8743
|
if (framebuffer.format === FLOAT) {
|
|
7377
8744
|
return framebuffer.channels === RGBA ? 'rgba32float' : 'rgba32float';
|
|
@@ -7668,10 +9035,6 @@ ${hookUniformFields}}
|
|
|
7668
9035
|
return super.filter(...args);
|
|
7669
9036
|
}
|
|
7670
9037
|
|
|
7671
|
-
getNoiseShaderSnippet() {
|
|
7672
|
-
return noiseWGSL;
|
|
7673
|
-
}
|
|
7674
|
-
|
|
7675
9038
|
|
|
7676
9039
|
baseFilterShader() {
|
|
7677
9040
|
if (!this._baseFilterShader) {
|
|
@@ -7695,6 +9058,21 @@ ${hookUniformFields}}
|
|
|
7695
9058
|
return this._baseFilterShader;
|
|
7696
9059
|
}
|
|
7697
9060
|
|
|
9061
|
+
baseComputeShader() {
|
|
9062
|
+
if (!this._baseComputeShader) {
|
|
9063
|
+
this._baseComputeShader = new Shader(
|
|
9064
|
+
this,
|
|
9065
|
+
baseComputeShader,
|
|
9066
|
+
{
|
|
9067
|
+
compute: {
|
|
9068
|
+
'void iteration': '(index: vec3<i32>) {}',
|
|
9069
|
+
},
|
|
9070
|
+
}
|
|
9071
|
+
);
|
|
9072
|
+
}
|
|
9073
|
+
return this._baseComputeShader;
|
|
9074
|
+
}
|
|
9075
|
+
|
|
7698
9076
|
/*
|
|
7699
9077
|
* WebGPU-specific implementation of imageLight shader creation
|
|
7700
9078
|
*/
|
|
@@ -7794,6 +9172,69 @@ ${hookUniformFields}}
|
|
|
7794
9172
|
glDataType: dataType || 'uint8'
|
|
7795
9173
|
};
|
|
7796
9174
|
}
|
|
9175
|
+
|
|
9176
|
+
compute(shader, x, y = 1, z = 1) {
|
|
9177
|
+
if (shader.shaderType !== 'compute') {
|
|
9178
|
+
throw new Error('compute() can only be called with a compute shader');
|
|
9179
|
+
}
|
|
9180
|
+
|
|
9181
|
+
this._finishActiveRenderPass();
|
|
9182
|
+
|
|
9183
|
+
// Ensure shader is initialized and finalized
|
|
9184
|
+
if (!shader._compiled) {
|
|
9185
|
+
shader.init();
|
|
9186
|
+
}
|
|
9187
|
+
|
|
9188
|
+
// Set default uniforms
|
|
9189
|
+
shader.setDefaultUniforms();
|
|
9190
|
+
shader.setUniform('uTotalCount', [x, y, z]);
|
|
9191
|
+
|
|
9192
|
+
// Calculate optimal workgroup size (8x8x1 = 64 threads per workgroup)
|
|
9193
|
+
const WORKGROUP_SIZE_X = 8;
|
|
9194
|
+
const WORKGROUP_SIZE_Y = 8;
|
|
9195
|
+
const WORKGROUP_SIZE_Z = 1;
|
|
9196
|
+
|
|
9197
|
+
// auto spreading: if any dimension is too large or for performance optimization,
|
|
9198
|
+
// spread total iteration count across dimensions
|
|
9199
|
+
const totalIterations = x * y * z;
|
|
9200
|
+
const MAX_THREADS_PER_DIM = 65535 * 8;
|
|
9201
|
+
|
|
9202
|
+
let px = x;
|
|
9203
|
+
let py = y;
|
|
9204
|
+
let pz = z;
|
|
9205
|
+
|
|
9206
|
+
// we spread if we exceed GPU limits OR if it involves a large 1D dispatch
|
|
9207
|
+
const exceedsLimits = x > MAX_THREADS_PER_DIM || y > MAX_THREADS_PER_DIM || z > MAX_THREADS_PER_DIM;
|
|
9208
|
+
const isLarge1D = totalIterations > 1024 && y === 1 && z === 1;
|
|
9209
|
+
|
|
9210
|
+
if (exceedsLimits || isLarge1D) {
|
|
9211
|
+
// Always use 2D square spreading (√N × √N).
|
|
9212
|
+
// Benchmarks showed 2D square equals or outperforms 3D cube at every
|
|
9213
|
+
// scale tested, with simpler index reconstruction in the shader.
|
|
9214
|
+
px = Math.ceil(Math.sqrt(totalIterations));
|
|
9215
|
+
py = Math.ceil(totalIterations / px);
|
|
9216
|
+
pz = 1;
|
|
9217
|
+
}
|
|
9218
|
+
|
|
9219
|
+
shader.setUniform('uPhysicalCount', [px, py, pz]);
|
|
9220
|
+
|
|
9221
|
+
const workgroupCountX = Math.ceil(px / WORKGROUP_SIZE_X);
|
|
9222
|
+
const workgroupCountY = Math.ceil(py / WORKGROUP_SIZE_Y);
|
|
9223
|
+
const workgroupCountZ = Math.ceil(pz / WORKGROUP_SIZE_Z);
|
|
9224
|
+
|
|
9225
|
+
const commandEncoder = this.device.createCommandEncoder();
|
|
9226
|
+
const passEncoder = commandEncoder.beginComputePass();
|
|
9227
|
+
this.setupShaderBindGroups(shader, passEncoder, {
|
|
9228
|
+
compute: true,
|
|
9229
|
+
workgroupSize: [WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y, WORKGROUP_SIZE_Z],
|
|
9230
|
+
});
|
|
9231
|
+
|
|
9232
|
+
// Dispatch compute workgroups
|
|
9233
|
+
passEncoder.dispatchWorkgroups(workgroupCountX, workgroupCountY, workgroupCountZ);
|
|
9234
|
+
|
|
9235
|
+
passEncoder.end();
|
|
9236
|
+
this.device.queue.submit([commandEncoder.finish()]);
|
|
9237
|
+
}
|
|
7797
9238
|
}
|
|
7798
9239
|
|
|
7799
9240
|
p5.RendererWebGPU = RendererWebGPU;
|
|
@@ -7804,6 +9245,7 @@ ${hookUniformFields}}
|
|
|
7804
9245
|
fn.setAttributes = async function (key, value) {
|
|
7805
9246
|
return this._renderer._setAttributes(key, value);
|
|
7806
9247
|
};
|
|
9248
|
+
|
|
7807
9249
|
}
|
|
7808
9250
|
|
|
7809
9251
|
if (typeof p5 !== "undefined") {
|