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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { W as WEBGPU, T as TRIANGLE_STRIP, L as LIGHTEST, D as DARKEST, S as SUBTRACT, R as REPLACE, E as EXCLUSION, a as SCREEN, M as MULTIPLY, b as REMOVE, A as ADD, B as BLEND, c as TRIANGLES, U as UNSIGNED_BYTE, F as FLOAT, H as HALF_FLOAT, d as UNSIGNED_INT, e as MIRROR, f as REPEAT, C as CLAMP, g as LINEAR, N as NEAREST } from '../constants-
|
|
1
|
+
import { W as WEBGPU, T as TRIANGLE_STRIP, L as LIGHTEST, D as DARKEST, S as SUBTRACT, R as REPLACE, E as EXCLUSION, a as SCREEN, M as MULTIPLY, b as REMOVE, A as ADD, B as BLEND, c as TRIANGLES, U as UNSIGNED_BYTE, F as FLOAT, H as HALF_FLOAT, d as UNSIGNED_INT, e as MIRROR, f as REPEAT, C as CLAMP, g as LINEAR, N as NEAREST } from '../constants-0wkVUfqa.js';
|
|
2
2
|
import { getStrokeDefs } from '../webgl/enums.js';
|
|
3
3
|
import { DataType } from '../strands/ir_types.js';
|
|
4
4
|
import { colorVertexShader, colorFragmentShader } from './shaders/color.js';
|
|
@@ -7,15 +7,26 @@ import { materialVertexShader, materialFragmentShader } from './shaders/material
|
|
|
7
7
|
import { fontVertexShader, fontFragmentShader } from './shaders/font.js';
|
|
8
8
|
import { blitVertexShader, blitFragmentShader } from './shaders/blit.js';
|
|
9
9
|
import { wgslBackend } from './strands_wgslBackend.js';
|
|
10
|
-
import noiseWGSL from './shaders/functions/noise3DWGSL.js';
|
|
11
10
|
import { baseFilterVertexShader, baseFilterFragmentShader } from './shaders/filters/base.js';
|
|
12
11
|
import { imageLightVertexShader, imageLightDiffusedFragmentShader, imageLightSpecularFragmentShader } from './shaders/imageLight.js';
|
|
12
|
+
import { baseComputeShader } from './shaders/compute.js';
|
|
13
|
+
import './shaders/functions/noise3DWGSL.js';
|
|
14
|
+
import './shaders/functions/randomWGSL.js';
|
|
15
|
+
import './shaders/functions/randomVertWGSL.js';
|
|
16
|
+
import './shaders/functions/randomComputeWGSL.js';
|
|
13
17
|
import '../strands/ir_dag.js';
|
|
14
18
|
import '../strands/strands_FES.js';
|
|
15
|
-
import '../ir_builders-
|
|
19
|
+
import '../ir_builders-C2ebb6Lu.js';
|
|
16
20
|
import '../strands/ir_cfg.js';
|
|
17
21
|
import '../strands/strands_builtins.js';
|
|
18
22
|
|
|
23
|
+
/**
|
|
24
|
+
* @module 3D
|
|
25
|
+
* @submodule p5.strands
|
|
26
|
+
* @for p5
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
|
|
19
30
|
const FRAME_STATE = {
|
|
20
31
|
PENDING: 0,
|
|
21
32
|
UNPROMOTED: 1,
|
|
@@ -37,6 +48,338 @@ function rendererWebGPU(p5, fn) {
|
|
|
37
48
|
RGBA,
|
|
38
49
|
} = p5;
|
|
39
50
|
|
|
51
|
+
class StorageBuffer {
|
|
52
|
+
constructor(buffer, size, renderer, schema = null) {
|
|
53
|
+
this._isStorageBuffer = true;
|
|
54
|
+
this.buffer = buffer;
|
|
55
|
+
this.size = size;
|
|
56
|
+
this._renderer = renderer;
|
|
57
|
+
this._schema = schema;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Updates the data in the buffer with new values. The new data must be in
|
|
62
|
+
* the same format as the data originally passed to
|
|
63
|
+
* <a href="#/p5/createStorage">`createStorage()`</a>.
|
|
64
|
+
*
|
|
65
|
+
* ```js example
|
|
66
|
+
* let particles;
|
|
67
|
+
* let computeShader;
|
|
68
|
+
* let displayShader;
|
|
69
|
+
* let instance;
|
|
70
|
+
* const numParticles = 100;
|
|
71
|
+
*
|
|
72
|
+
* async function setup() {
|
|
73
|
+
* await createCanvas(100, 100, WEBGPU);
|
|
74
|
+
* particles = createStorage(makeParticles(width / 2, height / 2));
|
|
75
|
+
* computeShader = buildComputeShader(simulate);
|
|
76
|
+
* displayShader = buildMaterialShader(display);
|
|
77
|
+
* instance = buildGeometry(drawParticle);
|
|
78
|
+
* describe('100 orange particles shooting outward.');
|
|
79
|
+
* }
|
|
80
|
+
*
|
|
81
|
+
* function makeParticles(x, y) {
|
|
82
|
+
* let data = [];
|
|
83
|
+
* for (let i = 0; i < numParticles; i++) {
|
|
84
|
+
* let angle = (i / numParticles) * TWO_PI;
|
|
85
|
+
* let speed = random(0.5, 2);
|
|
86
|
+
* data.push({
|
|
87
|
+
* position: createVector(x, y),
|
|
88
|
+
* velocity: createVector(cos(angle) * speed, sin(angle) * speed),
|
|
89
|
+
* });
|
|
90
|
+
* }
|
|
91
|
+
* return data;
|
|
92
|
+
* }
|
|
93
|
+
*
|
|
94
|
+
* function drawParticle() {
|
|
95
|
+
* sphere(2);
|
|
96
|
+
* }
|
|
97
|
+
*
|
|
98
|
+
* function simulate() {
|
|
99
|
+
* let data = uniformStorage(particles);
|
|
100
|
+
* let idx = index.x;
|
|
101
|
+
* data[idx].position = data[idx].position + data[idx].velocity;
|
|
102
|
+
* }
|
|
103
|
+
*
|
|
104
|
+
* function display() {
|
|
105
|
+
* let data = uniformStorage(particles);
|
|
106
|
+
* worldInputs.begin();
|
|
107
|
+
* let pos = data[instanceID()].position;
|
|
108
|
+
* worldInputs.position.xy += pos - [width / 2, height / 2];
|
|
109
|
+
* worldInputs.end();
|
|
110
|
+
* }
|
|
111
|
+
*
|
|
112
|
+
* function draw() {
|
|
113
|
+
* background(30);
|
|
114
|
+
* if (frameCount % 60 === 0) {
|
|
115
|
+
* particles.update(makeParticles(random(width), random(height)));
|
|
116
|
+
* }
|
|
117
|
+
* compute(computeShader, numParticles);
|
|
118
|
+
* noStroke();
|
|
119
|
+
* fill(255, 200, 50);
|
|
120
|
+
* shader(displayShader);
|
|
121
|
+
* model(instance, numParticles);
|
|
122
|
+
* }
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* @method update
|
|
126
|
+
* @for p5.StorageBuffer
|
|
127
|
+
* @beta
|
|
128
|
+
* @webgpu
|
|
129
|
+
* @webgpuOnly
|
|
130
|
+
* @param {Number[]|Float32Array|Object[]} data The new data to write into the buffer.
|
|
131
|
+
*/
|
|
132
|
+
update(data) {
|
|
133
|
+
const device = this._renderer.device;
|
|
134
|
+
|
|
135
|
+
if (this._schema !== null) {
|
|
136
|
+
// Buffer was created with a struct array
|
|
137
|
+
if (
|
|
138
|
+
!Array.isArray(data) ||
|
|
139
|
+
data.length === 0 ||
|
|
140
|
+
typeof data[0] !== 'object' ||
|
|
141
|
+
Array.isArray(data[0])
|
|
142
|
+
) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
'update() expects an array of objects matching the original struct format'
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const newSchema = this._renderer._inferStructSchema(data[0]);
|
|
149
|
+
if (newSchema.structBody !== this._schema.structBody) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`update() data structure doesn't match the original.\n` +
|
|
152
|
+
` Expected: ${this._schema.structBody}\n` +
|
|
153
|
+
` Got: ${newSchema.structBody}`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const packed = this._renderer._packStructArray(data, this._schema);
|
|
158
|
+
if (packed.byteLength > this.size) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`update() data (${packed.byteLength} bytes) exceeds buffer size (${this.size} bytes)`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
device.queue.writeBuffer(this.buffer, 0, packed);
|
|
164
|
+
} else {
|
|
165
|
+
// Buffer was created with a float array
|
|
166
|
+
let floatData;
|
|
167
|
+
if (data instanceof Float32Array) {
|
|
168
|
+
floatData = data;
|
|
169
|
+
} else if (Array.isArray(data)) {
|
|
170
|
+
floatData = new Float32Array(data);
|
|
171
|
+
} else {
|
|
172
|
+
throw new Error(
|
|
173
|
+
'update() expects a Float32Array or array of numbers for this buffer'
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (floatData.byteLength > this.size) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`update() data (${floatData.byteLength} bytes) exceeds buffer size (${this.size} bytes)`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
device.queue.writeBuffer(this.buffer, 0, floatData);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Reads data from a storage buffer back into JavaScript.
|
|
188
|
+
*
|
|
189
|
+
* Copies data from the GPU to the CPU using a temporary buffer,
|
|
190
|
+
* so it must be awaited. Returns a `Float32Array` for number
|
|
191
|
+
* buffers, or an array of plain objects for struct buffers.
|
|
192
|
+
*
|
|
193
|
+
* Note: This is a GPU -> CPU read, so calling it often (like every frame)
|
|
194
|
+
* can be slow.
|
|
195
|
+
*
|
|
196
|
+
* ```js example
|
|
197
|
+
* let data;
|
|
198
|
+
* let computeShader;
|
|
199
|
+
*
|
|
200
|
+
* async function setup() {
|
|
201
|
+
* await createCanvas(100, 100, WEBGPU);
|
|
202
|
+
*
|
|
203
|
+
* data = createStorage(new Float32Array([1, 2, 3, 4]));
|
|
204
|
+
* computeShader = buildComputeShader(doubleValues);
|
|
205
|
+
* compute(computeShader, 4);
|
|
206
|
+
*
|
|
207
|
+
* let result = await data.read();
|
|
208
|
+
* // result is Float32Array [2, 4, 6, 8]
|
|
209
|
+
* for (let i = 0; i < result.length; i++) {
|
|
210
|
+
* print(result[i]);
|
|
211
|
+
* }
|
|
212
|
+
* describe('Prints the values 2, 4, 6, 8 to the console.');
|
|
213
|
+
* }
|
|
214
|
+
*
|
|
215
|
+
* function doubleValues() {
|
|
216
|
+
* let d = uniformStorage(data);
|
|
217
|
+
* let idx = index.x;
|
|
218
|
+
* d[idx] = d[idx] * 2;
|
|
219
|
+
* }
|
|
220
|
+
* ```
|
|
221
|
+
*
|
|
222
|
+
* @method read
|
|
223
|
+
* @for p5.StorageBuffer
|
|
224
|
+
* @beta
|
|
225
|
+
* @webgpu
|
|
226
|
+
* @webgpuOnly
|
|
227
|
+
* @returns {Promise<Float32Array|Object[]>}
|
|
228
|
+
*/
|
|
229
|
+
async read() {
|
|
230
|
+
const device = this._renderer.device;
|
|
231
|
+
this._renderer.flushDraw();
|
|
232
|
+
|
|
233
|
+
const stagingBuffer = device.createBuffer({
|
|
234
|
+
size: this.size,
|
|
235
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const commandEncoder = device.createCommandEncoder();
|
|
239
|
+
commandEncoder.copyBufferToBuffer(this.buffer, 0, stagingBuffer, 0, this.size);
|
|
240
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
241
|
+
|
|
242
|
+
await stagingBuffer.mapAsync(GPUMapMode.READ, 0, this.size);
|
|
243
|
+
const mappedRange = stagingBuffer.getMappedRange(0, this.size);
|
|
244
|
+
|
|
245
|
+
// Copy before unmapping because mapped memory becomes invalid after unmap
|
|
246
|
+
const rawCopy = new Float32Array(mappedRange.byteLength / 4);
|
|
247
|
+
rawCopy.set(new Float32Array(mappedRange));
|
|
248
|
+
|
|
249
|
+
stagingBuffer.unmap();
|
|
250
|
+
stagingBuffer.destroy();
|
|
251
|
+
|
|
252
|
+
if (this._schema !== null) {
|
|
253
|
+
return this._renderer._unpackStructArray(rawCopy, this._schema);
|
|
254
|
+
}
|
|
255
|
+
return rawCopy;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Updates a single element in the buffer at a given index. Use this
|
|
260
|
+
* when only a small number of elements need to change. If you need to
|
|
261
|
+
* replace all the data at once, use
|
|
262
|
+
* <a href="#/p5.StorageBuffer/update">`update()`</a> instead.
|
|
263
|
+
*
|
|
264
|
+
* ```js
|
|
265
|
+
* let buf;
|
|
266
|
+
*
|
|
267
|
+
* async function setup() {
|
|
268
|
+
* await createCanvas(100, 100, WEBGPU);
|
|
269
|
+
*
|
|
270
|
+
* // Float buffer: update one value by index
|
|
271
|
+
* buf = createStorage(new Float32Array([1, 2, 3, 4]));
|
|
272
|
+
* buf.set(2, 9.5); // only index 2 changes → [1, 2, 9.5, 4]
|
|
273
|
+
*
|
|
274
|
+
* let result = await buf.read();
|
|
275
|
+
* print(result[2]); // 9.5
|
|
276
|
+
* describe('Prints 9.5 to the console.');
|
|
277
|
+
* }
|
|
278
|
+
* ```
|
|
279
|
+
*
|
|
280
|
+
* ```js
|
|
281
|
+
* let particles;
|
|
282
|
+
* const numParticles = 100;
|
|
283
|
+
*
|
|
284
|
+
* async function setup() {
|
|
285
|
+
* await createCanvas(100, 100, WEBGPU);
|
|
286
|
+
* particles = createStorage(makeParticles());
|
|
287
|
+
*
|
|
288
|
+
* // Replace particle 42 without touching the others
|
|
289
|
+
* particles.set(42, {
|
|
290
|
+
* position: createVector(0, 0),
|
|
291
|
+
* velocity: createVector(1, 0),
|
|
292
|
+
* });
|
|
293
|
+
*
|
|
294
|
+
* // Read back to confirm the update
|
|
295
|
+
* let result = await particles.read();
|
|
296
|
+
* print(result[42].position.x, result[42].position.y); // 0, 0
|
|
297
|
+
* describe('Prints the position of particle 42 after updating it.');
|
|
298
|
+
* }
|
|
299
|
+
*
|
|
300
|
+
* function makeParticles() {
|
|
301
|
+
* let data = [];
|
|
302
|
+
* for (let i = 0; i < numParticles; i++) {
|
|
303
|
+
* data.push({
|
|
304
|
+
* position: createVector(random(width), random(height)),
|
|
305
|
+
* velocity: createVector(random(-1, 1), random(-1, 1)),
|
|
306
|
+
* });
|
|
307
|
+
* }
|
|
308
|
+
* return data;
|
|
309
|
+
* }
|
|
310
|
+
* ```
|
|
311
|
+
*
|
|
312
|
+
* @method set
|
|
313
|
+
* @for p5.StorageBuffer
|
|
314
|
+
* @beta
|
|
315
|
+
* @webgpu
|
|
316
|
+
* @webgpuOnly
|
|
317
|
+
* @param {Number} index The zero-based index of the element to update.
|
|
318
|
+
* @param {Number|Object} value The new value. Pass a number for float
|
|
319
|
+
* buffers, or a plain object matching the original struct layout for
|
|
320
|
+
* struct buffers.
|
|
321
|
+
*/
|
|
322
|
+
set(index, value) {
|
|
323
|
+
const device = this._renderer.device;
|
|
324
|
+
|
|
325
|
+
if (this._schema !== null) {
|
|
326
|
+
// buffer was created with an array of structs
|
|
327
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
'set() expects a plain object matching the original struct format for this buffer'
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const { stride } = this._schema;
|
|
334
|
+
const byteOffset = index * stride;
|
|
335
|
+
|
|
336
|
+
if (byteOffset + stride > this.size) {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`set() index ${index} is out of bounds for this buffer ` +
|
|
339
|
+
`(buffer holds ${Math.floor(this.size / stride)} elements)`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// pack just this one element using the same logic as update()
|
|
344
|
+
const packed = this._renderer._packStructArray([value], this._schema);
|
|
345
|
+
// use packed.buffer (ArrayBuffer) so the size arg is always in bytes
|
|
346
|
+
device.queue.writeBuffer(this.buffer, byteOffset, packed.buffer, 0, stride);
|
|
347
|
+
} else {
|
|
348
|
+
// buffer was created with a float array
|
|
349
|
+
if (typeof value !== 'number') {
|
|
350
|
+
throw new Error(
|
|
351
|
+
'set() expects a number for this float buffer'
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const byteOffset = index * 4;
|
|
356
|
+
|
|
357
|
+
if (byteOffset + 4 > this.size) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`set() index ${index} is out of bounds for this buffer ` +
|
|
360
|
+
`(buffer holds ${Math.floor(this.size / 4)} floats)`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
device.queue.writeBuffer(this.buffer, byteOffset, new Float32Array([value]));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* A block of data that shaders can read from, and compute shaders can also
|
|
371
|
+
* write to. This is only available in WebGPU mode.
|
|
372
|
+
*
|
|
373
|
+
* Note: <a href="#/p5/createStorage">`createStorage()`</a> is the recommended
|
|
374
|
+
* way to create an instance of this class.
|
|
375
|
+
*
|
|
376
|
+
* @class p5.StorageBuffer
|
|
377
|
+
* @beta
|
|
378
|
+
* @webgpu
|
|
379
|
+
* @webgpuOnly
|
|
380
|
+
*/
|
|
381
|
+
p5.StorageBuffer = StorageBuffer;
|
|
382
|
+
|
|
40
383
|
class RendererWebGPU extends Renderer3D {
|
|
41
384
|
constructor(pInst, w, h, isMainCanvas, elt) {
|
|
42
385
|
super(pInst, w, h, isMainCanvas, elt);
|
|
@@ -88,6 +431,9 @@ function rendererWebGPU(p5, fn) {
|
|
|
88
431
|
// Retired buffers to destroy at end of frame
|
|
89
432
|
this._retiredBuffers = [];
|
|
90
433
|
|
|
434
|
+
// Storage buffers for compute shaders
|
|
435
|
+
this._storageBuffers = new Set();
|
|
436
|
+
|
|
91
437
|
// 2D canvas for pixel reading fallback
|
|
92
438
|
this._pixelReadCanvas = null;
|
|
93
439
|
this._pixelReadCtx = null;
|
|
@@ -164,7 +510,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
164
510
|
}
|
|
165
511
|
if (this._pInst._webgpuAttributes[key] !== value) {
|
|
166
512
|
//changing value of previously altered attribute
|
|
167
|
-
this._webgpuAttributes[key] = value;
|
|
513
|
+
this._pInst._webgpuAttributes[key] = value;
|
|
168
514
|
unchanged = false;
|
|
169
515
|
}
|
|
170
516
|
//setting all attributes with some change
|
|
@@ -298,9 +644,21 @@ function rendererWebGPU(p5, fn) {
|
|
|
298
644
|
const _b = args[2] || 0;
|
|
299
645
|
const _a = args[3] || 0;
|
|
300
646
|
|
|
301
|
-
// If PENDING and no custom framebuffer, clear means stay UNPROMOTED
|
|
302
|
-
if
|
|
303
|
-
|
|
647
|
+
// If PENDING and no custom framebuffer, clear means stay UNPROMOTED.
|
|
648
|
+
// However, if we are still in setup (frameCount == 0), we must promote
|
|
649
|
+
// so that mainFramebuffer gets the cleared content. This ensures that if
|
|
650
|
+
// draw() later promotes without a copy, it starts from the correct state
|
|
651
|
+
// rather than a stale mainFramebuffer.
|
|
652
|
+
// Note: a mid-draw-loop transition from UNPROMOTED back to PROMOTED
|
|
653
|
+
// (i.e. calling background() some frames but not others) will still
|
|
654
|
+
// lose intermediate UNPROMOTED frame content.
|
|
655
|
+
if (this._frameState !== FRAME_STATE.PROMOTED && !this.activeFramebuffer()) {
|
|
656
|
+
if (this._pInst.frameCount > 0) {
|
|
657
|
+
this._frameState = FRAME_STATE.UNPROMOTED;
|
|
658
|
+
} else {
|
|
659
|
+
this._promoteToFramebufferWithoutCopy();
|
|
660
|
+
// clear() then targets mainFramebuffer via activeFramebuffer()
|
|
661
|
+
}
|
|
304
662
|
}
|
|
305
663
|
|
|
306
664
|
this._finishActiveRenderPass();
|
|
@@ -503,7 +861,8 @@ function rendererWebGPU(p5, fn) {
|
|
|
503
861
|
return 4; // Cap at 4 for broader compatibility
|
|
504
862
|
}
|
|
505
863
|
|
|
506
|
-
_shaderOptions({ mode }) {
|
|
864
|
+
_shaderOptions({ mode, compute, workgroupSize }) {
|
|
865
|
+
if (compute) return { compute: true, workgroupSize };
|
|
507
866
|
const activeFramebuffer = this.activeFramebuffer();
|
|
508
867
|
const format = activeFramebuffer ?
|
|
509
868
|
this._getWebGPUColorFormat(activeFramebuffer) :
|
|
@@ -514,9 +873,9 @@ function rendererWebGPU(p5, fn) {
|
|
|
514
873
|
1; // No MSAA needed when blitting already-antialiased textures to canvas
|
|
515
874
|
const sampleCount = this._getValidSampleCount(requestedSampleCount);
|
|
516
875
|
|
|
517
|
-
const depthFormat = activeFramebuffer
|
|
518
|
-
this._getWebGPUDepthFormat(activeFramebuffer) :
|
|
519
|
-
this.depthFormat;
|
|
876
|
+
const depthFormat = activeFramebuffer
|
|
877
|
+
? (activeFramebuffer.useDepth ? this._getWebGPUDepthFormat(activeFramebuffer) : undefined)
|
|
878
|
+
: this.depthFormat;
|
|
520
879
|
|
|
521
880
|
const drawTarget = this.drawTarget();
|
|
522
881
|
const clipping = this._clipping;
|
|
@@ -544,6 +903,31 @@ function rendererWebGPU(p5, fn) {
|
|
|
544
903
|
_initShader(shader) {
|
|
545
904
|
const device = this.device;
|
|
546
905
|
|
|
906
|
+
if (shader.shaderType === 'compute') {
|
|
907
|
+
// Compute shader initialization
|
|
908
|
+
shader.computeModule = device.createShaderModule({ code: shader.computeSrc() });
|
|
909
|
+
shader._computePipelineCache = null;
|
|
910
|
+
shader._workgroupSize = null;
|
|
911
|
+
|
|
912
|
+
// Create compute pipeline (deferred until first compute() call)
|
|
913
|
+
shader.getPipeline = ({ workgroupSize }) => {
|
|
914
|
+
if (!shader._computePipelineCache) {
|
|
915
|
+
shader._computePipelineCache = device.createComputePipeline({
|
|
916
|
+
layout: shader._pipelineLayout,
|
|
917
|
+
compute: {
|
|
918
|
+
module: shader.computeModule,
|
|
919
|
+
entryPoint: 'main'
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
shader._workgroupSize = workgroupSize;
|
|
923
|
+
}
|
|
924
|
+
return shader._computePipelineCache;
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Render shader initialization
|
|
547
931
|
shader.vertModule = device.createShaderModule({ code: shader.vertSrc() });
|
|
548
932
|
shader.fragModule = device.createShaderModule({ code: shader.fragSrc() });
|
|
549
933
|
|
|
@@ -568,25 +952,27 @@ function rendererWebGPU(p5, fn) {
|
|
|
568
952
|
},
|
|
569
953
|
primitive: { topology },
|
|
570
954
|
multisample: { count: sampleCount },
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
955
|
+
...(depthFormat ? {
|
|
956
|
+
depthStencil: {
|
|
957
|
+
format: depthFormat,
|
|
958
|
+
depthWriteEnabled: !clipping,
|
|
959
|
+
depthCompare: 'less-equal',
|
|
960
|
+
stencilFront: {
|
|
961
|
+
compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
|
|
962
|
+
failOp: 'keep',
|
|
963
|
+
depthFailOp: 'keep',
|
|
964
|
+
passOp: clipping ? 'replace' : 'keep',
|
|
965
|
+
},
|
|
966
|
+
stencilBack: {
|
|
967
|
+
compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
|
|
968
|
+
failOp: 'keep',
|
|
969
|
+
depthFailOp: 'keep',
|
|
970
|
+
passOp: clipping ? 'replace' : 'keep',
|
|
971
|
+
},
|
|
972
|
+
stencilReadMask: 0xFF,
|
|
973
|
+
stencilWriteMask: clipping ? 0xFF : 0x00,
|
|
586
974
|
},
|
|
587
|
-
|
|
588
|
-
stencilWriteMask: clipping ? 0xFF : 0x00,
|
|
589
|
-
},
|
|
975
|
+
} : {}),
|
|
590
976
|
});
|
|
591
977
|
shader._pipelineCache.set(key, pipeline);
|
|
592
978
|
}
|
|
@@ -642,7 +1028,9 @@ function rendererWebGPU(p5, fn) {
|
|
|
642
1028
|
entries.push({
|
|
643
1029
|
bufferGroup,
|
|
644
1030
|
binding: bufferGroup.binding,
|
|
645
|
-
visibility:
|
|
1031
|
+
visibility: shader.shaderType === 'compute'
|
|
1032
|
+
? GPUShaderStage.COMPUTE
|
|
1033
|
+
: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
646
1034
|
buffer: { type: 'uniform', hasDynamicOffset: bufferGroup.dynamic },
|
|
647
1035
|
});
|
|
648
1036
|
structEntries.set(bufferGroup.group, entries);
|
|
@@ -676,6 +1064,24 @@ function rendererWebGPU(p5, fn) {
|
|
|
676
1064
|
groupEntries.set(group, entries);
|
|
677
1065
|
}
|
|
678
1066
|
|
|
1067
|
+
// Add storage buffer bindings
|
|
1068
|
+
for (const storage of shader._storageBuffers || []) {
|
|
1069
|
+
const group = storage.group;
|
|
1070
|
+
const entries = groupEntries.get(group) || [];
|
|
1071
|
+
|
|
1072
|
+
entries.push({
|
|
1073
|
+
binding: storage.binding,
|
|
1074
|
+
visibility: storage.visibility,
|
|
1075
|
+
buffer: {
|
|
1076
|
+
type: storage.accessMode === 'read' ? 'read-only-storage' : 'storage'
|
|
1077
|
+
},
|
|
1078
|
+
storage: storage,
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
entries.sort((a, b) => a.binding - b.binding);
|
|
1082
|
+
groupEntries.set(group, entries);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
679
1085
|
// Create layouts and bind groups
|
|
680
1086
|
const groupEntriesArr = [];
|
|
681
1087
|
for (const [group, entries] of groupEntries) {
|
|
@@ -694,6 +1100,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
694
1100
|
shader._pipelineLayout = this.device.createPipelineLayout({
|
|
695
1101
|
bindGroupLayouts: shader._bindGroupLayouts,
|
|
696
1102
|
});
|
|
1103
|
+
shader._compiled = true;
|
|
697
1104
|
}
|
|
698
1105
|
|
|
699
1106
|
_getBlendState(mode) {
|
|
@@ -940,8 +1347,11 @@ function rendererWebGPU(p5, fn) {
|
|
|
940
1347
|
|
|
941
1348
|
_resetBuffersBeforeDraw() {
|
|
942
1349
|
this._finishActiveRenderPass();
|
|
1350
|
+
|
|
943
1351
|
// Set state to PENDING - we'll decide on first draw
|
|
944
|
-
this.
|
|
1352
|
+
if (this._pInst.frameCount > 0) {
|
|
1353
|
+
this._frameState = FRAME_STATE.PENDING;
|
|
1354
|
+
}
|
|
945
1355
|
|
|
946
1356
|
// Clear depth buffer but DON'T start any render pass yet
|
|
947
1357
|
const activeFramebuffer = this.activeFramebuffer();
|
|
@@ -1052,6 +1462,8 @@ function rendererWebGPU(p5, fn) {
|
|
|
1052
1462
|
// once we're drawing to the framebuffer, because normally
|
|
1053
1463
|
// those are reset.
|
|
1054
1464
|
const savedModelMatrix = this.states.uModelMatrix.copy();
|
|
1465
|
+
this.states.uModelMatrix.set(this.states.uModelMatrix.copy());
|
|
1466
|
+
this.states.uModelMatrix.reset();
|
|
1055
1467
|
this.mainFramebuffer.defaultCamera.set(this.states.curCamera);
|
|
1056
1468
|
|
|
1057
1469
|
this.mainFramebuffer.begin();
|
|
@@ -1060,6 +1472,11 @@ function rendererWebGPU(p5, fn) {
|
|
|
1060
1472
|
}
|
|
1061
1473
|
|
|
1062
1474
|
_promoteToFramebufferWithoutCopy() {
|
|
1475
|
+
// Already promoted this frame
|
|
1476
|
+
if (this._frameState === FRAME_STATE.PROMOTED) {
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1063
1480
|
// Ensure mainFramebuffer matches canvas size
|
|
1064
1481
|
if (this.mainFramebuffer.width !== this.width ||
|
|
1065
1482
|
this.mainFramebuffer.height !== this.height) {
|
|
@@ -1074,6 +1491,8 @@ function rendererWebGPU(p5, fn) {
|
|
|
1074
1491
|
|
|
1075
1492
|
// Preserve transformation state
|
|
1076
1493
|
const savedModelMatrix = this.states.uModelMatrix.copy();
|
|
1494
|
+
this.states.uModelMatrix.set(this.states.uModelMatrix.copy());
|
|
1495
|
+
this.states.uModelMatrix.reset();
|
|
1077
1496
|
this.mainFramebuffer.defaultCamera.set(this.states.curCamera);
|
|
1078
1497
|
|
|
1079
1498
|
// Begin rendering to mainFramebuffer
|
|
@@ -1387,7 +1806,6 @@ function rendererWebGPU(p5, fn) {
|
|
|
1387
1806
|
}
|
|
1388
1807
|
this.flushDraw();
|
|
1389
1808
|
|
|
1390
|
-
// this._pInst.background('red');
|
|
1391
1809
|
this._pInst.push();
|
|
1392
1810
|
this.states.setValue('enableLighting', false);
|
|
1393
1811
|
this.states.setValue('activeImageLight', null);
|
|
@@ -1452,25 +1870,9 @@ function rendererWebGPU(p5, fn) {
|
|
|
1452
1870
|
|
|
1453
1871
|
this._beginActiveRenderPass();
|
|
1454
1872
|
const passEncoder = this.activeRenderPass;
|
|
1455
|
-
const currentShader = this._curShader;
|
|
1456
|
-
const shaderOptions = this._shaderOptions({ mode });
|
|
1457
|
-
if (this.activeShader !== currentShader || this._shaderOptionsDifferent(shaderOptions)) {
|
|
1458
|
-
passEncoder.setPipeline(currentShader.getPipeline(shaderOptions));
|
|
1459
|
-
}
|
|
1460
|
-
this.activeShader = currentShader;
|
|
1461
|
-
this.activeShaderOptions = shaderOptions;
|
|
1462
1873
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
if (drawTarget._isClipApplied && !this._clipping) {
|
|
1466
|
-
// When using the clip mask, test against reference value 0 (background)
|
|
1467
|
-
// WebGL uses NOTEQUAL with ref 0, so fragments pass where stencil != 0
|
|
1468
|
-
// In WebGPU with 'not-equal', we need ref 0 to pass where stencil != 0
|
|
1469
|
-
passEncoder.setStencilReference(0);
|
|
1470
|
-
} else if (this._clipping) {
|
|
1471
|
-
// When writing to the clip mask, write reference value 1
|
|
1472
|
-
passEncoder.setStencilReference(1);
|
|
1473
|
-
}
|
|
1874
|
+
const currentShader = this._curShader;
|
|
1875
|
+
this.setupShaderBindGroups(currentShader, passEncoder, { mode, buffers });
|
|
1474
1876
|
// Bind vertex buffers
|
|
1475
1877
|
for (const buffer of currentShader._vertexBuffers || this._getVertexBuffers(currentShader)) {
|
|
1476
1878
|
const location = currentShader.attributes[buffer.attr].location;
|
|
@@ -1478,6 +1880,58 @@ function rendererWebGPU(p5, fn) {
|
|
|
1478
1880
|
passEncoder.setVertexBuffer(location, gpuBuffer, 0);
|
|
1479
1881
|
}
|
|
1480
1882
|
|
|
1883
|
+
if (currentShader.shaderType === "fill") {
|
|
1884
|
+
// Bind index buffer and issue draw
|
|
1885
|
+
if (buffers.indexBuffer) {
|
|
1886
|
+
const indexFormat = buffers.indexFormat || "uint16";
|
|
1887
|
+
passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
|
|
1888
|
+
passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
|
|
1889
|
+
} else {
|
|
1890
|
+
passEncoder.draw(geometry.vertices.length, count, 0, 0);
|
|
1891
|
+
}
|
|
1892
|
+
} else if (currentShader.shaderType === "text") {
|
|
1893
|
+
if (!buffers.indexBuffer) {
|
|
1894
|
+
throw new Error("Text geometry must have an index buffer");
|
|
1895
|
+
}
|
|
1896
|
+
const indexFormat = buffers.indexFormat || "uint16";
|
|
1897
|
+
passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
|
|
1898
|
+
passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
if (buffers.lineVerticesBuffer && currentShader.shaderType === "stroke") {
|
|
1902
|
+
passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0);
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// Mark that we have pending draws that need submission
|
|
1906
|
+
this._hasPendingDraws = true;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
setupShaderBindGroups(currentShader, passEncoder, shaderOptionsParams) {
|
|
1910
|
+
const shaderOptions = this._shaderOptions(shaderOptionsParams);
|
|
1911
|
+
if (
|
|
1912
|
+
shaderOptions.compute ||
|
|
1913
|
+
this.activeShader !== currentShader ||
|
|
1914
|
+
this._shaderOptionsDifferent(shaderOptions)
|
|
1915
|
+
) {
|
|
1916
|
+
passEncoder.setPipeline(currentShader.getPipeline(shaderOptions));
|
|
1917
|
+
}
|
|
1918
|
+
if (!shaderOptions.compute) {
|
|
1919
|
+
this.activeShader = currentShader;
|
|
1920
|
+
this.activeShaderOptions = shaderOptions;
|
|
1921
|
+
|
|
1922
|
+
// Set stencil reference value for clipping
|
|
1923
|
+
const drawTarget = this.drawTarget();
|
|
1924
|
+
if (drawTarget._isClipApplied && !this._clipping) {
|
|
1925
|
+
// When using the clip mask, test against reference value 0 (background)
|
|
1926
|
+
// WebGL uses NOTEQUAL with ref 0, so fragments pass where stencil != 0
|
|
1927
|
+
// In WebGPU with 'not-equal', we need ref 0 to pass where stencil != 0
|
|
1928
|
+
passEncoder.setStencilReference(0);
|
|
1929
|
+
} else if (this._clipping) {
|
|
1930
|
+
// When writing to the clip mask, write reference value 1
|
|
1931
|
+
passEncoder.setStencilReference(1);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1481
1935
|
for (const bufferGroup of currentShader._uniformBufferGroups) {
|
|
1482
1936
|
if (bufferGroup.dynamic) {
|
|
1483
1937
|
// Bind uniforms into a part of a big dynamic memory block because
|
|
@@ -1530,6 +1984,13 @@ function rendererWebGPU(p5, fn) {
|
|
|
1530
1984
|
currentShader.buffersDirty.delete(key);
|
|
1531
1985
|
}
|
|
1532
1986
|
}
|
|
1987
|
+
for (const storage of currentShader._storageBuffers || []) {
|
|
1988
|
+
const key = storage.group * 1000 + storage.binding;
|
|
1989
|
+
if (currentShader.buffersDirty.has(key)) {
|
|
1990
|
+
currentShader._cachedBindGroup[storage.group] = undefined;
|
|
1991
|
+
currentShader.buffersDirty.delete(key);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1533
1994
|
|
|
1534
1995
|
// Bind sampler/texture uniforms and uniform buffers
|
|
1535
1996
|
for (const iter of currentShader._groupEntries) {
|
|
@@ -1559,6 +2020,19 @@ function rendererWebGPU(p5, fn) {
|
|
|
1559
2020
|
: { buffer: uniformBufferInfo.buffer },
|
|
1560
2021
|
});
|
|
1561
2022
|
}
|
|
2023
|
+
} else if (entry.storage && !bindGroup) {
|
|
2024
|
+
// Storage buffer binding
|
|
2025
|
+
const uniform = currentShader.uniforms[entry.storage.name];
|
|
2026
|
+
if (!uniform || !uniform._cachedData || !uniform._cachedData._isStorageBuffer) {
|
|
2027
|
+
throw new Error(
|
|
2028
|
+
`Storage buffer "${entry.storage.name}" not set. ` +
|
|
2029
|
+
`Use shader.setUniform("${entry.storage.name}", storageBuffer)`
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
bgEntries.push({
|
|
2033
|
+
binding: entry.binding,
|
|
2034
|
+
resource: { buffer: uniform._cachedData.buffer },
|
|
2035
|
+
});
|
|
1562
2036
|
} else if (!bindGroup) {
|
|
1563
2037
|
bgEntries.push({
|
|
1564
2038
|
binding: entry.binding,
|
|
@@ -1592,84 +2066,71 @@ function rendererWebGPU(p5, fn) {
|
|
|
1592
2066
|
);
|
|
1593
2067
|
}
|
|
1594
2068
|
}
|
|
2069
|
+
return passEncoder;
|
|
2070
|
+
}
|
|
1595
2071
|
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
2072
|
+
//////////////////////////////////////////////
|
|
2073
|
+
// SHADER
|
|
2074
|
+
//////////////////////////////////////////////
|
|
2075
|
+
|
|
2076
|
+
// Writes a single field's value into a Float32Array+DataView at (baseOffset + field.offset).
|
|
2077
|
+
//
|
|
2078
|
+
// Field interface (shared by uniform fields from _parseStruct and struct storage schema fields):
|
|
2079
|
+
// baseType: string - 'f32', 'i32', 'u32', etc.
|
|
2080
|
+
// size: number - byte size of the field
|
|
2081
|
+
// offset: number - byte offset of the field within its struct
|
|
2082
|
+
// packInPlace: bool - true for mat3, written with manual column padding
|
|
2083
|
+
//
|
|
2084
|
+
// value: number or number[] - the data to write
|
|
2085
|
+
_packField(field, value, floatView, dataView, baseOffset) {
|
|
2086
|
+
if (value === undefined) return;
|
|
2087
|
+
|
|
2088
|
+
// Duck typing instead of instanceof to avoid importing a separate
|
|
2089
|
+
// copy of the Color/Vector classes
|
|
2090
|
+
if (value?.isVector) {
|
|
2091
|
+
value = value.values.length !== value.dimensions ? value.values.slice(0, value.dimensions) : value.values;
|
|
2092
|
+
} else if (value?.isColor) {
|
|
2093
|
+
value = value._getRGBA([1, 1, 1, 1]);
|
|
2094
|
+
}
|
|
2095
|
+
const byteOffset = baseOffset + field.offset;
|
|
2096
|
+
if (field.baseType === 'u32') {
|
|
2097
|
+
if (field.size === 4) {
|
|
2098
|
+
dataView.setUint32(byteOffset, value, true);
|
|
1602
2099
|
} else {
|
|
1603
|
-
|
|
2100
|
+
for (let i = 0; i < value.length; i++) {
|
|
2101
|
+
dataView.setUint32(byteOffset + i * 4, value[i], true);
|
|
2102
|
+
}
|
|
1604
2103
|
}
|
|
1605
|
-
} else if (
|
|
1606
|
-
if (
|
|
1607
|
-
|
|
2104
|
+
} else if (field.baseType === 'i32') {
|
|
2105
|
+
if (field.size === 4) {
|
|
2106
|
+
dataView.setInt32(byteOffset, value, true);
|
|
2107
|
+
} else {
|
|
2108
|
+
for (let i = 0; i < value.length; i++) {
|
|
2109
|
+
dataView.setInt32(byteOffset + i * 4, value[i], true);
|
|
2110
|
+
}
|
|
1608
2111
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
2112
|
+
} else if (field.packInPlace) {
|
|
2113
|
+
// In-place packing for mat3: write directly to buffer with padding
|
|
2114
|
+
const base = byteOffset / 4;
|
|
2115
|
+
floatView[base + 0] = value[0]; floatView[base + 1] = value[1]; floatView[base + 2] = value[2];
|
|
2116
|
+
floatView[base + 4] = value[3]; floatView[base + 5] = value[4]; floatView[base + 6] = value[5];
|
|
2117
|
+
floatView[base + 8] = value[6]; floatView[base + 9] = value[7]; floatView[base + 10] = value[8];
|
|
2118
|
+
} else if (field.size === 4) {
|
|
2119
|
+
floatView.set([value], byteOffset / 4);
|
|
2120
|
+
} else {
|
|
2121
|
+
floatView.set(value, byteOffset / 4);
|
|
1616
2122
|
}
|
|
1617
|
-
|
|
1618
|
-
// Mark that we have pending draws that need submission
|
|
1619
|
-
this._hasPendingDraws = true;
|
|
1620
2123
|
}
|
|
1621
2124
|
|
|
1622
|
-
//////////////////////////////////////////////
|
|
1623
|
-
// SHADER
|
|
1624
|
-
//////////////////////////////////////////////
|
|
1625
|
-
|
|
1626
2125
|
_packUniformGroup(shader, groupUniforms, bufferInfo) {
|
|
1627
2126
|
// Pack a single group's uniforms into a buffer
|
|
1628
2127
|
const data = bufferInfo.data;
|
|
1629
2128
|
const dataView = bufferInfo.dataView;
|
|
1630
|
-
|
|
1631
2129
|
const offset = bufferInfo.offset || 0;
|
|
1632
2130
|
for (const uniform of groupUniforms) {
|
|
1633
2131
|
const fullUniform = shader.uniforms[uniform.name];
|
|
1634
2132
|
if (!fullUniform || fullUniform.isSampler) continue;
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
if (fullUniform.baseType === 'u32') {
|
|
1638
|
-
if (fullUniform.size === 4) {
|
|
1639
|
-
dataView.setUint32(offset + fullUniform.offset, uniformData, true);
|
|
1640
|
-
} else {
|
|
1641
|
-
for (let i = 0; i < uniformData.length; i++) {
|
|
1642
|
-
dataView.setUint32(offset + fullUniform.offset + i * 4, uniformData[i], true);
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
} else if (fullUniform.baseType === 'i32') {
|
|
1646
|
-
if (fullUniform.size === 4) {
|
|
1647
|
-
dataView.setInt32(offset + fullUniform.offset, uniformData, true);
|
|
1648
|
-
} else {
|
|
1649
|
-
for (let i = 0; i < uniformData.length; i++) {
|
|
1650
|
-
dataView.setInt32(offset + fullUniform.offset + i * 4, uniformData[i], true);
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
} else if (fullUniform.packInPlace) {
|
|
1654
|
-
// In-place packing for mat3: write directly to buffer with padding
|
|
1655
|
-
const baseOffset = (offset + fullUniform.offset) / 4;
|
|
1656
|
-
// Column 0
|
|
1657
|
-
data[baseOffset + 0] = uniformData[0];
|
|
1658
|
-
data[baseOffset + 1] = uniformData[1];
|
|
1659
|
-
data[baseOffset + 2] = uniformData[2];
|
|
1660
|
-
// Column 1
|
|
1661
|
-
data[baseOffset + 4] = uniformData[3];
|
|
1662
|
-
data[baseOffset + 5] = uniformData[4];
|
|
1663
|
-
data[baseOffset + 6] = uniformData[5];
|
|
1664
|
-
// Column 2
|
|
1665
|
-
data[baseOffset + 8] = uniformData[6];
|
|
1666
|
-
data[baseOffset + 9] = uniformData[7];
|
|
1667
|
-
data[baseOffset + 10] = uniformData[8];
|
|
1668
|
-
} else if (fullUniform.size === 4) {
|
|
1669
|
-
data.set([uniformData], (offset + fullUniform.offset) / 4);
|
|
1670
|
-
} else if (uniformData !== undefined) {
|
|
1671
|
-
data.set(uniformData, (offset + fullUniform.offset) / 4);
|
|
1672
|
-
}
|
|
2133
|
+
this._packField(fullUniform, fullUniform._mappedData, data, dataView, offset);
|
|
1673
2134
|
}
|
|
1674
2135
|
}
|
|
1675
2136
|
|
|
@@ -1816,10 +2277,11 @@ function rendererWebGPU(p5, fn) {
|
|
|
1816
2277
|
const uniformVarRegex = /@group\((\d+)\)\s+@binding\((\d+)\)\s+var<uniform>\s+(\w+)\s*:\s*(\w+);/g;
|
|
1817
2278
|
|
|
1818
2279
|
let match;
|
|
1819
|
-
|
|
2280
|
+
const src = shader.shaderType === 'compute' ? shader.computeSrc() : shader.vertSrc();
|
|
2281
|
+
while ((match = uniformVarRegex.exec(src)) !== null) {
|
|
1820
2282
|
const [_, groupNum, binding, varName, structType] = match;
|
|
1821
2283
|
const bindingIndex = parseInt(binding);
|
|
1822
|
-
const uniforms = this._parseStruct(
|
|
2284
|
+
const uniforms = this._parseStruct(src, structType);
|
|
1823
2285
|
|
|
1824
2286
|
uniformGroups.push({
|
|
1825
2287
|
group: parseInt(groupNum),
|
|
@@ -1830,7 +2292,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
1830
2292
|
});
|
|
1831
2293
|
}
|
|
1832
2294
|
|
|
1833
|
-
if (uniformGroups.length === 0) {
|
|
2295
|
+
if (uniformGroups.length === 0 && shader.shaderType !== 'compute') {
|
|
1834
2296
|
throw new Error('Expected at least one uniform struct bound to @group(0)');
|
|
1835
2297
|
}
|
|
1836
2298
|
|
|
@@ -1857,6 +2319,10 @@ function rendererWebGPU(p5, fn) {
|
|
|
1857
2319
|
// TODO: support other texture types
|
|
1858
2320
|
const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d<f32>|sampler);/g;
|
|
1859
2321
|
|
|
2322
|
+
// Extract storage buffers
|
|
2323
|
+
const storageBuffers = {};
|
|
2324
|
+
const storageRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var<storage,\s*(read|read_write)>\s+(\w+)\s*:\s*array<\w+>/g;
|
|
2325
|
+
|
|
1860
2326
|
// Track which bindings are taken by the struct properties we've parsed
|
|
1861
2327
|
// (the rest should be textures/samplers)
|
|
1862
2328
|
const structUniformBindings = {};
|
|
@@ -1866,8 +2332,11 @@ function rendererWebGPU(p5, fn) {
|
|
|
1866
2332
|
|
|
1867
2333
|
for (const [src, visibility] of [
|
|
1868
2334
|
[shader.vertSrc(), GPUShaderStage.VERTEX],
|
|
1869
|
-
[shader.fragSrc(), GPUShaderStage.FRAGMENT]
|
|
2335
|
+
[shader.fragSrc(), GPUShaderStage.FRAGMENT],
|
|
2336
|
+
[shader.computeSrc ? shader.computeSrc() : null, GPUShaderStage.COMPUTE]
|
|
1870
2337
|
]) {
|
|
2338
|
+
if (!src) continue; // Skip if shader stage doesn't exist
|
|
2339
|
+
|
|
1871
2340
|
let match;
|
|
1872
2341
|
while ((match = samplerRegex.exec(src)) !== null) {
|
|
1873
2342
|
const [_, group, binding, name, type] = match;
|
|
@@ -1902,21 +2371,51 @@ function rendererWebGPU(p5, fn) {
|
|
|
1902
2371
|
samplerNode.textureSource = sampler;
|
|
1903
2372
|
}
|
|
1904
2373
|
}
|
|
2374
|
+
|
|
2375
|
+
// Parse storage buffers
|
|
2376
|
+
while ((match = storageRegex.exec(src)) !== null) {
|
|
2377
|
+
const [_, group, binding, accessMode, name] = match;
|
|
2378
|
+
const groupIndex = parseInt(group);
|
|
2379
|
+
const bindingIndex = parseInt(binding);
|
|
2380
|
+
|
|
2381
|
+
const key = `${groupIndex},${bindingIndex}`;
|
|
2382
|
+
const existing = storageBuffers[key];
|
|
2383
|
+
// If any stage uses read_write, the bind group layout must use read_write
|
|
2384
|
+
const finalAccessMode = (existing?.accessMode === 'read_write' || accessMode === 'read_write')
|
|
2385
|
+
? 'read_write'
|
|
2386
|
+
: accessMode;
|
|
2387
|
+
|
|
2388
|
+
storageBuffers[key] = {
|
|
2389
|
+
visibility: (existing?.visibility || 0) | visibility,
|
|
2390
|
+
group: groupIndex,
|
|
2391
|
+
binding: bindingIndex,
|
|
2392
|
+
name,
|
|
2393
|
+
accessMode: finalAccessMode, // 'read' or 'read_write'
|
|
2394
|
+
isStorage: true,
|
|
2395
|
+
type: 'storage'
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
1905
2398
|
}
|
|
1906
|
-
|
|
2399
|
+
|
|
2400
|
+
// Store storage buffers on shader for later use
|
|
2401
|
+
shader._storageBuffers = Object.values(storageBuffers);
|
|
2402
|
+
|
|
2403
|
+
return [...Object.values(allUniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers), ...Object.values(storageBuffers)];
|
|
1907
2404
|
}
|
|
1908
2405
|
|
|
1909
|
-
getNextBindingIndex({ vert, frag }, group = 0) {
|
|
2406
|
+
getNextBindingIndex({ vert, frag, compute }, group = 0) {
|
|
1910
2407
|
// Get the highest binding index in the specified group and return the next available
|
|
1911
|
-
const
|
|
2408
|
+
const bindingRegex = /@group\((\d+)\)\s*@binding\((\d+)\)/g;
|
|
1912
2409
|
let maxBindingIndex = -1;
|
|
1913
2410
|
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
])
|
|
2411
|
+
const sources = [];
|
|
2412
|
+
if (vert) sources.push([vert, GPUShaderStage.VERTEX]);
|
|
2413
|
+
if (frag) sources.push([frag, GPUShaderStage.FRAGMENT]);
|
|
2414
|
+
if (compute) sources.push([compute, GPUShaderStage.COMPUTE]);
|
|
2415
|
+
|
|
2416
|
+
for (const [src, visibility] of sources) {
|
|
1918
2417
|
let match;
|
|
1919
|
-
while ((match =
|
|
2418
|
+
while ((match = bindingRegex.exec(src)) !== null) {
|
|
1920
2419
|
const [_, groupIndex, bindingIndex] = match;
|
|
1921
2420
|
if (parseInt(groupIndex) === group) {
|
|
1922
2421
|
maxBindingIndex = Math.max(maxBindingIndex, parseInt(bindingIndex));
|
|
@@ -1931,7 +2430,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
1931
2430
|
if (uniform.isSampler) {
|
|
1932
2431
|
uniform.texture =
|
|
1933
2432
|
data instanceof Texture ? data : this.getTexture(data);
|
|
1934
|
-
} else {
|
|
2433
|
+
} else if (!data?._isStorageBuffer) {
|
|
1935
2434
|
uniform._mappedData = this._mapUniformData(uniform, uniform._cachedData);
|
|
1936
2435
|
}
|
|
1937
2436
|
shader.buffersDirty.add(uniform.group * 1000 + uniform.binding);
|
|
@@ -2038,7 +2537,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
2038
2537
|
rgb += components.emissive;
|
|
2039
2538
|
return vec4<f32>(rgb, components.opacity);
|
|
2040
2539
|
}`,
|
|
2041
|
-
"vec4f getFinalColor": "(color: vec4<f32>) { return color; }",
|
|
2540
|
+
"vec4f getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
|
|
2042
2541
|
"void afterFragment": "() {}",
|
|
2043
2542
|
},
|
|
2044
2543
|
}
|
|
@@ -2063,7 +2562,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
2063
2562
|
},
|
|
2064
2563
|
fragment: {
|
|
2065
2564
|
"void beforeFragment": "() {}",
|
|
2066
|
-
"vec4<f32> getFinalColor": "(color: vec4<f32>) { return color; }",
|
|
2565
|
+
"vec4<f32> getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
|
|
2067
2566
|
"void afterFragment": "() {}",
|
|
2068
2567
|
},
|
|
2069
2568
|
}
|
|
@@ -2089,7 +2588,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
2089
2588
|
fragment: {
|
|
2090
2589
|
"void beforeFragment": "() {}",
|
|
2091
2590
|
"Inputs getPixelInputs": "(inputs: Inputs) { return inputs; }",
|
|
2092
|
-
"vec4<f32> getFinalColor": "(color: vec4<f32>) { return color; }",
|
|
2591
|
+
"vec4<f32> getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
|
|
2093
2592
|
"bool shouldDiscard": "(outside: bool) { return outside; };",
|
|
2094
2593
|
"void afterFragment": "() {}",
|
|
2095
2594
|
},
|
|
@@ -2248,11 +2747,87 @@ function rendererWebGPU(p5, fn) {
|
|
|
2248
2747
|
}
|
|
2249
2748
|
);
|
|
2250
2749
|
|
|
2251
|
-
let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment)\s*)?fn main[^{]+\{)/);
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2750
|
+
let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment|compute)\s*(?:@workgroup_size\([^)]+\)\s*)?)?fn main[^{]+\{)/);
|
|
2751
|
+
|
|
2752
|
+
const getBuiltinParamName = (mainSrc, builtinName) => {
|
|
2753
|
+
const match = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`).exec(mainSrc);
|
|
2754
|
+
return match ? match[1] : null;
|
|
2755
|
+
};
|
|
2756
|
+
|
|
2757
|
+
const ensureBuiltinParam = (mainSrc, builtinName, fallbackName, typeName) => {
|
|
2758
|
+
const existingName = getBuiltinParamName(mainSrc, builtinName);
|
|
2759
|
+
if (existingName) {
|
|
2760
|
+
return { mainSrc, argName: existingName };
|
|
2255
2761
|
}
|
|
2762
|
+
|
|
2763
|
+
const hasParams = /\(\s*\S/.test(mainSrc);
|
|
2764
|
+
const injectedMain = mainSrc.replace(
|
|
2765
|
+
/\)\s*(->|\{)/,
|
|
2766
|
+
`${hasParams ? ', ' : ''}@builtin(${builtinName}) ${fallbackName}: ${typeName}) $1`
|
|
2767
|
+
);
|
|
2768
|
+
|
|
2769
|
+
return { mainSrc: injectedMain, argName: fallbackName };
|
|
2770
|
+
};
|
|
2771
|
+
|
|
2772
|
+
const getMainStructParameter = (mainSrc) => {
|
|
2773
|
+
const match = /fn main\s*\(\s*(\w+)\s*:\s*(\w+)/.exec(mainSrc);
|
|
2774
|
+
if (!match) return null;
|
|
2775
|
+
return { inputName: match[1], structName: match[2] };
|
|
2776
|
+
};
|
|
2777
|
+
|
|
2778
|
+
const getStructBuiltinFieldName = (structName, builtinName) => {
|
|
2779
|
+
const structMatch = new RegExp(`struct\\s+${structName}\\s*\\{([^}]*)\\}`, 's').exec(preMain);
|
|
2780
|
+
if (!structMatch) return null;
|
|
2781
|
+
const fieldMatch = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`, 's').exec(structMatch[1]);
|
|
2782
|
+
return fieldMatch ? fieldMatch[1] : null;
|
|
2783
|
+
};
|
|
2784
|
+
|
|
2785
|
+
const appendHookParams = (params, additionalParams) => {
|
|
2786
|
+
if (additionalParams.length === 0) return params;
|
|
2787
|
+
const hasParams = !/^\(\s*\)$/.test(params);
|
|
2788
|
+
return `${params.slice(0, -1)}${hasParams ? ', ' : ''}${additionalParams.join(', ')})`;
|
|
2789
|
+
};
|
|
2790
|
+
|
|
2791
|
+
let hookExtraParams = [];
|
|
2792
|
+
let hookExtraArgs = [];
|
|
2793
|
+
|
|
2794
|
+
if (shaderType === 'vertex') {
|
|
2795
|
+
const ensuredInstance = ensureBuiltinParam(main, 'instance_index', 'instanceID', 'u32');
|
|
2796
|
+
main = ensuredInstance.mainSrc;
|
|
2797
|
+
|
|
2798
|
+
const ensuredVertex = ensureBuiltinParam(main, 'vertex_index', '_p5VertexId', 'u32');
|
|
2799
|
+
main = ensuredVertex.mainSrc;
|
|
2800
|
+
|
|
2801
|
+
hookExtraParams = ['instanceID: u32', '_p5VertexId: u32'];
|
|
2802
|
+
hookExtraArgs = [ensuredInstance.argName, ensuredVertex.argName];
|
|
2803
|
+
} else if (shaderType === 'fragment') {
|
|
2804
|
+
const directPositionArg = getBuiltinParamName(main, 'position');
|
|
2805
|
+
let fragmentPositionArg = directPositionArg;
|
|
2806
|
+
|
|
2807
|
+
if (!fragmentPositionArg) {
|
|
2808
|
+
const mainStructParam = getMainStructParameter(main);
|
|
2809
|
+
if (mainStructParam) {
|
|
2810
|
+
const positionField = getStructBuiltinFieldName(mainStructParam.structName, 'position');
|
|
2811
|
+
if (positionField) {
|
|
2812
|
+
fragmentPositionArg = `${mainStructParam.inputName}.${positionField}`;
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
if (!fragmentPositionArg) {
|
|
2818
|
+
const ensuredPosition = ensureBuiltinParam(main, 'position', '_p5FragPos', 'vec4<f32>');
|
|
2819
|
+
main = ensuredPosition.mainSrc;
|
|
2820
|
+
fragmentPositionArg = ensuredPosition.argName;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
hookExtraParams = ['_p5FragPos: vec4<f32>'];
|
|
2824
|
+
hookExtraArgs = [fragmentPositionArg];
|
|
2825
|
+
} else if (shaderType === 'compute') {
|
|
2826
|
+
const ensuredGlobalId = ensureBuiltinParam(main, 'global_invocation_id', '_p5GlobalId', 'vec3<u32>');
|
|
2827
|
+
main = ensuredGlobalId.mainSrc;
|
|
2828
|
+
|
|
2829
|
+
hookExtraParams = ['_p5GlobalId: vec3<u32>'];
|
|
2830
|
+
hookExtraArgs = [ensuredGlobalId.argName];
|
|
2256
2831
|
}
|
|
2257
2832
|
|
|
2258
2833
|
// Inject hook uniforms as a separate struct at a new binding
|
|
@@ -2272,6 +2847,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
2272
2847
|
const nextBinding = this.getNextBindingIndex({
|
|
2273
2848
|
vert: shaderType === 'vertex' ? preMain + (shader.hooks.vertex?.declarations ?? '') + shader.hooks.declarations : shader._vertSrc,
|
|
2274
2849
|
frag: shaderType === 'fragment' ? preMain + (shader.hooks.fragment?.declarations ?? '') + shader.hooks.declarations : shader._fragSrc,
|
|
2850
|
+
compute: shaderType === 'compute' ? preMain + (shader.hooks.compute?.declarations ?? '') + shader.hooks.declarations : shader._computeSrc,
|
|
2275
2851
|
}, 0);
|
|
2276
2852
|
|
|
2277
2853
|
// Create HookUniforms struct and binding
|
|
@@ -2282,8 +2858,14 @@ ${hookUniformFields}}
|
|
|
2282
2858
|
|
|
2283
2859
|
@group(0) @binding(${nextBinding}) var<uniform> hooks: HookUniforms;
|
|
2284
2860
|
`;
|
|
2285
|
-
// Insert before the first @group binding
|
|
2286
|
-
|
|
2861
|
+
// Insert before the first @group binding, or at the end if there are none
|
|
2862
|
+
const replaced = preMain.replace(/(@group\(0\)\s+@binding)/, `${hookUniformsDecl}\n$1`);
|
|
2863
|
+
if (replaced === preMain) {
|
|
2864
|
+
// No @group bindings found in base shader, append to preMain
|
|
2865
|
+
preMain = preMain + '\n' + hookUniformsDecl;
|
|
2866
|
+
} else {
|
|
2867
|
+
preMain = replaced;
|
|
2868
|
+
}
|
|
2287
2869
|
}
|
|
2288
2870
|
|
|
2289
2871
|
// Handle varying variables by injecting them into VertexOutput and FragmentInput structs
|
|
@@ -2349,10 +2931,9 @@ ${hookUniformFields}}
|
|
|
2349
2931
|
initStatements += ` ${varName} = INPUT_VAR.${varName};\n`;
|
|
2350
2932
|
}
|
|
2351
2933
|
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
const inputVarName = inputMatch[1];
|
|
2934
|
+
const mainStructParam = getMainStructParameter(main);
|
|
2935
|
+
if (mainStructParam) {
|
|
2936
|
+
const inputVarName = mainStructParam.inputName;
|
|
2356
2937
|
initStatements = initStatements.replace(/INPUT_VAR/g, inputVarName);
|
|
2357
2938
|
// Insert after the main function parameter but before any other code (anchored to start)
|
|
2358
2939
|
postMain = initStatements + postMain;
|
|
@@ -2360,12 +2941,56 @@ ${hookUniformFields}}
|
|
|
2360
2941
|
}
|
|
2361
2942
|
}
|
|
2362
2943
|
|
|
2944
|
+
// Handle instanceID varying for fragment access
|
|
2945
|
+
if (shader.hooks.instanceIDVarying) {
|
|
2946
|
+
const { name, declaration, source, interpolation } = shader.hooks.instanceIDVarying;
|
|
2947
|
+
const nextLocIndex = this._getNextAvailableLocation(preMain, shaderType);
|
|
2948
|
+
const interpAttr = interpolation ? ` @interpolate(${interpolation})` : '';
|
|
2949
|
+
const [varName, varType] = declaration.split(':').map(s => s.trim());
|
|
2950
|
+
const structMember = `@location(${nextLocIndex})${interpAttr} ${declaration},`;
|
|
2951
|
+
|
|
2952
|
+
if (shaderType === 'vertex') {
|
|
2953
|
+
// Inject into VertexOutput struct
|
|
2954
|
+
preMain = preMain.replace(
|
|
2955
|
+
/struct\s+VertexOutput\s+\{([^}]*)\}/,
|
|
2956
|
+
(match, body) => `struct VertexOutput {${body}\n${structMember}}`
|
|
2957
|
+
);
|
|
2958
|
+
// Add private global
|
|
2959
|
+
preMain += `var<private> ${declaration};\n`;
|
|
2960
|
+
// Assign from built-in instanceID at start of main()
|
|
2961
|
+
postMain = `\n ${varName} = ${source};\n` + postMain;
|
|
2962
|
+
// Copy to output struct before return
|
|
2963
|
+
const returnMatch = postMain.match(/return\s+(\w+)\s*;/);
|
|
2964
|
+
if (returnMatch) {
|
|
2965
|
+
const outputVarName = returnMatch[1];
|
|
2966
|
+
postMain = postMain.replace(
|
|
2967
|
+
/(return\s+\w+\s*;)/g,
|
|
2968
|
+
`${outputVarName}.${varName} = ${varName};\n $1`
|
|
2969
|
+
);
|
|
2970
|
+
}
|
|
2971
|
+
} else if (shaderType === 'fragment') {
|
|
2972
|
+
// Inject into FragmentInput struct
|
|
2973
|
+
preMain = preMain.replace(
|
|
2974
|
+
/struct\s+FragmentInput\s+\{([^}]*)\}/,
|
|
2975
|
+
(match, body) => `struct FragmentInput {${body}\n${structMember}}`
|
|
2976
|
+
);
|
|
2977
|
+
// Add private global
|
|
2978
|
+
preMain += `var<private> ${declaration};\n`;
|
|
2979
|
+
// Initialize from input struct at start of main()
|
|
2980
|
+
const mainStructParam = getMainStructParameter(main);
|
|
2981
|
+
if (mainStructParam) {
|
|
2982
|
+
const inputVarName = mainStructParam.inputName;
|
|
2983
|
+
postMain = `\n ${varName} = ${inputVarName}.${varName};\n` + postMain;
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2363
2988
|
let hooks = '';
|
|
2364
2989
|
let defines = '';
|
|
2365
2990
|
if (shader.hooks.declarations) {
|
|
2366
2991
|
hooks += shader.hooks.declarations + '\n';
|
|
2367
2992
|
}
|
|
2368
|
-
if (shader.hooks[shaderType].declarations) {
|
|
2993
|
+
if (shader.hooks[shaderType] && shader.hooks[shaderType].declarations) {
|
|
2369
2994
|
hooks += shader.hooks[shaderType].declarations + '\n';
|
|
2370
2995
|
}
|
|
2371
2996
|
for (const hookDef in shader.hooks.helpers) {
|
|
@@ -2389,11 +3014,7 @@ ${hookUniformFields}}
|
|
|
2389
3014
|
|
|
2390
3015
|
let [_, params, body] = /^(\([^\)]*\))((?:.|\n)*)$/.exec(shader.hooks[shaderType][hookDef]);
|
|
2391
3016
|
|
|
2392
|
-
|
|
2393
|
-
// Splice the instance ID in as a final parameter to every WGSL hook function
|
|
2394
|
-
let hasParams = !!params.match(/^\(\s*\S+.*\)$/);
|
|
2395
|
-
params = params.slice(0, -1) + (hasParams ? ', ' : '') + 'instanceID: u32)';
|
|
2396
|
-
}
|
|
3017
|
+
params = appendHookParams(params, hookExtraParams);
|
|
2397
3018
|
|
|
2398
3019
|
if (hookType === 'void') {
|
|
2399
3020
|
hooks += `fn HOOK_${hookName}${params}${body}\n`;
|
|
@@ -2402,40 +3023,45 @@ ${hookUniformFields}}
|
|
|
2402
3023
|
}
|
|
2403
3024
|
}
|
|
2404
3025
|
|
|
2405
|
-
//
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
if (nesting === 0) {
|
|
2427
|
-
break;
|
|
2428
|
-
}
|
|
3026
|
+
// Pass stage-specific builtins from main to each hook call.
|
|
3027
|
+
// Collect ALL HOOK_ calls (including nested ones) then insert
|
|
3028
|
+
// extra args from right to left so position shifts don't
|
|
3029
|
+
// invalidate earlier insertion points.
|
|
3030
|
+
if (hookExtraArgs.length > 0) {
|
|
3031
|
+
const addHookArgs = (src) => {
|
|
3032
|
+
const insertions = [];
|
|
3033
|
+
let searchIdx = 0;
|
|
3034
|
+
let m;
|
|
3035
|
+
while ((m = /HOOK_\w+\(/.exec(src.slice(searchIdx))) !== null) {
|
|
3036
|
+
const openParen = searchIdx + m.index + m[0].length - 1;
|
|
3037
|
+
let pos = openParen + 1;
|
|
3038
|
+
let nesting = 1;
|
|
3039
|
+
let hasParams = false;
|
|
3040
|
+
while (pos < src.length && nesting > 0) {
|
|
3041
|
+
if (src[pos] === '(') nesting++;
|
|
3042
|
+
else if (src[pos] === ')') {
|
|
3043
|
+
nesting--;
|
|
3044
|
+
if (nesting === 0) break;
|
|
3045
|
+
} else if (/\S/.test(src[pos])) {
|
|
3046
|
+
hasParams = true;
|
|
2429
3047
|
}
|
|
2430
|
-
|
|
2431
|
-
result = result.slice(0, idx-1) + insertion + result.slice(idx-1);
|
|
2432
|
-
idx += insertion.length;
|
|
3048
|
+
pos++;
|
|
2433
3049
|
}
|
|
2434
|
-
|
|
3050
|
+
insertions.push({ pos, hasParams });
|
|
3051
|
+
searchIdx = openParen + 1;
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
insertions.sort((a, b) => b.pos - a.pos);
|
|
3055
|
+
|
|
3056
|
+
let result = src;
|
|
3057
|
+
for (const { pos, hasParams } of insertions) {
|
|
3058
|
+
const insertion = (hasParams ? ', ' : '') + hookExtraArgs.join(', ');
|
|
3059
|
+
result = result.slice(0, pos) + insertion + result.slice(pos);
|
|
3060
|
+
}
|
|
2435
3061
|
return result;
|
|
2436
3062
|
};
|
|
2437
|
-
preMain =
|
|
2438
|
-
postMain =
|
|
3063
|
+
preMain = addHookArgs(preMain);
|
|
3064
|
+
postMain = addHookArgs(postMain);
|
|
2439
3065
|
}
|
|
2440
3066
|
|
|
2441
3067
|
return preMain + '\n' + defines + hooks + main + postMain;
|
|
@@ -2494,6 +3120,10 @@ ${hookUniformFields}}
|
|
|
2494
3120
|
body = shader.hooks.fragment[hookName];
|
|
2495
3121
|
fullSrc = shader._fragSrc;
|
|
2496
3122
|
}
|
|
3123
|
+
if (!body) {
|
|
3124
|
+
body = shader.hooks.compute[hookName];
|
|
3125
|
+
fullSrc = shader._computeSrc;
|
|
3126
|
+
}
|
|
2497
3127
|
if (!body) {
|
|
2498
3128
|
throw new Error(`Can't find hook ${hookName}!`);
|
|
2499
3129
|
}
|
|
@@ -2625,7 +3255,7 @@ ${hookUniformFields}}
|
|
|
2625
3255
|
}
|
|
2626
3256
|
|
|
2627
3257
|
defaultFramebufferAntialias() {
|
|
2628
|
-
return
|
|
3258
|
+
return this._pInst._webgpuAttributes?.antialias !== false;
|
|
2629
3259
|
}
|
|
2630
3260
|
|
|
2631
3261
|
supportsFramebufferAntialias() {
|
|
@@ -2818,6 +3448,267 @@ ${hookUniformFields}}
|
|
|
2818
3448
|
};
|
|
2819
3449
|
}
|
|
2820
3450
|
|
|
3451
|
+
// Maps a plain JS value to the WGSL type string that represents it in a struct.
|
|
3452
|
+
_jsValueToWgslType(value) {
|
|
3453
|
+
if (typeof value === 'number') return 'f32';
|
|
3454
|
+
// Duck typing instead of instanceof to avoid importing a separate
|
|
3455
|
+
// copy of the Color/Vector classes
|
|
3456
|
+
if (value?.isVector) {
|
|
3457
|
+
if (value.dimensions === 2) return 'vec2f';
|
|
3458
|
+
if (value.dimensions === 3) return 'vec3f';
|
|
3459
|
+
if (value.dimensions === 4) return 'vec4f';
|
|
3460
|
+
throw new Error(`Unsupported vector dimension ${value.dimensions} for struct storage field`);
|
|
3461
|
+
}
|
|
3462
|
+
if (value?.isColor) {
|
|
3463
|
+
return 'vec4f';
|
|
3464
|
+
}
|
|
3465
|
+
if (Array.isArray(value)) {
|
|
3466
|
+
if (value.length === 2) return 'vec2f';
|
|
3467
|
+
if (value.length === 3) return 'vec3f';
|
|
3468
|
+
if (value.length === 4) return 'vec4f';
|
|
3469
|
+
throw new Error(`Unsupported array length ${value.length} for struct storage field`);
|
|
3470
|
+
}
|
|
3471
|
+
throw new Error(`Unsupported value type ${typeof value} for struct storage field`);
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
// Infers a struct schema from the first element of a struct array.
|
|
3475
|
+
//
|
|
3476
|
+
// Returns { fields, stride, structBody } where:
|
|
3477
|
+
// fields: field has the _packField interface (baseType, size, offset, packInPlace) plus:
|
|
3478
|
+
// name: string - JS property name
|
|
3479
|
+
// dim: number - float component count, used when creating StrandsNodes
|
|
3480
|
+
// structBody: everything inside the { ... } of a WGSL struct definition
|
|
3481
|
+
// stride: how many bytes are reserved for this struct in the buffer
|
|
3482
|
+
_inferStructSchema(firstElement) {
|
|
3483
|
+
const entries = Object.entries(firstElement);
|
|
3484
|
+
|
|
3485
|
+
if (!p5.disableFriendlyErrors) {
|
|
3486
|
+
for (const [name, value] of entries) {
|
|
3487
|
+
if (
|
|
3488
|
+
value !== null &&
|
|
3489
|
+
typeof value === 'object' &&
|
|
3490
|
+
!Array.isArray(value) &&
|
|
3491
|
+
// Duck typing instead of instanceof to avoid importing a separate
|
|
3492
|
+
// copy of the Color/Vector classes
|
|
3493
|
+
!value?.isVector &&
|
|
3494
|
+
!value?.isColor
|
|
3495
|
+
) {
|
|
3496
|
+
p5._friendlyError(
|
|
3497
|
+
`The "${name}" property in your storage data contains a nested object. ` +
|
|
3498
|
+
`Make sure you only use properties with numbers, arrays of numbers, or p5.Vector.`,
|
|
3499
|
+
'createStorage'
|
|
3500
|
+
);
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
const fieldLines = entries.map(([name, value]) =>
|
|
3506
|
+
` ${name}: ${this._jsValueToWgslType(value)},`
|
|
3507
|
+
).join('\n');
|
|
3508
|
+
const structBody = `{\n${fieldLines}\n}`;
|
|
3509
|
+
const elements = this._parseStruct(`struct _Tmp ${structBody}`, '_Tmp');
|
|
3510
|
+
|
|
3511
|
+
let maxEnd = 0;
|
|
3512
|
+
let maxAlign = 1;
|
|
3513
|
+
const fields = entries.map(([name, value]) => {
|
|
3514
|
+
const el = elements[name];
|
|
3515
|
+
maxEnd = Math.max(maxEnd, el.offsetEnd);
|
|
3516
|
+
// Alignment for scalars/vectors: <=4 -> 4, <=8 -> 8, else 16
|
|
3517
|
+
const align = el.size <= 4 ? 4 : el.size <= 8 ? 8 : 16;
|
|
3518
|
+
maxAlign = Math.max(maxAlign, align);
|
|
3519
|
+
// Track original JS type for reconstruction during readback
|
|
3520
|
+
const kind = value?.isVector ? 'vector'
|
|
3521
|
+
: value?.isColor ? 'color'
|
|
3522
|
+
: undefined;
|
|
3523
|
+
return {
|
|
3524
|
+
name,
|
|
3525
|
+
baseType: el.baseType,
|
|
3526
|
+
size: el.size,
|
|
3527
|
+
offset: el.offset,
|
|
3528
|
+
packInPlace: el.packInPlace ?? false,
|
|
3529
|
+
dim: el.size / 4,
|
|
3530
|
+
kind,
|
|
3531
|
+
};
|
|
3532
|
+
});
|
|
3533
|
+
|
|
3534
|
+
const stride = Math.ceil(maxEnd / maxAlign) * maxAlign;
|
|
3535
|
+
return { fields, stride, structBody };
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
// Packs an array of plain objects into a Float32Array using the given struct schema.
|
|
3539
|
+
// Reuses _packField so layout rules match uniform packing exactly.
|
|
3540
|
+
_packStructArray(data, schema) {
|
|
3541
|
+
const { fields, stride } = schema;
|
|
3542
|
+
const totalBytes = Math.max(data.length * stride, 16);
|
|
3543
|
+
const alignedBytes = Math.ceil(totalBytes / 16) * 16;
|
|
3544
|
+
const buffer = new ArrayBuffer(alignedBytes);
|
|
3545
|
+
const floatView = new Float32Array(buffer);
|
|
3546
|
+
const dataView = new DataView(buffer);
|
|
3547
|
+
for (let i = 0; i < data.length; i++) {
|
|
3548
|
+
const item = data[i];
|
|
3549
|
+
const baseOffset = i * stride;
|
|
3550
|
+
for (const field of fields) {
|
|
3551
|
+
this._packField(field, item[field.name], floatView, dataView, baseOffset);
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
return floatView;
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
// Inverse of _packStructArray reads packed buffer back into plain JS objects
|
|
3558
|
+
// using the same schema layout - fields, stride and offsets
|
|
3559
|
+
_unpackStructArray(floatView, schema) {
|
|
3560
|
+
const { fields, stride } = schema;
|
|
3561
|
+
const dataView = new DataView(floatView.buffer);
|
|
3562
|
+
const count = Math.floor(floatView.byteLength / stride);
|
|
3563
|
+
const result = [];
|
|
3564
|
+
|
|
3565
|
+
for (let i = 0; i < count; i++) {
|
|
3566
|
+
const item = {};
|
|
3567
|
+
const baseOffset = i * stride;
|
|
3568
|
+
for (const field of fields) {
|
|
3569
|
+
const byteOffset = baseOffset + field.offset;
|
|
3570
|
+
const n = field.size / 4;
|
|
3571
|
+
|
|
3572
|
+
if (field.baseType === 'u32') {
|
|
3573
|
+
if (n === 1) {
|
|
3574
|
+
item[field.name] = dataView.getUint32(byteOffset, true);
|
|
3575
|
+
} else {
|
|
3576
|
+
item[field.name] = Array.from({ length: n }, (_, j) =>
|
|
3577
|
+
dataView.getUint32(byteOffset + j * 4, true)
|
|
3578
|
+
);
|
|
3579
|
+
}
|
|
3580
|
+
} else if (field.baseType === 'i32') {
|
|
3581
|
+
if (n === 1) {
|
|
3582
|
+
item[field.name] = dataView.getInt32(byteOffset, true);
|
|
3583
|
+
} else {
|
|
3584
|
+
item[field.name] = Array.from({ length: n }, (_, j) =>
|
|
3585
|
+
dataView.getInt32(byteOffset + j * 4, true)
|
|
3586
|
+
);
|
|
3587
|
+
}
|
|
3588
|
+
} else {
|
|
3589
|
+
const idx = byteOffset / 4;
|
|
3590
|
+
if (n === 1) {
|
|
3591
|
+
item[field.name] = floatView[idx];
|
|
3592
|
+
} else {
|
|
3593
|
+
const values = Array.from(floatView.slice(idx, idx + n));
|
|
3594
|
+
if (field.kind === 'vector') {
|
|
3595
|
+
item[field.name] = this._pInst.createVector(...values);
|
|
3596
|
+
} else if (field.kind === 'color') {
|
|
3597
|
+
// Color was packed as normalized RGBA [0-1] via _getRGBA([1,1,1,1])
|
|
3598
|
+
// Scale back to the current colorMode range
|
|
3599
|
+
const maxes = this.states.colorMaxes[this.states.colorMode];
|
|
3600
|
+
item[field.name] = this._pInst.color(
|
|
3601
|
+
values[0] * maxes[0], values[1] * maxes[1],
|
|
3602
|
+
values[2] * maxes[2], values[3] * maxes[3]
|
|
3603
|
+
);
|
|
3604
|
+
} else {
|
|
3605
|
+
item[field.name] = values;
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
result.push(item);
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
return result;
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
createStorage(dataOrCount) {
|
|
3617
|
+
const device = this.device;
|
|
3618
|
+
|
|
3619
|
+
// Struct array: an array of plain objects
|
|
3620
|
+
if (Array.isArray(dataOrCount) && dataOrCount.length > 0 &&
|
|
3621
|
+
typeof dataOrCount[0] === 'object' && !Array.isArray(dataOrCount[0])) {
|
|
3622
|
+
if (!p5.disableFriendlyErrors && dataOrCount.length > 1) {
|
|
3623
|
+
const firstKeys = Object.keys(dataOrCount[0]);
|
|
3624
|
+
let warned = false;
|
|
3625
|
+
for (let i = 1; i < dataOrCount.length; i++) {
|
|
3626
|
+
const el = dataOrCount[i];
|
|
3627
|
+
const elKeys = Object.keys(el);
|
|
3628
|
+
const sameKeys = firstKeys.length === elKeys.length &&
|
|
3629
|
+
firstKeys.every((k, j) => k === elKeys[j]);
|
|
3630
|
+
if (!sameKeys) {
|
|
3631
|
+
p5._friendlyError(
|
|
3632
|
+
`Element ${i} has different fields than element 0. ` +
|
|
3633
|
+
`All elements should have the same properties.`,
|
|
3634
|
+
'createStorage'
|
|
3635
|
+
);
|
|
3636
|
+
break;
|
|
3637
|
+
}
|
|
3638
|
+
for (const key of firstKeys) {
|
|
3639
|
+
const firstType = this._jsValueToWgslType(dataOrCount[0][key]);
|
|
3640
|
+
const elType = this._jsValueToWgslType(el[key]);
|
|
3641
|
+
if (firstType !== elType) {
|
|
3642
|
+
p5._friendlyError(
|
|
3643
|
+
`The "${key}" property of element ${i} has type ${elType} ` +
|
|
3644
|
+
`but element 0 has type ${firstType}. Proporties should have the same type across all elements.`,
|
|
3645
|
+
'createStorage'
|
|
3646
|
+
);
|
|
3647
|
+
warned = true;
|
|
3648
|
+
break;
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
if (warned) break;
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
const schema = this._inferStructSchema(dataOrCount[0]);
|
|
3655
|
+
const packed = this._packStructArray(dataOrCount, schema);
|
|
3656
|
+
const size = packed.byteLength;
|
|
3657
|
+
const buffer = device.createBuffer({
|
|
3658
|
+
size,
|
|
3659
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
|
3660
|
+
mappedAtCreation: true,
|
|
3661
|
+
});
|
|
3662
|
+
new Float32Array(buffer.getMappedRange()).set(packed);
|
|
3663
|
+
buffer.unmap();
|
|
3664
|
+
const storageBuffer = new StorageBuffer(buffer, size, this, schema);
|
|
3665
|
+
this._storageBuffers.add(storageBuffer);
|
|
3666
|
+
return storageBuffer;
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
// Determine buffer size and initial data
|
|
3670
|
+
let size, initialData;
|
|
3671
|
+
if (typeof dataOrCount === 'number') {
|
|
3672
|
+
// createStorage(count) - zero-initialized
|
|
3673
|
+
size = dataOrCount * 4; // floats are 4 bytes
|
|
3674
|
+
initialData = new Float32Array(dataOrCount);
|
|
3675
|
+
} else {
|
|
3676
|
+
// createStorage(array) - from data
|
|
3677
|
+
if (dataOrCount instanceof Float32Array) {
|
|
3678
|
+
initialData = dataOrCount;
|
|
3679
|
+
} else if (Array.isArray(dataOrCount)) {
|
|
3680
|
+
initialData = new Float32Array(dataOrCount);
|
|
3681
|
+
} else {
|
|
3682
|
+
throw new Error('createStorage expects a number or array/Float32Array');
|
|
3683
|
+
}
|
|
3684
|
+
size = initialData.byteLength;
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
// Align to 16 bytes (WGSL storage buffer alignment requirement)
|
|
3688
|
+
size = Math.ceil(size / 16) * 16;
|
|
3689
|
+
|
|
3690
|
+
// Create storage buffer with STORAGE | COPY_DST | COPY_SRC usage
|
|
3691
|
+
const buffer = device.createBuffer({
|
|
3692
|
+
size,
|
|
3693
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
|
3694
|
+
mappedAtCreation: initialData.length > 0
|
|
3695
|
+
});
|
|
3696
|
+
|
|
3697
|
+
// Write initial data if provided
|
|
3698
|
+
if (initialData.length > 0) {
|
|
3699
|
+
const mapping = new Float32Array(buffer.getMappedRange());
|
|
3700
|
+
mapping.set(initialData);
|
|
3701
|
+
buffer.unmap();
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
const storageBuffer = new StorageBuffer(buffer, size, this);
|
|
3705
|
+
|
|
3706
|
+
// Track for cleanup
|
|
3707
|
+
this._storageBuffers.add(storageBuffer);
|
|
3708
|
+
|
|
3709
|
+
return storageBuffer;
|
|
3710
|
+
}
|
|
3711
|
+
|
|
2821
3712
|
_getWebGPUColorFormat(framebuffer) {
|
|
2822
3713
|
if (framebuffer.format === FLOAT) {
|
|
2823
3714
|
return framebuffer.channels === RGBA ? 'rgba32float' : 'rgba32float';
|
|
@@ -3114,10 +4005,6 @@ ${hookUniformFields}}
|
|
|
3114
4005
|
return super.filter(...args);
|
|
3115
4006
|
}
|
|
3116
4007
|
|
|
3117
|
-
getNoiseShaderSnippet() {
|
|
3118
|
-
return noiseWGSL;
|
|
3119
|
-
}
|
|
3120
|
-
|
|
3121
4008
|
|
|
3122
4009
|
baseFilterShader() {
|
|
3123
4010
|
if (!this._baseFilterShader) {
|
|
@@ -3141,6 +4028,21 @@ ${hookUniformFields}}
|
|
|
3141
4028
|
return this._baseFilterShader;
|
|
3142
4029
|
}
|
|
3143
4030
|
|
|
4031
|
+
baseComputeShader() {
|
|
4032
|
+
if (!this._baseComputeShader) {
|
|
4033
|
+
this._baseComputeShader = new Shader(
|
|
4034
|
+
this,
|
|
4035
|
+
baseComputeShader,
|
|
4036
|
+
{
|
|
4037
|
+
compute: {
|
|
4038
|
+
'void iteration': '(index: vec3<i32>) {}',
|
|
4039
|
+
},
|
|
4040
|
+
}
|
|
4041
|
+
);
|
|
4042
|
+
}
|
|
4043
|
+
return this._baseComputeShader;
|
|
4044
|
+
}
|
|
4045
|
+
|
|
3144
4046
|
/*
|
|
3145
4047
|
* WebGPU-specific implementation of imageLight shader creation
|
|
3146
4048
|
*/
|
|
@@ -3240,6 +4142,69 @@ ${hookUniformFields}}
|
|
|
3240
4142
|
glDataType: dataType || 'uint8'
|
|
3241
4143
|
};
|
|
3242
4144
|
}
|
|
4145
|
+
|
|
4146
|
+
compute(shader, x, y = 1, z = 1) {
|
|
4147
|
+
if (shader.shaderType !== 'compute') {
|
|
4148
|
+
throw new Error('compute() can only be called with a compute shader');
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
this._finishActiveRenderPass();
|
|
4152
|
+
|
|
4153
|
+
// Ensure shader is initialized and finalized
|
|
4154
|
+
if (!shader._compiled) {
|
|
4155
|
+
shader.init();
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
// Set default uniforms
|
|
4159
|
+
shader.setDefaultUniforms();
|
|
4160
|
+
shader.setUniform('uTotalCount', [x, y, z]);
|
|
4161
|
+
|
|
4162
|
+
// Calculate optimal workgroup size (8x8x1 = 64 threads per workgroup)
|
|
4163
|
+
const WORKGROUP_SIZE_X = 8;
|
|
4164
|
+
const WORKGROUP_SIZE_Y = 8;
|
|
4165
|
+
const WORKGROUP_SIZE_Z = 1;
|
|
4166
|
+
|
|
4167
|
+
// auto spreading: if any dimension is too large or for performance optimization,
|
|
4168
|
+
// spread total iteration count across dimensions
|
|
4169
|
+
const totalIterations = x * y * z;
|
|
4170
|
+
const MAX_THREADS_PER_DIM = 65535 * 8;
|
|
4171
|
+
|
|
4172
|
+
let px = x;
|
|
4173
|
+
let py = y;
|
|
4174
|
+
let pz = z;
|
|
4175
|
+
|
|
4176
|
+
// we spread if we exceed GPU limits OR if it involves a large 1D dispatch
|
|
4177
|
+
const exceedsLimits = x > MAX_THREADS_PER_DIM || y > MAX_THREADS_PER_DIM || z > MAX_THREADS_PER_DIM;
|
|
4178
|
+
const isLarge1D = totalIterations > 1024 && y === 1 && z === 1;
|
|
4179
|
+
|
|
4180
|
+
if (exceedsLimits || isLarge1D) {
|
|
4181
|
+
// Always use 2D square spreading (√N × √N).
|
|
4182
|
+
// Benchmarks showed 2D square equals or outperforms 3D cube at every
|
|
4183
|
+
// scale tested, with simpler index reconstruction in the shader.
|
|
4184
|
+
px = Math.ceil(Math.sqrt(totalIterations));
|
|
4185
|
+
py = Math.ceil(totalIterations / px);
|
|
4186
|
+
pz = 1;
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
shader.setUniform('uPhysicalCount', [px, py, pz]);
|
|
4190
|
+
|
|
4191
|
+
const workgroupCountX = Math.ceil(px / WORKGROUP_SIZE_X);
|
|
4192
|
+
const workgroupCountY = Math.ceil(py / WORKGROUP_SIZE_Y);
|
|
4193
|
+
const workgroupCountZ = Math.ceil(pz / WORKGROUP_SIZE_Z);
|
|
4194
|
+
|
|
4195
|
+
const commandEncoder = this.device.createCommandEncoder();
|
|
4196
|
+
const passEncoder = commandEncoder.beginComputePass();
|
|
4197
|
+
this.setupShaderBindGroups(shader, passEncoder, {
|
|
4198
|
+
compute: true,
|
|
4199
|
+
workgroupSize: [WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y, WORKGROUP_SIZE_Z],
|
|
4200
|
+
});
|
|
4201
|
+
|
|
4202
|
+
// Dispatch compute workgroups
|
|
4203
|
+
passEncoder.dispatchWorkgroups(workgroupCountX, workgroupCountY, workgroupCountZ);
|
|
4204
|
+
|
|
4205
|
+
passEncoder.end();
|
|
4206
|
+
this.device.queue.submit([commandEncoder.finish()]);
|
|
4207
|
+
}
|
|
3243
4208
|
}
|
|
3244
4209
|
|
|
3245
4210
|
p5.RendererWebGPU = RendererWebGPU;
|
|
@@ -3250,6 +4215,7 @@ ${hookUniformFields}}
|
|
|
3250
4215
|
fn.setAttributes = async function (key, value) {
|
|
3251
4216
|
return this._renderer._setAttributes(key, value);
|
|
3252
4217
|
};
|
|
4218
|
+
|
|
3253
4219
|
}
|
|
3254
4220
|
|
|
3255
4221
|
if (typeof p5 !== "undefined") {
|