diff --git a/docs/parameterData.json b/docs/parameterData.json index cffc10a735..d8a0c9881f 100644 --- a/docs/parameterData.json +++ b/docs/parameterData.json @@ -3281,7 +3281,7 @@ "createStorage": { "overloads": [ [ - "Number|Array|Float32Array|Object[]" + "Number|Array|Float32Array|Uint32Array|Int32Array|Object[]" ] ] }, @@ -3334,7 +3334,12 @@ "buildComputeShader": { "overloads": [ [ - "Function" + "Function", + "Object?" + ], + [ + "Object", + "Object?" ] ] }, @@ -4612,7 +4617,7 @@ "update": { "overloads": [ [ - "Number[]|Float32Array|Object[]" + "Number[]|Float32Array|Uint32Array|Int32Array|Object[]" ] ] }, diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index d6cd1ce5f5..5f4004e227 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -2714,6 +2714,13 @@ function renderer3D(p5, fn) { * @webgpu * @webgpuOnly * @param {Function} callback A function building a p5.strands compute shader. + * @param {Object} [scope] An optional scope object passed to .modify(). + * @returns {p5.Shader} The compute shader. + */ + /** + * @method buildComputeShader + * @param {Object} hooks An object specifying p5.strands hooks in WGSL. + * @param {Object} [scope] An optional scope object passed to .modify(). * @returns {p5.Shader} The compute shader. */ fn.buildComputeShader = function (cb, context) { diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index d4d26e1df2..5107d068ba 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -3023,7 +3023,12 @@ function rendererWebGPU(p5, fn) { // Parse storage buffers while ((match = storageRegex.exec(src)) !== null) { - const [, group, binding, accessMode, name, elementType] = match; + const [, group, binding, accessMode, name, bindingType] = match; + // The regex captures the whole binding type, e.g. array> + // or array. Unwrap arrays so the element type check below + // sees just the element type. Struct names are left as-is. + const elementType = + bindingType.match(/^array<(.+?)(?:,\s*\d+)?>$/)?.[1] ?? bindingType; const groupIndex = parseInt(group); const bindingIndex = parseInt(binding); @@ -4453,6 +4458,8 @@ ${hookUniformFields}} // Resolved once when the shader was parsed const expected = parsedStorage?.expectedArrayType; if (!expected) return; + // Storage lists only hold floats or structs, so they can't mismatch + if (storageBuffer._isStorageList) return; if (storageBuffer._checkedArrayType === expected) return; storageBuffer._checkedArrayType = expected; diff --git a/test/unit/webgpu/p5.RendererWebGPU.js b/test/unit/webgpu/p5.RendererWebGPU.js index 57aa98c397..bb5f8acc96 100644 --- a/test/unit/webgpu/p5.RendererWebGPU.js +++ b/test/unit/webgpu/p5.RendererWebGPU.js @@ -711,6 +711,123 @@ suite('WebGPU p5.RendererWebGPU', function () { }); }); + suite('Storage element type check', function () { + let spy; + + // Only count the element type warning, not other friendly errors + const typeWarnings = () => + spy.mock.calls.filter(([msg]) => String(msg).includes('is declared as')); + + const atomicShader = () => + myp5.buildComputeShader({ + computeDeclarations: ` + @group(0) @binding(1) var counts: array>; + `, + 'void iteration': `(index: vec3) { + atomicAdd(&counts[index.x], 5u); + }` + }); + + beforeEach(function () { + spy = vi.spyOn(p5, '_friendlyError').mockImplementation(() => {}); + }); + + afterEach(function () { + spy.mockRestore(); + }); + + test('parses the element type out of array>', function () { + const buf = myp5.createStorage(new Uint32Array([10, 20, 30, 40])); + const shader = atomicShader(); + shader.setUniform('counts', buf); + myp5.compute(shader, 4); + + const parsed = shader._storageBuffersByName.counts; + expect(parsed.elementType).to.equal('atomic'); + expect(parsed.expectedArrayType).to.equal(Uint32Array); + }); + + test('warns once when a Float32Array is bound to array>', function () { + const buf = myp5.createStorage(new Float32Array([1, 2, 3, 4])); + const shader = atomicShader(); + shader.setUniform('counts', buf); + myp5.compute(shader, 4); + myp5.compute(shader, 4); + + const warnings = typeWarnings(); + expect(warnings).to.have.length(1); + expect(warnings[0][0]).to.contain('array>'); + expect(warnings[0][0]).not.to.contain('array>', async function () { + const buf = myp5.createStorage(new Uint32Array([10, 20, 30, 40])); + const shader = atomicShader(); + shader.setUniform('counts', buf); + myp5.compute(shader, 4); + + const result = await buf.read(); + expect(typeWarnings()).to.have.length(0); + expect(Array.from(result)).to.deep.equal([15, 25, 35, 45]); + }); + + test('does not warn for a p5.strands float buffer', function () { + const buf = myp5.createStorage(new Float32Array([1, 2, 3, 4])); + const shader = myp5.buildComputeShader( + () => { + const d = myp5.uniformStorage(); + d[myp5.index.x] = d[myp5.index.x] * 2; + }, + { myp5 } + ); + shader.setUniform('d', buf); + myp5.compute(shader, 4); + + expect(shader._storageBuffersByName.d.elementType).to.equal('f32'); + expect(typeWarnings()).to.have.length(0); + }); + + test('does not throw or warn for a StorageList drawn with instances()', async function () { + myp5.pixelDensity(1); + const list = myp5.createStorageList(5); + list.push(0.0); + const shader = myp5.buildMaterialShader( + () => { + const l = myp5.uniformStorage('l', list); + myp5.worldInputs.begin(); + myp5.worldInputs.position.x += l[myp5.instanceIndex]; + myp5.worldInputs.end(); + }, + { myp5, list } + ); + + myp5.background(0); + myp5.noStroke(); + myp5.fill(255); + myp5.shader(shader); + myp5.instances(list).rect(-10, -10, 20, 20); + + const pixel = await myp5.get(25, 25); + expect(pixel[0]).to.equal(255); + expect(typeWarnings()).to.have.length(0); + }); + + test('does not warn for a StorageList in a compute shader', function () { + const list = myp5.createStorageList(10); + const shader = myp5.buildComputeShader( + () => { + const l = myp5.uniformStorage('l', list); + l.push(1.0); + }, + { myp5, list } + ); + myp5.compute(shader, 4); + + expect(typeWarnings()).to.have.length(0); + }); + }); + suite('p5.strands', function () { test('a uniform whose name matches a hook parameter name does not break', async function () { myp5.pixelDensity(1);