Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions src/webgpu/p5.RendererWebGPU.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,114 @@ function rendererWebGPU(p5, fn) {
}
return rawCopy;
}

/**
* Updates a single element in the buffer at the given index without
* rewriting the entire buffer. This is more efficient than `update()`
* when only one element needs to change.
*
* Uses WebGPU's `GPUQueue.writeBuffer()` with a byte offset, so the
* cost is proportional to one element rather than the whole buffer.
*
* ```js example
* 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]));
}
}
}

/**
Expand Down
67 changes: 67 additions & 0 deletions test/unit/webgpu/p5.RendererWebGPU.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});