@@ -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
0 commit comments