p5 2.2.3 → 2.3.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-CYF6mp5_.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-D2MtO721.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 +233 -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-C0Kzy71d.js} +31 -24
- package/dist/{rendering-CC8JNTwG.js → rendering-CvNr0bB8.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 +1036 -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 +4088 -1950
- package/lib/p5.esm.min.js +1 -1
- package/lib/p5.js +4088 -1950
- package/lib/p5.min.js +1 -1
- package/lib/p5.webgpu.esm.js +1638 -306
- package/lib/p5.webgpu.js +1637 -305
- package/lib/p5.webgpu.min.js +1 -1
- package/package.json +6 -1
- package/types/global.d.ts +4137 -2396
- package/types/p5.d.ts +2702 -1658
- 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-CYF6mp5_.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,228 @@ 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
|
+
/**
|
|
260
|
+
* A block of data that shaders can read from, and compute shaders can also
|
|
261
|
+
* write to. This is only available in WebGPU mode.
|
|
262
|
+
*
|
|
263
|
+
* Note: <a href="#/p5/createStorage">`createStorage()`</a> is the recommended
|
|
264
|
+
* way to create an instance of this class.
|
|
265
|
+
*
|
|
266
|
+
* @class p5.StorageBuffer
|
|
267
|
+
* @beta
|
|
268
|
+
* @webgpu
|
|
269
|
+
* @webgpuOnly
|
|
270
|
+
*/
|
|
271
|
+
p5.StorageBuffer = StorageBuffer;
|
|
272
|
+
|
|
40
273
|
class RendererWebGPU extends Renderer3D {
|
|
41
274
|
constructor(pInst, w, h, isMainCanvas, elt) {
|
|
42
275
|
super(pInst, w, h, isMainCanvas, elt);
|
|
@@ -88,6 +321,9 @@ function rendererWebGPU(p5, fn) {
|
|
|
88
321
|
// Retired buffers to destroy at end of frame
|
|
89
322
|
this._retiredBuffers = [];
|
|
90
323
|
|
|
324
|
+
// Storage buffers for compute shaders
|
|
325
|
+
this._storageBuffers = new Set();
|
|
326
|
+
|
|
91
327
|
// 2D canvas for pixel reading fallback
|
|
92
328
|
this._pixelReadCanvas = null;
|
|
93
329
|
this._pixelReadCtx = null;
|
|
@@ -164,7 +400,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
164
400
|
}
|
|
165
401
|
if (this._pInst._webgpuAttributes[key] !== value) {
|
|
166
402
|
//changing value of previously altered attribute
|
|
167
|
-
this._webgpuAttributes[key] = value;
|
|
403
|
+
this._pInst._webgpuAttributes[key] = value;
|
|
168
404
|
unchanged = false;
|
|
169
405
|
}
|
|
170
406
|
//setting all attributes with some change
|
|
@@ -298,9 +534,21 @@ function rendererWebGPU(p5, fn) {
|
|
|
298
534
|
const _b = args[2] || 0;
|
|
299
535
|
const _a = args[3] || 0;
|
|
300
536
|
|
|
301
|
-
// If PENDING and no custom framebuffer, clear means stay UNPROMOTED
|
|
302
|
-
if
|
|
303
|
-
|
|
537
|
+
// If PENDING and no custom framebuffer, clear means stay UNPROMOTED.
|
|
538
|
+
// However, if we are still in setup (frameCount == 0), we must promote
|
|
539
|
+
// so that mainFramebuffer gets the cleared content. This ensures that if
|
|
540
|
+
// draw() later promotes without a copy, it starts from the correct state
|
|
541
|
+
// rather than a stale mainFramebuffer.
|
|
542
|
+
// Note: a mid-draw-loop transition from UNPROMOTED back to PROMOTED
|
|
543
|
+
// (i.e. calling background() some frames but not others) will still
|
|
544
|
+
// lose intermediate UNPROMOTED frame content.
|
|
545
|
+
if (this._frameState !== FRAME_STATE.PROMOTED && !this.activeFramebuffer()) {
|
|
546
|
+
if (this._pInst.frameCount > 0) {
|
|
547
|
+
this._frameState = FRAME_STATE.UNPROMOTED;
|
|
548
|
+
} else {
|
|
549
|
+
this._promoteToFramebufferWithoutCopy();
|
|
550
|
+
// clear() then targets mainFramebuffer via activeFramebuffer()
|
|
551
|
+
}
|
|
304
552
|
}
|
|
305
553
|
|
|
306
554
|
this._finishActiveRenderPass();
|
|
@@ -503,7 +751,8 @@ function rendererWebGPU(p5, fn) {
|
|
|
503
751
|
return 4; // Cap at 4 for broader compatibility
|
|
504
752
|
}
|
|
505
753
|
|
|
506
|
-
_shaderOptions({ mode }) {
|
|
754
|
+
_shaderOptions({ mode, compute, workgroupSize }) {
|
|
755
|
+
if (compute) return { compute: true, workgroupSize };
|
|
507
756
|
const activeFramebuffer = this.activeFramebuffer();
|
|
508
757
|
const format = activeFramebuffer ?
|
|
509
758
|
this._getWebGPUColorFormat(activeFramebuffer) :
|
|
@@ -514,9 +763,9 @@ function rendererWebGPU(p5, fn) {
|
|
|
514
763
|
1; // No MSAA needed when blitting already-antialiased textures to canvas
|
|
515
764
|
const sampleCount = this._getValidSampleCount(requestedSampleCount);
|
|
516
765
|
|
|
517
|
-
const depthFormat = activeFramebuffer
|
|
518
|
-
this._getWebGPUDepthFormat(activeFramebuffer) :
|
|
519
|
-
this.depthFormat;
|
|
766
|
+
const depthFormat = activeFramebuffer
|
|
767
|
+
? (activeFramebuffer.useDepth ? this._getWebGPUDepthFormat(activeFramebuffer) : undefined)
|
|
768
|
+
: this.depthFormat;
|
|
520
769
|
|
|
521
770
|
const drawTarget = this.drawTarget();
|
|
522
771
|
const clipping = this._clipping;
|
|
@@ -544,6 +793,31 @@ function rendererWebGPU(p5, fn) {
|
|
|
544
793
|
_initShader(shader) {
|
|
545
794
|
const device = this.device;
|
|
546
795
|
|
|
796
|
+
if (shader.shaderType === 'compute') {
|
|
797
|
+
// Compute shader initialization
|
|
798
|
+
shader.computeModule = device.createShaderModule({ code: shader.computeSrc() });
|
|
799
|
+
shader._computePipelineCache = null;
|
|
800
|
+
shader._workgroupSize = null;
|
|
801
|
+
|
|
802
|
+
// Create compute pipeline (deferred until first compute() call)
|
|
803
|
+
shader.getPipeline = ({ workgroupSize }) => {
|
|
804
|
+
if (!shader._computePipelineCache) {
|
|
805
|
+
shader._computePipelineCache = device.createComputePipeline({
|
|
806
|
+
layout: shader._pipelineLayout,
|
|
807
|
+
compute: {
|
|
808
|
+
module: shader.computeModule,
|
|
809
|
+
entryPoint: 'main'
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
shader._workgroupSize = workgroupSize;
|
|
813
|
+
}
|
|
814
|
+
return shader._computePipelineCache;
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Render shader initialization
|
|
547
821
|
shader.vertModule = device.createShaderModule({ code: shader.vertSrc() });
|
|
548
822
|
shader.fragModule = device.createShaderModule({ code: shader.fragSrc() });
|
|
549
823
|
|
|
@@ -568,25 +842,27 @@ function rendererWebGPU(p5, fn) {
|
|
|
568
842
|
},
|
|
569
843
|
primitive: { topology },
|
|
570
844
|
multisample: { count: sampleCount },
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
845
|
+
...(depthFormat ? {
|
|
846
|
+
depthStencil: {
|
|
847
|
+
format: depthFormat,
|
|
848
|
+
depthWriteEnabled: !clipping,
|
|
849
|
+
depthCompare: 'less-equal',
|
|
850
|
+
stencilFront: {
|
|
851
|
+
compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
|
|
852
|
+
failOp: 'keep',
|
|
853
|
+
depthFailOp: 'keep',
|
|
854
|
+
passOp: clipping ? 'replace' : 'keep',
|
|
855
|
+
},
|
|
856
|
+
stencilBack: {
|
|
857
|
+
compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
|
|
858
|
+
failOp: 'keep',
|
|
859
|
+
depthFailOp: 'keep',
|
|
860
|
+
passOp: clipping ? 'replace' : 'keep',
|
|
861
|
+
},
|
|
862
|
+
stencilReadMask: 0xFF,
|
|
863
|
+
stencilWriteMask: clipping ? 0xFF : 0x00,
|
|
586
864
|
},
|
|
587
|
-
|
|
588
|
-
stencilWriteMask: clipping ? 0xFF : 0x00,
|
|
589
|
-
},
|
|
865
|
+
} : {}),
|
|
590
866
|
});
|
|
591
867
|
shader._pipelineCache.set(key, pipeline);
|
|
592
868
|
}
|
|
@@ -642,7 +918,9 @@ function rendererWebGPU(p5, fn) {
|
|
|
642
918
|
entries.push({
|
|
643
919
|
bufferGroup,
|
|
644
920
|
binding: bufferGroup.binding,
|
|
645
|
-
visibility:
|
|
921
|
+
visibility: shader.shaderType === 'compute'
|
|
922
|
+
? GPUShaderStage.COMPUTE
|
|
923
|
+
: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
646
924
|
buffer: { type: 'uniform', hasDynamicOffset: bufferGroup.dynamic },
|
|
647
925
|
});
|
|
648
926
|
structEntries.set(bufferGroup.group, entries);
|
|
@@ -676,6 +954,24 @@ function rendererWebGPU(p5, fn) {
|
|
|
676
954
|
groupEntries.set(group, entries);
|
|
677
955
|
}
|
|
678
956
|
|
|
957
|
+
// Add storage buffer bindings
|
|
958
|
+
for (const storage of shader._storageBuffers || []) {
|
|
959
|
+
const group = storage.group;
|
|
960
|
+
const entries = groupEntries.get(group) || [];
|
|
961
|
+
|
|
962
|
+
entries.push({
|
|
963
|
+
binding: storage.binding,
|
|
964
|
+
visibility: storage.visibility,
|
|
965
|
+
buffer: {
|
|
966
|
+
type: storage.accessMode === 'read' ? 'read-only-storage' : 'storage'
|
|
967
|
+
},
|
|
968
|
+
storage: storage,
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
entries.sort((a, b) => a.binding - b.binding);
|
|
972
|
+
groupEntries.set(group, entries);
|
|
973
|
+
}
|
|
974
|
+
|
|
679
975
|
// Create layouts and bind groups
|
|
680
976
|
const groupEntriesArr = [];
|
|
681
977
|
for (const [group, entries] of groupEntries) {
|
|
@@ -694,6 +990,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
694
990
|
shader._pipelineLayout = this.device.createPipelineLayout({
|
|
695
991
|
bindGroupLayouts: shader._bindGroupLayouts,
|
|
696
992
|
});
|
|
993
|
+
shader._compiled = true;
|
|
697
994
|
}
|
|
698
995
|
|
|
699
996
|
_getBlendState(mode) {
|
|
@@ -940,8 +1237,11 @@ function rendererWebGPU(p5, fn) {
|
|
|
940
1237
|
|
|
941
1238
|
_resetBuffersBeforeDraw() {
|
|
942
1239
|
this._finishActiveRenderPass();
|
|
1240
|
+
|
|
943
1241
|
// Set state to PENDING - we'll decide on first draw
|
|
944
|
-
this.
|
|
1242
|
+
if (this._pInst.frameCount > 0) {
|
|
1243
|
+
this._frameState = FRAME_STATE.PENDING;
|
|
1244
|
+
}
|
|
945
1245
|
|
|
946
1246
|
// Clear depth buffer but DON'T start any render pass yet
|
|
947
1247
|
const activeFramebuffer = this.activeFramebuffer();
|
|
@@ -1052,6 +1352,8 @@ function rendererWebGPU(p5, fn) {
|
|
|
1052
1352
|
// once we're drawing to the framebuffer, because normally
|
|
1053
1353
|
// those are reset.
|
|
1054
1354
|
const savedModelMatrix = this.states.uModelMatrix.copy();
|
|
1355
|
+
this.states.uModelMatrix.set(this.states.uModelMatrix.copy());
|
|
1356
|
+
this.states.uModelMatrix.reset();
|
|
1055
1357
|
this.mainFramebuffer.defaultCamera.set(this.states.curCamera);
|
|
1056
1358
|
|
|
1057
1359
|
this.mainFramebuffer.begin();
|
|
@@ -1060,6 +1362,11 @@ function rendererWebGPU(p5, fn) {
|
|
|
1060
1362
|
}
|
|
1061
1363
|
|
|
1062
1364
|
_promoteToFramebufferWithoutCopy() {
|
|
1365
|
+
// Already promoted this frame
|
|
1366
|
+
if (this._frameState === FRAME_STATE.PROMOTED) {
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1063
1370
|
// Ensure mainFramebuffer matches canvas size
|
|
1064
1371
|
if (this.mainFramebuffer.width !== this.width ||
|
|
1065
1372
|
this.mainFramebuffer.height !== this.height) {
|
|
@@ -1074,6 +1381,8 @@ function rendererWebGPU(p5, fn) {
|
|
|
1074
1381
|
|
|
1075
1382
|
// Preserve transformation state
|
|
1076
1383
|
const savedModelMatrix = this.states.uModelMatrix.copy();
|
|
1384
|
+
this.states.uModelMatrix.set(this.states.uModelMatrix.copy());
|
|
1385
|
+
this.states.uModelMatrix.reset();
|
|
1077
1386
|
this.mainFramebuffer.defaultCamera.set(this.states.curCamera);
|
|
1078
1387
|
|
|
1079
1388
|
// Begin rendering to mainFramebuffer
|
|
@@ -1387,7 +1696,6 @@ function rendererWebGPU(p5, fn) {
|
|
|
1387
1696
|
}
|
|
1388
1697
|
this.flushDraw();
|
|
1389
1698
|
|
|
1390
|
-
// this._pInst.background('red');
|
|
1391
1699
|
this._pInst.push();
|
|
1392
1700
|
this.states.setValue('enableLighting', false);
|
|
1393
1701
|
this.states.setValue('activeImageLight', null);
|
|
@@ -1452,25 +1760,9 @@ function rendererWebGPU(p5, fn) {
|
|
|
1452
1760
|
|
|
1453
1761
|
this._beginActiveRenderPass();
|
|
1454
1762
|
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
1763
|
|
|
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
|
-
}
|
|
1764
|
+
const currentShader = this._curShader;
|
|
1765
|
+
this.setupShaderBindGroups(currentShader, passEncoder, { mode, buffers });
|
|
1474
1766
|
// Bind vertex buffers
|
|
1475
1767
|
for (const buffer of currentShader._vertexBuffers || this._getVertexBuffers(currentShader)) {
|
|
1476
1768
|
const location = currentShader.attributes[buffer.attr].location;
|
|
@@ -1478,6 +1770,58 @@ function rendererWebGPU(p5, fn) {
|
|
|
1478
1770
|
passEncoder.setVertexBuffer(location, gpuBuffer, 0);
|
|
1479
1771
|
}
|
|
1480
1772
|
|
|
1773
|
+
if (currentShader.shaderType === "fill") {
|
|
1774
|
+
// Bind index buffer and issue draw
|
|
1775
|
+
if (buffers.indexBuffer) {
|
|
1776
|
+
const indexFormat = buffers.indexFormat || "uint16";
|
|
1777
|
+
passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
|
|
1778
|
+
passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
|
|
1779
|
+
} else {
|
|
1780
|
+
passEncoder.draw(geometry.vertices.length, count, 0, 0);
|
|
1781
|
+
}
|
|
1782
|
+
} else if (currentShader.shaderType === "text") {
|
|
1783
|
+
if (!buffers.indexBuffer) {
|
|
1784
|
+
throw new Error("Text geometry must have an index buffer");
|
|
1785
|
+
}
|
|
1786
|
+
const indexFormat = buffers.indexFormat || "uint16";
|
|
1787
|
+
passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
|
|
1788
|
+
passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
if (buffers.lineVerticesBuffer && currentShader.shaderType === "stroke") {
|
|
1792
|
+
passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
// Mark that we have pending draws that need submission
|
|
1796
|
+
this._hasPendingDraws = true;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
setupShaderBindGroups(currentShader, passEncoder, shaderOptionsParams) {
|
|
1800
|
+
const shaderOptions = this._shaderOptions(shaderOptionsParams);
|
|
1801
|
+
if (
|
|
1802
|
+
shaderOptions.compute ||
|
|
1803
|
+
this.activeShader !== currentShader ||
|
|
1804
|
+
this._shaderOptionsDifferent(shaderOptions)
|
|
1805
|
+
) {
|
|
1806
|
+
passEncoder.setPipeline(currentShader.getPipeline(shaderOptions));
|
|
1807
|
+
}
|
|
1808
|
+
if (!shaderOptions.compute) {
|
|
1809
|
+
this.activeShader = currentShader;
|
|
1810
|
+
this.activeShaderOptions = shaderOptions;
|
|
1811
|
+
|
|
1812
|
+
// Set stencil reference value for clipping
|
|
1813
|
+
const drawTarget = this.drawTarget();
|
|
1814
|
+
if (drawTarget._isClipApplied && !this._clipping) {
|
|
1815
|
+
// When using the clip mask, test against reference value 0 (background)
|
|
1816
|
+
// WebGL uses NOTEQUAL with ref 0, so fragments pass where stencil != 0
|
|
1817
|
+
// In WebGPU with 'not-equal', we need ref 0 to pass where stencil != 0
|
|
1818
|
+
passEncoder.setStencilReference(0);
|
|
1819
|
+
} else if (this._clipping) {
|
|
1820
|
+
// When writing to the clip mask, write reference value 1
|
|
1821
|
+
passEncoder.setStencilReference(1);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1481
1825
|
for (const bufferGroup of currentShader._uniformBufferGroups) {
|
|
1482
1826
|
if (bufferGroup.dynamic) {
|
|
1483
1827
|
// Bind uniforms into a part of a big dynamic memory block because
|
|
@@ -1530,6 +1874,13 @@ function rendererWebGPU(p5, fn) {
|
|
|
1530
1874
|
currentShader.buffersDirty.delete(key);
|
|
1531
1875
|
}
|
|
1532
1876
|
}
|
|
1877
|
+
for (const storage of currentShader._storageBuffers || []) {
|
|
1878
|
+
const key = storage.group * 1000 + storage.binding;
|
|
1879
|
+
if (currentShader.buffersDirty.has(key)) {
|
|
1880
|
+
currentShader._cachedBindGroup[storage.group] = undefined;
|
|
1881
|
+
currentShader.buffersDirty.delete(key);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1533
1884
|
|
|
1534
1885
|
// Bind sampler/texture uniforms and uniform buffers
|
|
1535
1886
|
for (const iter of currentShader._groupEntries) {
|
|
@@ -1559,6 +1910,19 @@ function rendererWebGPU(p5, fn) {
|
|
|
1559
1910
|
: { buffer: uniformBufferInfo.buffer },
|
|
1560
1911
|
});
|
|
1561
1912
|
}
|
|
1913
|
+
} else if (entry.storage && !bindGroup) {
|
|
1914
|
+
// Storage buffer binding
|
|
1915
|
+
const uniform = currentShader.uniforms[entry.storage.name];
|
|
1916
|
+
if (!uniform || !uniform._cachedData || !uniform._cachedData._isStorageBuffer) {
|
|
1917
|
+
throw new Error(
|
|
1918
|
+
`Storage buffer "${entry.storage.name}" not set. ` +
|
|
1919
|
+
`Use shader.setUniform("${entry.storage.name}", storageBuffer)`
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
bgEntries.push({
|
|
1923
|
+
binding: entry.binding,
|
|
1924
|
+
resource: { buffer: uniform._cachedData.buffer },
|
|
1925
|
+
});
|
|
1562
1926
|
} else if (!bindGroup) {
|
|
1563
1927
|
bgEntries.push({
|
|
1564
1928
|
binding: entry.binding,
|
|
@@ -1592,84 +1956,71 @@ function rendererWebGPU(p5, fn) {
|
|
|
1592
1956
|
);
|
|
1593
1957
|
}
|
|
1594
1958
|
}
|
|
1959
|
+
return passEncoder;
|
|
1960
|
+
}
|
|
1595
1961
|
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1962
|
+
//////////////////////////////////////////////
|
|
1963
|
+
// SHADER
|
|
1964
|
+
//////////////////////////////////////////////
|
|
1965
|
+
|
|
1966
|
+
// Writes a single field's value into a Float32Array+DataView at (baseOffset + field.offset).
|
|
1967
|
+
//
|
|
1968
|
+
// Field interface (shared by uniform fields from _parseStruct and struct storage schema fields):
|
|
1969
|
+
// baseType: string - 'f32', 'i32', 'u32', etc.
|
|
1970
|
+
// size: number - byte size of the field
|
|
1971
|
+
// offset: number - byte offset of the field within its struct
|
|
1972
|
+
// packInPlace: bool - true for mat3, written with manual column padding
|
|
1973
|
+
//
|
|
1974
|
+
// value: number or number[] - the data to write
|
|
1975
|
+
_packField(field, value, floatView, dataView, baseOffset) {
|
|
1976
|
+
if (value === undefined) return;
|
|
1977
|
+
|
|
1978
|
+
// Duck typing instead of instanceof to avoid importing a separate
|
|
1979
|
+
// copy of the Color/Vector classes
|
|
1980
|
+
if (value?.isVector) {
|
|
1981
|
+
value = value.values.length !== value.dimensions ? value.values.slice(0, value.dimensions) : value.values;
|
|
1982
|
+
} else if (value?.isColor) {
|
|
1983
|
+
value = value._getRGBA([1, 1, 1, 1]);
|
|
1984
|
+
}
|
|
1985
|
+
const byteOffset = baseOffset + field.offset;
|
|
1986
|
+
if (field.baseType === 'u32') {
|
|
1987
|
+
if (field.size === 4) {
|
|
1988
|
+
dataView.setUint32(byteOffset, value, true);
|
|
1602
1989
|
} else {
|
|
1603
|
-
|
|
1990
|
+
for (let i = 0; i < value.length; i++) {
|
|
1991
|
+
dataView.setUint32(byteOffset + i * 4, value[i], true);
|
|
1992
|
+
}
|
|
1604
1993
|
}
|
|
1605
|
-
} else if (
|
|
1606
|
-
if (
|
|
1607
|
-
|
|
1994
|
+
} else if (field.baseType === 'i32') {
|
|
1995
|
+
if (field.size === 4) {
|
|
1996
|
+
dataView.setInt32(byteOffset, value, true);
|
|
1997
|
+
} else {
|
|
1998
|
+
for (let i = 0; i < value.length; i++) {
|
|
1999
|
+
dataView.setInt32(byteOffset + i * 4, value[i], true);
|
|
2000
|
+
}
|
|
1608
2001
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
2002
|
+
} else if (field.packInPlace) {
|
|
2003
|
+
// In-place packing for mat3: write directly to buffer with padding
|
|
2004
|
+
const base = byteOffset / 4;
|
|
2005
|
+
floatView[base + 0] = value[0]; floatView[base + 1] = value[1]; floatView[base + 2] = value[2];
|
|
2006
|
+
floatView[base + 4] = value[3]; floatView[base + 5] = value[4]; floatView[base + 6] = value[5];
|
|
2007
|
+
floatView[base + 8] = value[6]; floatView[base + 9] = value[7]; floatView[base + 10] = value[8];
|
|
2008
|
+
} else if (field.size === 4) {
|
|
2009
|
+
floatView.set([value], byteOffset / 4);
|
|
2010
|
+
} else {
|
|
2011
|
+
floatView.set(value, byteOffset / 4);
|
|
1616
2012
|
}
|
|
1617
|
-
|
|
1618
|
-
// Mark that we have pending draws that need submission
|
|
1619
|
-
this._hasPendingDraws = true;
|
|
1620
2013
|
}
|
|
1621
2014
|
|
|
1622
|
-
//////////////////////////////////////////////
|
|
1623
|
-
// SHADER
|
|
1624
|
-
//////////////////////////////////////////////
|
|
1625
|
-
|
|
1626
2015
|
_packUniformGroup(shader, groupUniforms, bufferInfo) {
|
|
1627
2016
|
// Pack a single group's uniforms into a buffer
|
|
1628
2017
|
const data = bufferInfo.data;
|
|
1629
2018
|
const dataView = bufferInfo.dataView;
|
|
1630
|
-
|
|
1631
2019
|
const offset = bufferInfo.offset || 0;
|
|
1632
2020
|
for (const uniform of groupUniforms) {
|
|
1633
2021
|
const fullUniform = shader.uniforms[uniform.name];
|
|
1634
2022
|
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
|
-
}
|
|
2023
|
+
this._packField(fullUniform, fullUniform._mappedData, data, dataView, offset);
|
|
1673
2024
|
}
|
|
1674
2025
|
}
|
|
1675
2026
|
|
|
@@ -1816,10 +2167,11 @@ function rendererWebGPU(p5, fn) {
|
|
|
1816
2167
|
const uniformVarRegex = /@group\((\d+)\)\s+@binding\((\d+)\)\s+var<uniform>\s+(\w+)\s*:\s*(\w+);/g;
|
|
1817
2168
|
|
|
1818
2169
|
let match;
|
|
1819
|
-
|
|
2170
|
+
const src = shader.shaderType === 'compute' ? shader.computeSrc() : shader.vertSrc();
|
|
2171
|
+
while ((match = uniformVarRegex.exec(src)) !== null) {
|
|
1820
2172
|
const [_, groupNum, binding, varName, structType] = match;
|
|
1821
2173
|
const bindingIndex = parseInt(binding);
|
|
1822
|
-
const uniforms = this._parseStruct(
|
|
2174
|
+
const uniforms = this._parseStruct(src, structType);
|
|
1823
2175
|
|
|
1824
2176
|
uniformGroups.push({
|
|
1825
2177
|
group: parseInt(groupNum),
|
|
@@ -1830,7 +2182,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
1830
2182
|
});
|
|
1831
2183
|
}
|
|
1832
2184
|
|
|
1833
|
-
if (uniformGroups.length === 0) {
|
|
2185
|
+
if (uniformGroups.length === 0 && shader.shaderType !== 'compute') {
|
|
1834
2186
|
throw new Error('Expected at least one uniform struct bound to @group(0)');
|
|
1835
2187
|
}
|
|
1836
2188
|
|
|
@@ -1857,6 +2209,10 @@ function rendererWebGPU(p5, fn) {
|
|
|
1857
2209
|
// TODO: support other texture types
|
|
1858
2210
|
const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d<f32>|sampler);/g;
|
|
1859
2211
|
|
|
2212
|
+
// Extract storage buffers
|
|
2213
|
+
const storageBuffers = {};
|
|
2214
|
+
const storageRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var<storage,\s*(read|read_write)>\s+(\w+)\s*:\s*array<\w+>/g;
|
|
2215
|
+
|
|
1860
2216
|
// Track which bindings are taken by the struct properties we've parsed
|
|
1861
2217
|
// (the rest should be textures/samplers)
|
|
1862
2218
|
const structUniformBindings = {};
|
|
@@ -1866,8 +2222,11 @@ function rendererWebGPU(p5, fn) {
|
|
|
1866
2222
|
|
|
1867
2223
|
for (const [src, visibility] of [
|
|
1868
2224
|
[shader.vertSrc(), GPUShaderStage.VERTEX],
|
|
1869
|
-
[shader.fragSrc(), GPUShaderStage.FRAGMENT]
|
|
2225
|
+
[shader.fragSrc(), GPUShaderStage.FRAGMENT],
|
|
2226
|
+
[shader.computeSrc ? shader.computeSrc() : null, GPUShaderStage.COMPUTE]
|
|
1870
2227
|
]) {
|
|
2228
|
+
if (!src) continue; // Skip if shader stage doesn't exist
|
|
2229
|
+
|
|
1871
2230
|
let match;
|
|
1872
2231
|
while ((match = samplerRegex.exec(src)) !== null) {
|
|
1873
2232
|
const [_, group, binding, name, type] = match;
|
|
@@ -1902,21 +2261,51 @@ function rendererWebGPU(p5, fn) {
|
|
|
1902
2261
|
samplerNode.textureSource = sampler;
|
|
1903
2262
|
}
|
|
1904
2263
|
}
|
|
2264
|
+
|
|
2265
|
+
// Parse storage buffers
|
|
2266
|
+
while ((match = storageRegex.exec(src)) !== null) {
|
|
2267
|
+
const [_, group, binding, accessMode, name] = match;
|
|
2268
|
+
const groupIndex = parseInt(group);
|
|
2269
|
+
const bindingIndex = parseInt(binding);
|
|
2270
|
+
|
|
2271
|
+
const key = `${groupIndex},${bindingIndex}`;
|
|
2272
|
+
const existing = storageBuffers[key];
|
|
2273
|
+
// If any stage uses read_write, the bind group layout must use read_write
|
|
2274
|
+
const finalAccessMode = (existing?.accessMode === 'read_write' || accessMode === 'read_write')
|
|
2275
|
+
? 'read_write'
|
|
2276
|
+
: accessMode;
|
|
2277
|
+
|
|
2278
|
+
storageBuffers[key] = {
|
|
2279
|
+
visibility: (existing?.visibility || 0) | visibility,
|
|
2280
|
+
group: groupIndex,
|
|
2281
|
+
binding: bindingIndex,
|
|
2282
|
+
name,
|
|
2283
|
+
accessMode: finalAccessMode, // 'read' or 'read_write'
|
|
2284
|
+
isStorage: true,
|
|
2285
|
+
type: 'storage'
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
1905
2288
|
}
|
|
1906
|
-
|
|
2289
|
+
|
|
2290
|
+
// Store storage buffers on shader for later use
|
|
2291
|
+
shader._storageBuffers = Object.values(storageBuffers);
|
|
2292
|
+
|
|
2293
|
+
return [...Object.values(allUniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers), ...Object.values(storageBuffers)];
|
|
1907
2294
|
}
|
|
1908
2295
|
|
|
1909
|
-
getNextBindingIndex({ vert, frag }, group = 0) {
|
|
2296
|
+
getNextBindingIndex({ vert, frag, compute }, group = 0) {
|
|
1910
2297
|
// Get the highest binding index in the specified group and return the next available
|
|
1911
|
-
const
|
|
2298
|
+
const bindingRegex = /@group\((\d+)\)\s*@binding\((\d+)\)/g;
|
|
1912
2299
|
let maxBindingIndex = -1;
|
|
1913
2300
|
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
])
|
|
2301
|
+
const sources = [];
|
|
2302
|
+
if (vert) sources.push([vert, GPUShaderStage.VERTEX]);
|
|
2303
|
+
if (frag) sources.push([frag, GPUShaderStage.FRAGMENT]);
|
|
2304
|
+
if (compute) sources.push([compute, GPUShaderStage.COMPUTE]);
|
|
2305
|
+
|
|
2306
|
+
for (const [src, visibility] of sources) {
|
|
1918
2307
|
let match;
|
|
1919
|
-
while ((match =
|
|
2308
|
+
while ((match = bindingRegex.exec(src)) !== null) {
|
|
1920
2309
|
const [_, groupIndex, bindingIndex] = match;
|
|
1921
2310
|
if (parseInt(groupIndex) === group) {
|
|
1922
2311
|
maxBindingIndex = Math.max(maxBindingIndex, parseInt(bindingIndex));
|
|
@@ -1931,7 +2320,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
1931
2320
|
if (uniform.isSampler) {
|
|
1932
2321
|
uniform.texture =
|
|
1933
2322
|
data instanceof Texture ? data : this.getTexture(data);
|
|
1934
|
-
} else {
|
|
2323
|
+
} else if (!data?._isStorageBuffer) {
|
|
1935
2324
|
uniform._mappedData = this._mapUniformData(uniform, uniform._cachedData);
|
|
1936
2325
|
}
|
|
1937
2326
|
shader.buffersDirty.add(uniform.group * 1000 + uniform.binding);
|
|
@@ -2038,7 +2427,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
2038
2427
|
rgb += components.emissive;
|
|
2039
2428
|
return vec4<f32>(rgb, components.opacity);
|
|
2040
2429
|
}`,
|
|
2041
|
-
"vec4f getFinalColor": "(color: vec4<f32>) { return color; }",
|
|
2430
|
+
"vec4f getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
|
|
2042
2431
|
"void afterFragment": "() {}",
|
|
2043
2432
|
},
|
|
2044
2433
|
}
|
|
@@ -2063,7 +2452,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
2063
2452
|
},
|
|
2064
2453
|
fragment: {
|
|
2065
2454
|
"void beforeFragment": "() {}",
|
|
2066
|
-
"vec4<f32> getFinalColor": "(color: vec4<f32>) { return color; }",
|
|
2455
|
+
"vec4<f32> getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
|
|
2067
2456
|
"void afterFragment": "() {}",
|
|
2068
2457
|
},
|
|
2069
2458
|
}
|
|
@@ -2089,7 +2478,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
2089
2478
|
fragment: {
|
|
2090
2479
|
"void beforeFragment": "() {}",
|
|
2091
2480
|
"Inputs getPixelInputs": "(inputs: Inputs) { return inputs; }",
|
|
2092
|
-
"vec4<f32> getFinalColor": "(color: vec4<f32>) { return color; }",
|
|
2481
|
+
"vec4<f32> getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
|
|
2093
2482
|
"bool shouldDiscard": "(outside: bool) { return outside; };",
|
|
2094
2483
|
"void afterFragment": "() {}",
|
|
2095
2484
|
},
|
|
@@ -2248,11 +2637,87 @@ function rendererWebGPU(p5, fn) {
|
|
|
2248
2637
|
}
|
|
2249
2638
|
);
|
|
2250
2639
|
|
|
2251
|
-
let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment)\s*)?fn main[^{]+\{)/);
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2640
|
+
let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment|compute)\s*(?:@workgroup_size\([^)]+\)\s*)?)?fn main[^{]+\{)/);
|
|
2641
|
+
|
|
2642
|
+
const getBuiltinParamName = (mainSrc, builtinName) => {
|
|
2643
|
+
const match = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`).exec(mainSrc);
|
|
2644
|
+
return match ? match[1] : null;
|
|
2645
|
+
};
|
|
2646
|
+
|
|
2647
|
+
const ensureBuiltinParam = (mainSrc, builtinName, fallbackName, typeName) => {
|
|
2648
|
+
const existingName = getBuiltinParamName(mainSrc, builtinName);
|
|
2649
|
+
if (existingName) {
|
|
2650
|
+
return { mainSrc, argName: existingName };
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
const hasParams = /\(\s*\S/.test(mainSrc);
|
|
2654
|
+
const injectedMain = mainSrc.replace(
|
|
2655
|
+
/\)\s*(->|\{)/,
|
|
2656
|
+
`${hasParams ? ', ' : ''}@builtin(${builtinName}) ${fallbackName}: ${typeName}) $1`
|
|
2657
|
+
);
|
|
2658
|
+
|
|
2659
|
+
return { mainSrc: injectedMain, argName: fallbackName };
|
|
2660
|
+
};
|
|
2661
|
+
|
|
2662
|
+
const getMainStructParameter = (mainSrc) => {
|
|
2663
|
+
const match = /fn main\s*\(\s*(\w+)\s*:\s*(\w+)/.exec(mainSrc);
|
|
2664
|
+
if (!match) return null;
|
|
2665
|
+
return { inputName: match[1], structName: match[2] };
|
|
2666
|
+
};
|
|
2667
|
+
|
|
2668
|
+
const getStructBuiltinFieldName = (structName, builtinName) => {
|
|
2669
|
+
const structMatch = new RegExp(`struct\\s+${structName}\\s*\\{([^}]*)\\}`, 's').exec(preMain);
|
|
2670
|
+
if (!structMatch) return null;
|
|
2671
|
+
const fieldMatch = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`, 's').exec(structMatch[1]);
|
|
2672
|
+
return fieldMatch ? fieldMatch[1] : null;
|
|
2673
|
+
};
|
|
2674
|
+
|
|
2675
|
+
const appendHookParams = (params, additionalParams) => {
|
|
2676
|
+
if (additionalParams.length === 0) return params;
|
|
2677
|
+
const hasParams = !/^\(\s*\)$/.test(params);
|
|
2678
|
+
return `${params.slice(0, -1)}${hasParams ? ', ' : ''}${additionalParams.join(', ')})`;
|
|
2679
|
+
};
|
|
2680
|
+
|
|
2681
|
+
let hookExtraParams = [];
|
|
2682
|
+
let hookExtraArgs = [];
|
|
2683
|
+
|
|
2684
|
+
if (shaderType === 'vertex') {
|
|
2685
|
+
const ensuredInstance = ensureBuiltinParam(main, 'instance_index', 'instanceID', 'u32');
|
|
2686
|
+
main = ensuredInstance.mainSrc;
|
|
2687
|
+
|
|
2688
|
+
const ensuredVertex = ensureBuiltinParam(main, 'vertex_index', '_p5VertexId', 'u32');
|
|
2689
|
+
main = ensuredVertex.mainSrc;
|
|
2690
|
+
|
|
2691
|
+
hookExtraParams = ['instanceID: u32', '_p5VertexId: u32'];
|
|
2692
|
+
hookExtraArgs = [ensuredInstance.argName, ensuredVertex.argName];
|
|
2693
|
+
} else if (shaderType === 'fragment') {
|
|
2694
|
+
const directPositionArg = getBuiltinParamName(main, 'position');
|
|
2695
|
+
let fragmentPositionArg = directPositionArg;
|
|
2696
|
+
|
|
2697
|
+
if (!fragmentPositionArg) {
|
|
2698
|
+
const mainStructParam = getMainStructParameter(main);
|
|
2699
|
+
if (mainStructParam) {
|
|
2700
|
+
const positionField = getStructBuiltinFieldName(mainStructParam.structName, 'position');
|
|
2701
|
+
if (positionField) {
|
|
2702
|
+
fragmentPositionArg = `${mainStructParam.inputName}.${positionField}`;
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2255
2705
|
}
|
|
2706
|
+
|
|
2707
|
+
if (!fragmentPositionArg) {
|
|
2708
|
+
const ensuredPosition = ensureBuiltinParam(main, 'position', '_p5FragPos', 'vec4<f32>');
|
|
2709
|
+
main = ensuredPosition.mainSrc;
|
|
2710
|
+
fragmentPositionArg = ensuredPosition.argName;
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
hookExtraParams = ['_p5FragPos: vec4<f32>'];
|
|
2714
|
+
hookExtraArgs = [fragmentPositionArg];
|
|
2715
|
+
} else if (shaderType === 'compute') {
|
|
2716
|
+
const ensuredGlobalId = ensureBuiltinParam(main, 'global_invocation_id', '_p5GlobalId', 'vec3<u32>');
|
|
2717
|
+
main = ensuredGlobalId.mainSrc;
|
|
2718
|
+
|
|
2719
|
+
hookExtraParams = ['_p5GlobalId: vec3<u32>'];
|
|
2720
|
+
hookExtraArgs = [ensuredGlobalId.argName];
|
|
2256
2721
|
}
|
|
2257
2722
|
|
|
2258
2723
|
// Inject hook uniforms as a separate struct at a new binding
|
|
@@ -2272,6 +2737,7 @@ function rendererWebGPU(p5, fn) {
|
|
|
2272
2737
|
const nextBinding = this.getNextBindingIndex({
|
|
2273
2738
|
vert: shaderType === 'vertex' ? preMain + (shader.hooks.vertex?.declarations ?? '') + shader.hooks.declarations : shader._vertSrc,
|
|
2274
2739
|
frag: shaderType === 'fragment' ? preMain + (shader.hooks.fragment?.declarations ?? '') + shader.hooks.declarations : shader._fragSrc,
|
|
2740
|
+
compute: shaderType === 'compute' ? preMain + (shader.hooks.compute?.declarations ?? '') + shader.hooks.declarations : shader._computeSrc,
|
|
2275
2741
|
}, 0);
|
|
2276
2742
|
|
|
2277
2743
|
// Create HookUniforms struct and binding
|
|
@@ -2282,8 +2748,14 @@ ${hookUniformFields}}
|
|
|
2282
2748
|
|
|
2283
2749
|
@group(0) @binding(${nextBinding}) var<uniform> hooks: HookUniforms;
|
|
2284
2750
|
`;
|
|
2285
|
-
// Insert before the first @group binding
|
|
2286
|
-
|
|
2751
|
+
// Insert before the first @group binding, or at the end if there are none
|
|
2752
|
+
const replaced = preMain.replace(/(@group\(0\)\s+@binding)/, `${hookUniformsDecl}\n$1`);
|
|
2753
|
+
if (replaced === preMain) {
|
|
2754
|
+
// No @group bindings found in base shader, append to preMain
|
|
2755
|
+
preMain = preMain + '\n' + hookUniformsDecl;
|
|
2756
|
+
} else {
|
|
2757
|
+
preMain = replaced;
|
|
2758
|
+
}
|
|
2287
2759
|
}
|
|
2288
2760
|
|
|
2289
2761
|
// Handle varying variables by injecting them into VertexOutput and FragmentInput structs
|
|
@@ -2349,10 +2821,9 @@ ${hookUniformFields}}
|
|
|
2349
2821
|
initStatements += ` ${varName} = INPUT_VAR.${varName};\n`;
|
|
2350
2822
|
}
|
|
2351
2823
|
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
const inputVarName = inputMatch[1];
|
|
2824
|
+
const mainStructParam = getMainStructParameter(main);
|
|
2825
|
+
if (mainStructParam) {
|
|
2826
|
+
const inputVarName = mainStructParam.inputName;
|
|
2356
2827
|
initStatements = initStatements.replace(/INPUT_VAR/g, inputVarName);
|
|
2357
2828
|
// Insert after the main function parameter but before any other code (anchored to start)
|
|
2358
2829
|
postMain = initStatements + postMain;
|
|
@@ -2360,12 +2831,56 @@ ${hookUniformFields}}
|
|
|
2360
2831
|
}
|
|
2361
2832
|
}
|
|
2362
2833
|
|
|
2834
|
+
// Handle instanceID varying for fragment access
|
|
2835
|
+
if (shader.hooks.instanceIDVarying) {
|
|
2836
|
+
const { name, declaration, source, interpolation } = shader.hooks.instanceIDVarying;
|
|
2837
|
+
const nextLocIndex = this._getNextAvailableLocation(preMain, shaderType);
|
|
2838
|
+
const interpAttr = interpolation ? ` @interpolate(${interpolation})` : '';
|
|
2839
|
+
const [varName, varType] = declaration.split(':').map(s => s.trim());
|
|
2840
|
+
const structMember = `@location(${nextLocIndex})${interpAttr} ${declaration},`;
|
|
2841
|
+
|
|
2842
|
+
if (shaderType === 'vertex') {
|
|
2843
|
+
// Inject into VertexOutput struct
|
|
2844
|
+
preMain = preMain.replace(
|
|
2845
|
+
/struct\s+VertexOutput\s+\{([^}]*)\}/,
|
|
2846
|
+
(match, body) => `struct VertexOutput {${body}\n${structMember}}`
|
|
2847
|
+
);
|
|
2848
|
+
// Add private global
|
|
2849
|
+
preMain += `var<private> ${declaration};\n`;
|
|
2850
|
+
// Assign from built-in instanceID at start of main()
|
|
2851
|
+
postMain = `\n ${varName} = ${source};\n` + postMain;
|
|
2852
|
+
// Copy to output struct before return
|
|
2853
|
+
const returnMatch = postMain.match(/return\s+(\w+)\s*;/);
|
|
2854
|
+
if (returnMatch) {
|
|
2855
|
+
const outputVarName = returnMatch[1];
|
|
2856
|
+
postMain = postMain.replace(
|
|
2857
|
+
/(return\s+\w+\s*;)/g,
|
|
2858
|
+
`${outputVarName}.${varName} = ${varName};\n $1`
|
|
2859
|
+
);
|
|
2860
|
+
}
|
|
2861
|
+
} else if (shaderType === 'fragment') {
|
|
2862
|
+
// Inject into FragmentInput struct
|
|
2863
|
+
preMain = preMain.replace(
|
|
2864
|
+
/struct\s+FragmentInput\s+\{([^}]*)\}/,
|
|
2865
|
+
(match, body) => `struct FragmentInput {${body}\n${structMember}}`
|
|
2866
|
+
);
|
|
2867
|
+
// Add private global
|
|
2868
|
+
preMain += `var<private> ${declaration};\n`;
|
|
2869
|
+
// Initialize from input struct at start of main()
|
|
2870
|
+
const mainStructParam = getMainStructParameter(main);
|
|
2871
|
+
if (mainStructParam) {
|
|
2872
|
+
const inputVarName = mainStructParam.inputName;
|
|
2873
|
+
postMain = `\n ${varName} = ${inputVarName}.${varName};\n` + postMain;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2363
2878
|
let hooks = '';
|
|
2364
2879
|
let defines = '';
|
|
2365
2880
|
if (shader.hooks.declarations) {
|
|
2366
2881
|
hooks += shader.hooks.declarations + '\n';
|
|
2367
2882
|
}
|
|
2368
|
-
if (shader.hooks[shaderType].declarations) {
|
|
2883
|
+
if (shader.hooks[shaderType] && shader.hooks[shaderType].declarations) {
|
|
2369
2884
|
hooks += shader.hooks[shaderType].declarations + '\n';
|
|
2370
2885
|
}
|
|
2371
2886
|
for (const hookDef in shader.hooks.helpers) {
|
|
@@ -2389,11 +2904,7 @@ ${hookUniformFields}}
|
|
|
2389
2904
|
|
|
2390
2905
|
let [_, params, body] = /^(\([^\)]*\))((?:.|\n)*)$/.exec(shader.hooks[shaderType][hookDef]);
|
|
2391
2906
|
|
|
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
|
-
}
|
|
2907
|
+
params = appendHookParams(params, hookExtraParams);
|
|
2397
2908
|
|
|
2398
2909
|
if (hookType === 'void') {
|
|
2399
2910
|
hooks += `fn HOOK_${hookName}${params}${body}\n`;
|
|
@@ -2402,40 +2913,45 @@ ${hookUniformFields}}
|
|
|
2402
2913
|
}
|
|
2403
2914
|
}
|
|
2404
2915
|
|
|
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
|
-
}
|
|
2916
|
+
// Pass stage-specific builtins from main to each hook call.
|
|
2917
|
+
// Collect ALL HOOK_ calls (including nested ones) then insert
|
|
2918
|
+
// extra args from right to left so position shifts don't
|
|
2919
|
+
// invalidate earlier insertion points.
|
|
2920
|
+
if (hookExtraArgs.length > 0) {
|
|
2921
|
+
const addHookArgs = (src) => {
|
|
2922
|
+
const insertions = [];
|
|
2923
|
+
let searchIdx = 0;
|
|
2924
|
+
let m;
|
|
2925
|
+
while ((m = /HOOK_\w+\(/.exec(src.slice(searchIdx))) !== null) {
|
|
2926
|
+
const openParen = searchIdx + m.index + m[0].length - 1;
|
|
2927
|
+
let pos = openParen + 1;
|
|
2928
|
+
let nesting = 1;
|
|
2929
|
+
let hasParams = false;
|
|
2930
|
+
while (pos < src.length && nesting > 0) {
|
|
2931
|
+
if (src[pos] === '(') nesting++;
|
|
2932
|
+
else if (src[pos] === ')') {
|
|
2933
|
+
nesting--;
|
|
2934
|
+
if (nesting === 0) break;
|
|
2935
|
+
} else if (/\S/.test(src[pos])) {
|
|
2936
|
+
hasParams = true;
|
|
2429
2937
|
}
|
|
2430
|
-
|
|
2431
|
-
result = result.slice(0, idx-1) + insertion + result.slice(idx-1);
|
|
2432
|
-
idx += insertion.length;
|
|
2938
|
+
pos++;
|
|
2433
2939
|
}
|
|
2434
|
-
|
|
2940
|
+
insertions.push({ pos, hasParams });
|
|
2941
|
+
searchIdx = openParen + 1;
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
insertions.sort((a, b) => b.pos - a.pos);
|
|
2945
|
+
|
|
2946
|
+
let result = src;
|
|
2947
|
+
for (const { pos, hasParams } of insertions) {
|
|
2948
|
+
const insertion = (hasParams ? ', ' : '') + hookExtraArgs.join(', ');
|
|
2949
|
+
result = result.slice(0, pos) + insertion + result.slice(pos);
|
|
2950
|
+
}
|
|
2435
2951
|
return result;
|
|
2436
2952
|
};
|
|
2437
|
-
preMain =
|
|
2438
|
-
postMain =
|
|
2953
|
+
preMain = addHookArgs(preMain);
|
|
2954
|
+
postMain = addHookArgs(postMain);
|
|
2439
2955
|
}
|
|
2440
2956
|
|
|
2441
2957
|
return preMain + '\n' + defines + hooks + main + postMain;
|
|
@@ -2494,6 +3010,10 @@ ${hookUniformFields}}
|
|
|
2494
3010
|
body = shader.hooks.fragment[hookName];
|
|
2495
3011
|
fullSrc = shader._fragSrc;
|
|
2496
3012
|
}
|
|
3013
|
+
if (!body) {
|
|
3014
|
+
body = shader.hooks.compute[hookName];
|
|
3015
|
+
fullSrc = shader._computeSrc;
|
|
3016
|
+
}
|
|
2497
3017
|
if (!body) {
|
|
2498
3018
|
throw new Error(`Can't find hook ${hookName}!`);
|
|
2499
3019
|
}
|
|
@@ -2625,7 +3145,7 @@ ${hookUniformFields}}
|
|
|
2625
3145
|
}
|
|
2626
3146
|
|
|
2627
3147
|
defaultFramebufferAntialias() {
|
|
2628
|
-
return
|
|
3148
|
+
return this._pInst._webgpuAttributes?.antialias !== false;
|
|
2629
3149
|
}
|
|
2630
3150
|
|
|
2631
3151
|
supportsFramebufferAntialias() {
|
|
@@ -2818,6 +3338,267 @@ ${hookUniformFields}}
|
|
|
2818
3338
|
};
|
|
2819
3339
|
}
|
|
2820
3340
|
|
|
3341
|
+
// Maps a plain JS value to the WGSL type string that represents it in a struct.
|
|
3342
|
+
_jsValueToWgslType(value) {
|
|
3343
|
+
if (typeof value === 'number') return 'f32';
|
|
3344
|
+
// Duck typing instead of instanceof to avoid importing a separate
|
|
3345
|
+
// copy of the Color/Vector classes
|
|
3346
|
+
if (value?.isVector) {
|
|
3347
|
+
if (value.dimensions === 2) return 'vec2f';
|
|
3348
|
+
if (value.dimensions === 3) return 'vec3f';
|
|
3349
|
+
if (value.dimensions === 4) return 'vec4f';
|
|
3350
|
+
throw new Error(`Unsupported vector dimension ${value.dimensions} for struct storage field`);
|
|
3351
|
+
}
|
|
3352
|
+
if (value?.isColor) {
|
|
3353
|
+
return 'vec4f';
|
|
3354
|
+
}
|
|
3355
|
+
if (Array.isArray(value)) {
|
|
3356
|
+
if (value.length === 2) return 'vec2f';
|
|
3357
|
+
if (value.length === 3) return 'vec3f';
|
|
3358
|
+
if (value.length === 4) return 'vec4f';
|
|
3359
|
+
throw new Error(`Unsupported array length ${value.length} for struct storage field`);
|
|
3360
|
+
}
|
|
3361
|
+
throw new Error(`Unsupported value type ${typeof value} for struct storage field`);
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
// Infers a struct schema from the first element of a struct array.
|
|
3365
|
+
//
|
|
3366
|
+
// Returns { fields, stride, structBody } where:
|
|
3367
|
+
// fields: field has the _packField interface (baseType, size, offset, packInPlace) plus:
|
|
3368
|
+
// name: string - JS property name
|
|
3369
|
+
// dim: number - float component count, used when creating StrandsNodes
|
|
3370
|
+
// structBody: everything inside the { ... } of a WGSL struct definition
|
|
3371
|
+
// stride: how many bytes are reserved for this struct in the buffer
|
|
3372
|
+
_inferStructSchema(firstElement) {
|
|
3373
|
+
const entries = Object.entries(firstElement);
|
|
3374
|
+
|
|
3375
|
+
if (!p5.disableFriendlyErrors) {
|
|
3376
|
+
for (const [name, value] of entries) {
|
|
3377
|
+
if (
|
|
3378
|
+
value !== null &&
|
|
3379
|
+
typeof value === 'object' &&
|
|
3380
|
+
!Array.isArray(value) &&
|
|
3381
|
+
// Duck typing instead of instanceof to avoid importing a separate
|
|
3382
|
+
// copy of the Color/Vector classes
|
|
3383
|
+
!value?.isVector &&
|
|
3384
|
+
!value?.isColor
|
|
3385
|
+
) {
|
|
3386
|
+
p5._friendlyError(
|
|
3387
|
+
`The "${name}" property in your storage data contains a nested object. ` +
|
|
3388
|
+
`Make sure you only use properties with numbers, arrays of numbers, or p5.Vector.`,
|
|
3389
|
+
'createStorage'
|
|
3390
|
+
);
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
const fieldLines = entries.map(([name, value]) =>
|
|
3396
|
+
` ${name}: ${this._jsValueToWgslType(value)},`
|
|
3397
|
+
).join('\n');
|
|
3398
|
+
const structBody = `{\n${fieldLines}\n}`;
|
|
3399
|
+
const elements = this._parseStruct(`struct _Tmp ${structBody}`, '_Tmp');
|
|
3400
|
+
|
|
3401
|
+
let maxEnd = 0;
|
|
3402
|
+
let maxAlign = 1;
|
|
3403
|
+
const fields = entries.map(([name, value]) => {
|
|
3404
|
+
const el = elements[name];
|
|
3405
|
+
maxEnd = Math.max(maxEnd, el.offsetEnd);
|
|
3406
|
+
// Alignment for scalars/vectors: <=4 -> 4, <=8 -> 8, else 16
|
|
3407
|
+
const align = el.size <= 4 ? 4 : el.size <= 8 ? 8 : 16;
|
|
3408
|
+
maxAlign = Math.max(maxAlign, align);
|
|
3409
|
+
// Track original JS type for reconstruction during readback
|
|
3410
|
+
const kind = value?.isVector ? 'vector'
|
|
3411
|
+
: value?.isColor ? 'color'
|
|
3412
|
+
: undefined;
|
|
3413
|
+
return {
|
|
3414
|
+
name,
|
|
3415
|
+
baseType: el.baseType,
|
|
3416
|
+
size: el.size,
|
|
3417
|
+
offset: el.offset,
|
|
3418
|
+
packInPlace: el.packInPlace ?? false,
|
|
3419
|
+
dim: el.size / 4,
|
|
3420
|
+
kind,
|
|
3421
|
+
};
|
|
3422
|
+
});
|
|
3423
|
+
|
|
3424
|
+
const stride = Math.ceil(maxEnd / maxAlign) * maxAlign;
|
|
3425
|
+
return { fields, stride, structBody };
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
// Packs an array of plain objects into a Float32Array using the given struct schema.
|
|
3429
|
+
// Reuses _packField so layout rules match uniform packing exactly.
|
|
3430
|
+
_packStructArray(data, schema) {
|
|
3431
|
+
const { fields, stride } = schema;
|
|
3432
|
+
const totalBytes = Math.max(data.length * stride, 16);
|
|
3433
|
+
const alignedBytes = Math.ceil(totalBytes / 16) * 16;
|
|
3434
|
+
const buffer = new ArrayBuffer(alignedBytes);
|
|
3435
|
+
const floatView = new Float32Array(buffer);
|
|
3436
|
+
const dataView = new DataView(buffer);
|
|
3437
|
+
for (let i = 0; i < data.length; i++) {
|
|
3438
|
+
const item = data[i];
|
|
3439
|
+
const baseOffset = i * stride;
|
|
3440
|
+
for (const field of fields) {
|
|
3441
|
+
this._packField(field, item[field.name], floatView, dataView, baseOffset);
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
return floatView;
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
// Inverse of _packStructArray reads packed buffer back into plain JS objects
|
|
3448
|
+
// using the same schema layout - fields, stride and offsets
|
|
3449
|
+
_unpackStructArray(floatView, schema) {
|
|
3450
|
+
const { fields, stride } = schema;
|
|
3451
|
+
const dataView = new DataView(floatView.buffer);
|
|
3452
|
+
const count = Math.floor(floatView.byteLength / stride);
|
|
3453
|
+
const result = [];
|
|
3454
|
+
|
|
3455
|
+
for (let i = 0; i < count; i++) {
|
|
3456
|
+
const item = {};
|
|
3457
|
+
const baseOffset = i * stride;
|
|
3458
|
+
for (const field of fields) {
|
|
3459
|
+
const byteOffset = baseOffset + field.offset;
|
|
3460
|
+
const n = field.size / 4;
|
|
3461
|
+
|
|
3462
|
+
if (field.baseType === 'u32') {
|
|
3463
|
+
if (n === 1) {
|
|
3464
|
+
item[field.name] = dataView.getUint32(byteOffset, true);
|
|
3465
|
+
} else {
|
|
3466
|
+
item[field.name] = Array.from({ length: n }, (_, j) =>
|
|
3467
|
+
dataView.getUint32(byteOffset + j * 4, true)
|
|
3468
|
+
);
|
|
3469
|
+
}
|
|
3470
|
+
} else if (field.baseType === 'i32') {
|
|
3471
|
+
if (n === 1) {
|
|
3472
|
+
item[field.name] = dataView.getInt32(byteOffset, true);
|
|
3473
|
+
} else {
|
|
3474
|
+
item[field.name] = Array.from({ length: n }, (_, j) =>
|
|
3475
|
+
dataView.getInt32(byteOffset + j * 4, true)
|
|
3476
|
+
);
|
|
3477
|
+
}
|
|
3478
|
+
} else {
|
|
3479
|
+
const idx = byteOffset / 4;
|
|
3480
|
+
if (n === 1) {
|
|
3481
|
+
item[field.name] = floatView[idx];
|
|
3482
|
+
} else {
|
|
3483
|
+
const values = Array.from(floatView.slice(idx, idx + n));
|
|
3484
|
+
if (field.kind === 'vector') {
|
|
3485
|
+
item[field.name] = this._pInst.createVector(...values);
|
|
3486
|
+
} else if (field.kind === 'color') {
|
|
3487
|
+
// Color was packed as normalized RGBA [0-1] via _getRGBA([1,1,1,1])
|
|
3488
|
+
// Scale back to the current colorMode range
|
|
3489
|
+
const maxes = this.states.colorMaxes[this.states.colorMode];
|
|
3490
|
+
item[field.name] = this._pInst.color(
|
|
3491
|
+
values[0] * maxes[0], values[1] * maxes[1],
|
|
3492
|
+
values[2] * maxes[2], values[3] * maxes[3]
|
|
3493
|
+
);
|
|
3494
|
+
} else {
|
|
3495
|
+
item[field.name] = values;
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
result.push(item);
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
return result;
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
createStorage(dataOrCount) {
|
|
3507
|
+
const device = this.device;
|
|
3508
|
+
|
|
3509
|
+
// Struct array: an array of plain objects
|
|
3510
|
+
if (Array.isArray(dataOrCount) && dataOrCount.length > 0 &&
|
|
3511
|
+
typeof dataOrCount[0] === 'object' && !Array.isArray(dataOrCount[0])) {
|
|
3512
|
+
if (!p5.disableFriendlyErrors && dataOrCount.length > 1) {
|
|
3513
|
+
const firstKeys = Object.keys(dataOrCount[0]);
|
|
3514
|
+
let warned = false;
|
|
3515
|
+
for (let i = 1; i < dataOrCount.length; i++) {
|
|
3516
|
+
const el = dataOrCount[i];
|
|
3517
|
+
const elKeys = Object.keys(el);
|
|
3518
|
+
const sameKeys = firstKeys.length === elKeys.length &&
|
|
3519
|
+
firstKeys.every((k, j) => k === elKeys[j]);
|
|
3520
|
+
if (!sameKeys) {
|
|
3521
|
+
p5._friendlyError(
|
|
3522
|
+
`Element ${i} has different fields than element 0. ` +
|
|
3523
|
+
`All elements should have the same properties.`,
|
|
3524
|
+
'createStorage'
|
|
3525
|
+
);
|
|
3526
|
+
break;
|
|
3527
|
+
}
|
|
3528
|
+
for (const key of firstKeys) {
|
|
3529
|
+
const firstType = this._jsValueToWgslType(dataOrCount[0][key]);
|
|
3530
|
+
const elType = this._jsValueToWgslType(el[key]);
|
|
3531
|
+
if (firstType !== elType) {
|
|
3532
|
+
p5._friendlyError(
|
|
3533
|
+
`The "${key}" property of element ${i} has type ${elType} ` +
|
|
3534
|
+
`but element 0 has type ${firstType}. Proporties should have the same type across all elements.`,
|
|
3535
|
+
'createStorage'
|
|
3536
|
+
);
|
|
3537
|
+
warned = true;
|
|
3538
|
+
break;
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
if (warned) break;
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
const schema = this._inferStructSchema(dataOrCount[0]);
|
|
3545
|
+
const packed = this._packStructArray(dataOrCount, schema);
|
|
3546
|
+
const size = packed.byteLength;
|
|
3547
|
+
const buffer = device.createBuffer({
|
|
3548
|
+
size,
|
|
3549
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
|
3550
|
+
mappedAtCreation: true,
|
|
3551
|
+
});
|
|
3552
|
+
new Float32Array(buffer.getMappedRange()).set(packed);
|
|
3553
|
+
buffer.unmap();
|
|
3554
|
+
const storageBuffer = new StorageBuffer(buffer, size, this, schema);
|
|
3555
|
+
this._storageBuffers.add(storageBuffer);
|
|
3556
|
+
return storageBuffer;
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
// Determine buffer size and initial data
|
|
3560
|
+
let size, initialData;
|
|
3561
|
+
if (typeof dataOrCount === 'number') {
|
|
3562
|
+
// createStorage(count) - zero-initialized
|
|
3563
|
+
size = dataOrCount * 4; // floats are 4 bytes
|
|
3564
|
+
initialData = new Float32Array(dataOrCount);
|
|
3565
|
+
} else {
|
|
3566
|
+
// createStorage(array) - from data
|
|
3567
|
+
if (dataOrCount instanceof Float32Array) {
|
|
3568
|
+
initialData = dataOrCount;
|
|
3569
|
+
} else if (Array.isArray(dataOrCount)) {
|
|
3570
|
+
initialData = new Float32Array(dataOrCount);
|
|
3571
|
+
} else {
|
|
3572
|
+
throw new Error('createStorage expects a number or array/Float32Array');
|
|
3573
|
+
}
|
|
3574
|
+
size = initialData.byteLength;
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
// Align to 16 bytes (WGSL storage buffer alignment requirement)
|
|
3578
|
+
size = Math.ceil(size / 16) * 16;
|
|
3579
|
+
|
|
3580
|
+
// Create storage buffer with STORAGE | COPY_DST | COPY_SRC usage
|
|
3581
|
+
const buffer = device.createBuffer({
|
|
3582
|
+
size,
|
|
3583
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
|
3584
|
+
mappedAtCreation: initialData.length > 0
|
|
3585
|
+
});
|
|
3586
|
+
|
|
3587
|
+
// Write initial data if provided
|
|
3588
|
+
if (initialData.length > 0) {
|
|
3589
|
+
const mapping = new Float32Array(buffer.getMappedRange());
|
|
3590
|
+
mapping.set(initialData);
|
|
3591
|
+
buffer.unmap();
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
const storageBuffer = new StorageBuffer(buffer, size, this);
|
|
3595
|
+
|
|
3596
|
+
// Track for cleanup
|
|
3597
|
+
this._storageBuffers.add(storageBuffer);
|
|
3598
|
+
|
|
3599
|
+
return storageBuffer;
|
|
3600
|
+
}
|
|
3601
|
+
|
|
2821
3602
|
_getWebGPUColorFormat(framebuffer) {
|
|
2822
3603
|
if (framebuffer.format === FLOAT) {
|
|
2823
3604
|
return framebuffer.channels === RGBA ? 'rgba32float' : 'rgba32float';
|
|
@@ -3114,10 +3895,6 @@ ${hookUniformFields}}
|
|
|
3114
3895
|
return super.filter(...args);
|
|
3115
3896
|
}
|
|
3116
3897
|
|
|
3117
|
-
getNoiseShaderSnippet() {
|
|
3118
|
-
return noiseWGSL;
|
|
3119
|
-
}
|
|
3120
|
-
|
|
3121
3898
|
|
|
3122
3899
|
baseFilterShader() {
|
|
3123
3900
|
if (!this._baseFilterShader) {
|
|
@@ -3141,6 +3918,21 @@ ${hookUniformFields}}
|
|
|
3141
3918
|
return this._baseFilterShader;
|
|
3142
3919
|
}
|
|
3143
3920
|
|
|
3921
|
+
baseComputeShader() {
|
|
3922
|
+
if (!this._baseComputeShader) {
|
|
3923
|
+
this._baseComputeShader = new Shader(
|
|
3924
|
+
this,
|
|
3925
|
+
baseComputeShader,
|
|
3926
|
+
{
|
|
3927
|
+
compute: {
|
|
3928
|
+
'void iteration': '(index: vec3<i32>) {}',
|
|
3929
|
+
},
|
|
3930
|
+
}
|
|
3931
|
+
);
|
|
3932
|
+
}
|
|
3933
|
+
return this._baseComputeShader;
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3144
3936
|
/*
|
|
3145
3937
|
* WebGPU-specific implementation of imageLight shader creation
|
|
3146
3938
|
*/
|
|
@@ -3240,6 +4032,69 @@ ${hookUniformFields}}
|
|
|
3240
4032
|
glDataType: dataType || 'uint8'
|
|
3241
4033
|
};
|
|
3242
4034
|
}
|
|
4035
|
+
|
|
4036
|
+
compute(shader, x, y = 1, z = 1) {
|
|
4037
|
+
if (shader.shaderType !== 'compute') {
|
|
4038
|
+
throw new Error('compute() can only be called with a compute shader');
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
this._finishActiveRenderPass();
|
|
4042
|
+
|
|
4043
|
+
// Ensure shader is initialized and finalized
|
|
4044
|
+
if (!shader._compiled) {
|
|
4045
|
+
shader.init();
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
// Set default uniforms
|
|
4049
|
+
shader.setDefaultUniforms();
|
|
4050
|
+
shader.setUniform('uTotalCount', [x, y, z]);
|
|
4051
|
+
|
|
4052
|
+
// Calculate optimal workgroup size (8x8x1 = 64 threads per workgroup)
|
|
4053
|
+
const WORKGROUP_SIZE_X = 8;
|
|
4054
|
+
const WORKGROUP_SIZE_Y = 8;
|
|
4055
|
+
const WORKGROUP_SIZE_Z = 1;
|
|
4056
|
+
|
|
4057
|
+
// auto spreading: if any dimension is too large or for performance optimization,
|
|
4058
|
+
// spread total iteration count across dimensions
|
|
4059
|
+
const totalIterations = x * y * z;
|
|
4060
|
+
const MAX_THREADS_PER_DIM = 65535 * 8;
|
|
4061
|
+
|
|
4062
|
+
let px = x;
|
|
4063
|
+
let py = y;
|
|
4064
|
+
let pz = z;
|
|
4065
|
+
|
|
4066
|
+
// we spread if we exceed GPU limits OR if it involves a large 1D dispatch
|
|
4067
|
+
const exceedsLimits = x > MAX_THREADS_PER_DIM || y > MAX_THREADS_PER_DIM || z > MAX_THREADS_PER_DIM;
|
|
4068
|
+
const isLarge1D = totalIterations > 1024 && y === 1 && z === 1;
|
|
4069
|
+
|
|
4070
|
+
if (exceedsLimits || isLarge1D) {
|
|
4071
|
+
// Always use 2D square spreading (√N × √N).
|
|
4072
|
+
// Benchmarks showed 2D square equals or outperforms 3D cube at every
|
|
4073
|
+
// scale tested, with simpler index reconstruction in the shader.
|
|
4074
|
+
px = Math.ceil(Math.sqrt(totalIterations));
|
|
4075
|
+
py = Math.ceil(totalIterations / px);
|
|
4076
|
+
pz = 1;
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4079
|
+
shader.setUniform('uPhysicalCount', [px, py, pz]);
|
|
4080
|
+
|
|
4081
|
+
const workgroupCountX = Math.ceil(px / WORKGROUP_SIZE_X);
|
|
4082
|
+
const workgroupCountY = Math.ceil(py / WORKGROUP_SIZE_Y);
|
|
4083
|
+
const workgroupCountZ = Math.ceil(pz / WORKGROUP_SIZE_Z);
|
|
4084
|
+
|
|
4085
|
+
const commandEncoder = this.device.createCommandEncoder();
|
|
4086
|
+
const passEncoder = commandEncoder.beginComputePass();
|
|
4087
|
+
this.setupShaderBindGroups(shader, passEncoder, {
|
|
4088
|
+
compute: true,
|
|
4089
|
+
workgroupSize: [WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y, WORKGROUP_SIZE_Z],
|
|
4090
|
+
});
|
|
4091
|
+
|
|
4092
|
+
// Dispatch compute workgroups
|
|
4093
|
+
passEncoder.dispatchWorkgroups(workgroupCountX, workgroupCountY, workgroupCountZ);
|
|
4094
|
+
|
|
4095
|
+
passEncoder.end();
|
|
4096
|
+
this.device.queue.submit([commandEncoder.finish()]);
|
|
4097
|
+
}
|
|
3243
4098
|
}
|
|
3244
4099
|
|
|
3245
4100
|
p5.RendererWebGPU = RendererWebGPU;
|
|
@@ -3250,6 +4105,7 @@ ${hookUniformFields}}
|
|
|
3250
4105
|
fn.setAttributes = async function (key, value) {
|
|
3251
4106
|
return this._renderer._setAttributes(key, value);
|
|
3252
4107
|
};
|
|
4108
|
+
|
|
3253
4109
|
}
|
|
3254
4110
|
|
|
3255
4111
|
if (typeof p5 !== "undefined") {
|