diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 2b4e6c7d82..0307104e5e 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -246,6 +246,112 @@ function rendererWebGPU(p5, fn) { } return rawCopy; } + + /** + * Updates a single element in the buffer at a given index. Use this + * when only a small number of elements need to change. If you need to + * replace all the data at once, use + * `update()` instead. + * + * ```js + * let buf; + * + * async function setup() { + * await createCanvas(100, 100, WEBGPU); + * + * // Float buffer: update one value by index + * buf = createStorage(new Float32Array([1, 2, 3, 4])); + * buf.set(2, 9.5); // only index 2 changes → [1, 2, 9.5, 4] + * + * let result = await buf.read(); + * print(result[2]); // 9.5 + * describe('Prints 9.5 to the console.'); + * } + * ``` + * + * ```js example + * let particles; + * const numParticles = 100; + * + * async function setup() { + * await createCanvas(100, 100, WEBGPU); + * particles = createStorage(makeParticles()); + * + * // Struct buffer: replace particle 42 without touching others + * particles.set(42, { + * position: createVector(0, 0), + * velocity: createVector(1, 0), + * }); + * describe('Updates a single particle in a storage buffer.'); + * } + * + * function makeParticles() { + * let data = []; + * for (let i = 0; i < numParticles; i++) { + * data.push({ + * position: createVector(random(width), random(height)), + * velocity: createVector(random(-1, 1), random(-1, 1)), + * }); + * } + * return data; + * } + * ``` + * + * @method set + * @for p5.StorageBuffer + * @beta + * @webgpu + * @webgpuOnly + * @param {Number} index The zero-based index of the element to update. + * @param {Number|Object} value The new value. Pass a number for float + * buffers, or a plain object matching the original struct layout for + * struct buffers. + */ + set(index, value) { + const device = this._renderer.device; + + if (this._schema !== null) { + // buffer was created with an array of structs + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new Error( + 'set() expects a plain object matching the original struct format for this buffer' + ); + } + + const { stride } = this._schema; + const byteOffset = index * stride; + + if (byteOffset + stride > this.size) { + throw new Error( + `set() index ${index} is out of bounds for this buffer ` + + `(buffer holds ${Math.floor(this.size / stride)} elements)` + ); + } + + // pack just this one element using the same logic as update() + const packed = this._renderer._packStructArray([value], this._schema); + // use packed.buffer (ArrayBuffer) so the size arg is always in bytes + device.queue.writeBuffer(this.buffer, byteOffset, packed.buffer, 0, stride); + } else { + // buffer was created with a float array + if (typeof value !== 'number') { + throw new Error( + 'set() expects a number for this float buffer' + ); + } + + const byteOffset = index * 4; + + if (byteOffset + 4 > this.size) { + throw new Error( + `set() index ${index} is out of bounds for this buffer ` + + `(buffer holds ${Math.floor(this.size / 4)} floats)` + ); + } + + device.queue.writeBuffer(this.buffer, byteOffset, new Float32Array([value])); + } + } } /** diff --git a/test/unit/webgpu/p5.RendererWebGPU.js b/test/unit/webgpu/p5.RendererWebGPU.js index 936ab9a853..f21503be60 100644 --- a/test/unit/webgpu/p5.RendererWebGPU.js +++ b/test/unit/webgpu/p5.RendererWebGPU.js @@ -254,4 +254,71 @@ suite('WebGPU p5.RendererWebGPU', function() { } }); }); + + suite('StorageBuffer.set()', function() { + test('updates a single float value at the given index', async function() { + const buf = myp5.createStorage(new Float32Array([1, 2, 3, 4])); + buf.set(2, 9.5); + + const result = await buf.read(); + + expect(result[0]).to.be.closeTo(1, 0.001); + expect(result[1]).to.be.closeTo(2, 0.001); + expect(result[2]).to.be.closeTo(9.5, 0.001); // only this changed + expect(result[3]).to.be.closeTo(4, 0.001); + }); + + test('updates a single struct element without touching neighbours', async function() { + const input = [ + { x: 1.0, y: 2.0 }, + { x: 3.0, y: 4.0 }, + { x: 5.0, y: 6.0 }, + ]; + const buf = myp5.createStorage(input); + + // Replace only the middle element + buf.set(1, { x: 99.0, y: 88.0 }); + + const result = await buf.read(); + + expect(result.length).to.be.at.least(3); + // Element 0 unchanged + expect(result[0].x).to.be.closeTo(1.0, 0.001); + expect(result[0].y).to.be.closeTo(2.0, 0.001); + // Element 1 updated + expect(result[1].x).to.be.closeTo(99.0, 0.001); + expect(result[1].y).to.be.closeTo(88.0, 0.001); + // Element 2 unchanged + expect(result[2].x).to.be.closeTo(5.0, 0.001); + expect(result[2].y).to.be.closeTo(6.0, 0.001); + }); + + test('set() then read() reflects the new value immediately', async function() { + const buf = myp5.createStorage(new Float32Array([0, 0, 0])); + buf.set(0, 42); + + const result = await buf.read(); + expect(result[0]).to.be.closeTo(42, 0.001); + }); + + test('throws on out-of-bounds index for float buffer', function() { + const buf = myp5.createStorage(new Float32Array([1, 2, 3])); + expect(() => buf.set(10, 5.0)).to.throw(); + }); + + test('throws on out-of-bounds index for struct buffer', function() { + const buf = myp5.createStorage([{ x: 1.0 }, { x: 2.0 }]); + expect(() => buf.set(99, { x: 3.0 })).to.throw(); + }); + + test('throws when passing a non-number to a float buffer', function() { + const buf = myp5.createStorage(new Float32Array([1, 2, 3])); + expect(() => buf.set(0, { x: 1 })).to.throw(); + }); + + test('throws when passing a non-object to a struct buffer', function() { + const buf = myp5.createStorage([{ x: 1.0 }, { x: 2.0 }]); + expect(() => buf.set(0, 42)).to.throw(); + }); + }); });