Skip to content

Commit 2eee170

Browse files
authored
Merge pull request #8726 from aashu2006/feat/storagebuffer-read
feat(webgpu): add read() to p5.StorageBuffer with tests
2 parents d516e81 + bc4e526 commit 2eee170

2 files changed

Lines changed: 224 additions & 1 deletion

File tree

src/webgpu/p5.RendererWebGPU.js

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,78 @@ function rendererWebGPU(p5, fn) {
173173
device.queue.writeBuffer(this.buffer, 0, floatData);
174174
}
175175
}
176+
177+
/**
178+
* Reads data from a storage buffer back into JavaScript.
179+
*
180+
* Copies data from the GPU to the CPU using a temporary buffer,
181+
* so it must be awaited. Returns a `Float32Array` for number
182+
* buffers, or an array of plain objects for struct buffers.
183+
*
184+
* Note: This is a GPU -> CPU read, so calling it often (like every frame)
185+
* can be slow.
186+
*
187+
* ```js example
188+
* let data;
189+
* let computeShader;
190+
*
191+
* async function setup() {
192+
* await createCanvas(100, 100, WEBGPU);
193+
*
194+
* data = createStorage(new Float32Array([1, 2, 3, 4]));
195+
* computeShader = buildComputeShader(doubleValues);
196+
* compute(computeShader, 4);
197+
*
198+
* let result = await data.read();
199+
* // result is Float32Array [2, 4, 6, 8]
200+
* for (let i = 0; i < result.length; i++) {
201+
* print(result[i]);
202+
* }
203+
* describe('Prints the values 2, 4, 6, 8 to the console.');
204+
* }
205+
*
206+
* function doubleValues() {
207+
* let d = uniformStorage(data);
208+
* let idx = index.x;
209+
* d[idx] = d[idx] * 2;
210+
* }
211+
* ```
212+
*
213+
* @method read
214+
* @for p5.StorageBuffer
215+
* @beta
216+
* @webgpu
217+
* @webgpuOnly
218+
* @returns {Promise<Float32Array|Object[]>}
219+
*/
220+
async read() {
221+
const device = this._renderer.device;
222+
this._renderer.flushDraw();
223+
224+
const stagingBuffer = device.createBuffer({
225+
size: this.size,
226+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
227+
});
228+
229+
const commandEncoder = device.createCommandEncoder();
230+
commandEncoder.copyBufferToBuffer(this.buffer, 0, stagingBuffer, 0, this.size);
231+
device.queue.submit([commandEncoder.finish()]);
232+
233+
await stagingBuffer.mapAsync(GPUMapMode.READ, 0, this.size);
234+
const mappedRange = stagingBuffer.getMappedRange(0, this.size);
235+
236+
// Copy before unmapping because mapped memory becomes invalid after unmap
237+
const rawCopy = new Float32Array(mappedRange.byteLength / 4);
238+
rawCopy.set(new Float32Array(mappedRange));
239+
240+
stagingBuffer.unmap();
241+
stagingBuffer.destroy();
242+
243+
if (this._schema !== null) {
244+
return this._renderer._unpackStructArray(rawCopy, this._schema);
245+
}
246+
return rawCopy;
247+
}
176248
}
177249

178250
/**
@@ -3243,19 +3315,24 @@ ${hookUniformFields}}
32433315

32443316
let maxEnd = 0;
32453317
let maxAlign = 1;
3246-
const fields = entries.map(([name]) => {
3318+
const fields = entries.map(([name, value]) => {
32473319
const el = elements[name];
32483320
maxEnd = Math.max(maxEnd, el.offsetEnd);
32493321
// Alignment for scalars/vectors: <=4 -> 4, <=8 -> 8, else 16
32503322
const align = el.size <= 4 ? 4 : el.size <= 8 ? 8 : 16;
32513323
maxAlign = Math.max(maxAlign, align);
3324+
// Track original JS type for reconstruction during readback
3325+
const kind = value?.isVector ? 'vector'
3326+
: value?.isColor ? 'color'
3327+
: undefined;
32523328
return {
32533329
name,
32543330
baseType: el.baseType,
32553331
size: el.size,
32563332
offset: el.offset,
32573333
packInPlace: el.packInPlace ?? false,
32583334
dim: el.size / 4,
3335+
kind,
32593336
};
32603337
});
32613338

@@ -3282,6 +3359,65 @@ ${hookUniformFields}}
32823359
return floatView;
32833360
}
32843361

3362+
// Inverse of _packStructArray reads packed buffer back into plain JS objects
3363+
// using the same schema layout - fields, stride and offsets
3364+
_unpackStructArray(floatView, schema) {
3365+
const { fields, stride } = schema;
3366+
const dataView = new DataView(floatView.buffer);
3367+
const count = Math.floor(floatView.byteLength / stride);
3368+
const result = [];
3369+
3370+
for (let i = 0; i < count; i++) {
3371+
const item = {};
3372+
const baseOffset = i * stride;
3373+
for (const field of fields) {
3374+
const byteOffset = baseOffset + field.offset;
3375+
const n = field.size / 4;
3376+
3377+
if (field.baseType === 'u32') {
3378+
if (n === 1) {
3379+
item[field.name] = dataView.getUint32(byteOffset, true);
3380+
} else {
3381+
item[field.name] = Array.from({ length: n }, (_, j) =>
3382+
dataView.getUint32(byteOffset + j * 4, true)
3383+
);
3384+
}
3385+
} else if (field.baseType === 'i32') {
3386+
if (n === 1) {
3387+
item[field.name] = dataView.getInt32(byteOffset, true);
3388+
} else {
3389+
item[field.name] = Array.from({ length: n }, (_, j) =>
3390+
dataView.getInt32(byteOffset + j * 4, true)
3391+
);
3392+
}
3393+
} else {
3394+
const idx = byteOffset / 4;
3395+
if (n === 1) {
3396+
item[field.name] = floatView[idx];
3397+
} else {
3398+
const values = Array.from(floatView.slice(idx, idx + n));
3399+
if (field.kind === 'vector') {
3400+
item[field.name] = this._pInst.createVector(...values);
3401+
} else if (field.kind === 'color') {
3402+
// Color was packed as normalized RGBA [0-1] via _getRGBA([1,1,1,1])
3403+
// Scale back to the current colorMode range
3404+
const maxes = this.states.colorMaxes[this.states.colorMode];
3405+
item[field.name] = this._pInst.color(
3406+
values[0] * maxes[0], values[1] * maxes[1],
3407+
values[2] * maxes[2], values[3] * maxes[3]
3408+
);
3409+
} else {
3410+
item[field.name] = values;
3411+
}
3412+
}
3413+
}
3414+
}
3415+
result.push(item);
3416+
}
3417+
3418+
return result;
3419+
}
3420+
32853421
createStorage(dataOrCount) {
32863422
const device = this.device;
32873423

test/unit/webgpu/p5.RendererWebGPU.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,91 @@ suite('WebGPU p5.RendererWebGPU', function() {
167167
expect(myp5._renderer).to.exist;
168168
});
169169
});
170+
171+
suite('StorageBuffer.read()', function() {
172+
test('reads back float array data', async function() {
173+
const input = new Float32Array([1, 2, 3, 4]);
174+
const buf = myp5.createStorage(input);
175+
176+
const result = await buf.read();
177+
178+
expect(result).to.be.instanceOf(Float32Array);
179+
expect(result.length).to.equal(input.length);
180+
for (let i = 0; i < input.length; i++) {
181+
expect(result[i]).to.be.closeTo(input[i], 0.001);
182+
}
183+
});
184+
185+
test('reads back struct array data', async function() {
186+
const input = [
187+
{ x: 1.0, y: 2.0 },
188+
{ x: 3.0, y: 4.0 },
189+
];
190+
const buf = myp5.createStorage(input);
191+
192+
const result = await buf.read();
193+
194+
expect(result).to.be.an('array');
195+
expect(result.length).to.equal(input.length);
196+
for (let i = 0; i < input.length; i++) {
197+
expect(result[i].x).to.be.closeTo(input[i].x, 0.001);
198+
expect(result[i].y).to.be.closeTo(input[i].y, 0.001);
199+
}
200+
});
201+
202+
test('read after update returns new data', async function() {
203+
const buf = myp5.createStorage(new Float32Array([10, 20, 30]));
204+
const updated = new Float32Array([100, 200, 300]);
205+
buf.update(updated);
206+
207+
const result = await buf.read();
208+
209+
for (let i = 0; i < updated.length; i++) {
210+
expect(result[i]).to.be.closeTo(updated[i], 0.001);
211+
}
212+
});
213+
214+
test('reads back struct with vector fields as p5.Vector', async function() {
215+
const input = [
216+
{ position: myp5.createVector(1, 2), speed: 5.0 },
217+
{ position: myp5.createVector(3, 4), speed: 10.0 },
218+
];
219+
const buf = myp5.createStorage(input);
220+
221+
const result = await buf.read();
222+
223+
expect(result).to.be.an('array');
224+
expect(result.length).to.equal(2);
225+
// Vector fields come back as p5.Vector
226+
expect(result[0].position.isVector).to.be.true;
227+
expect(result[0].position.x).to.be.closeTo(1, 0.001);
228+
expect(result[0].position.y).to.be.closeTo(2, 0.001);
229+
expect(result[0].speed).to.be.closeTo(5.0, 0.001);
230+
expect(result[1].position.isVector).to.be.true;
231+
expect(result[1].position.x).to.be.closeTo(3, 0.001);
232+
expect(result[1].position.y).to.be.closeTo(4, 0.001);
233+
expect(result[1].speed).to.be.closeTo(10.0, 0.001);
234+
});
235+
236+
test('reads back data modified by a compute shader', async function() {
237+
const input = new Float32Array([1, 2, 3, 4]);
238+
const buf = myp5.createStorage(input);
239+
240+
const computeShader = myp5.buildComputeShader(() => {
241+
const d = myp5.uniformStorage();
242+
const idx = myp5.index.x;
243+
d[idx] = d[idx] * 2;
244+
}, { myp5 });
245+
246+
computeShader.setUniform('d', buf);
247+
myp5.compute(computeShader, 4);
248+
249+
const result = await buf.read();
250+
251+
expect(result).to.be.instanceOf(Float32Array);
252+
for (let i = 0; i < input.length; i++) {
253+
expect(result[i]).to.be.closeTo(input[i] * 2, 0.001);
254+
}
255+
});
256+
});
170257
});

0 commit comments

Comments
 (0)