{"record":{"id":"b6ec0400e7952ace","repo":"mrdoob/three.js","slug":"three-webgpuattributeutils-readbackbuffer-must-be","errorCode":null,"errorMessage":"THREE.WebGPUAttributeUtils: ReadbackBuffer must be released before being used again.","messagePattern":"THREE\\.WebGPUAttributeUtils: ReadbackBuffer must be released before being used again\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/renderers/webgpu/utils/WebGPUAttributeUtils.js","lineNumber":392,"sourceCode":"\t * @return {Promise<ArrayBuffer|ReadbackBuffer>} A promise that resolves with the buffer data when the data are ready.\n\t */\n\tasync getArrayBufferAsync( attribute, target = null, offset = 0, count = - 1 ) {\n\n\t\tconst backend = this.backend;\n\t\tconst device = backend.device;\n\n\t\tconst data = backend.get( this._getBufferAttribute( attribute ) );\n\t\tconst bufferGPU = data.buffer;\n\t\tconst byteLength = count === - 1 ? bufferGPU.size - offset : count;\n\n\t\tlet readBufferGPU;\n\t\tif ( target !== null && target.isReadbackBuffer ) {\n\n\t\t\tconst readbackInfo = backend.get( target );\n\n\t\t\tif ( target._mapped === true ) {\n\n\t\t\t\tthrow new Error( 'THREE.WebGPUAttributeUtils: ReadbackBuffer must be released before being used again.' );\n\n\t\t\t}\n\n\t\t\ttarget._mapped = true;\n\n\t\t\t// initialize the GPU-side read copy buffer if it is not present\n\t\t\tif ( readbackInfo.readBufferGPU === undefined ) {\n\n\t\t\t\t_bufferDescriptor.label = `${ target.name }_readback`;\n\t\t\t\t_bufferDescriptor.size = target.maxByteLength;\n\t\t\t\t_bufferDescriptor.usage = GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ;\n\n\t\t\t\treadBufferGPU = device.createBuffer( _bufferDescriptor );\n\n\t\t\t\t_bufferDescriptor.reset();\n\n\t\t\t\t// release / dispose\n\t\t\t\tconst releaseCallback = () => {","sourceCodeStart":374,"sourceCodeEnd":410,"githubUrl":"https://github.com/mrdoob/three.js/blob/da05705fa31f02710c19772a2aa70c7454807f0c/src/renderers/webgpu/utils/WebGPUAttributeUtils.js#L374-L410","documentation":"Thrown by WebGPUAttributeUtils.getArrayBufferAsync when a ReadbackBuffer passed as the target is still in the mapped state. The method sets target._mapped = true at the start of a readback (line 396) and only resets it to false when the buffer's 'release' or 'dispose' event fires (lines 413, 422). WebGPU forbids a buffer with MAP_READ usage from being re-used for a new copy-then-map cycle while a previous mapping is still outstanding, so the library treats a second call as a programmer error rather than queuing it.","triggerScenarios":"Calling renderer.getArrayBufferAsync(attribute, sameReadbackBuffer) a second time before the first promise has resolved, or after it resolved but before calling target.release(). Also: reusing one ReadbackBuffer across multiple concurrent compute-shader readbacks, or keeping a reference to it in a loop that re-reads without releasing between iterations.","commonSituations":"Polling a storage buffer attribute each frame (e.g. GPU particle positions, transform feedback, compute output) with a single shared ReadbackBuffer and forgetting to release it after consuming target.buffer. Refactoring code that previously passed a plain ArrayBuffer (which auto-destroys each call, line 496) to reuse a ReadbackBuffer for performance without adding the release() step. Async/await ordering bugs where the next readback is dispatched before the prior await resolves.","solutions":["After the getArrayBufferAsync promise resolves, read target.buffer, then call target.release() before issuing the next readback with the same ReadbackBuffer.","Await the previous readback promise fully before calling getArrayBufferAsync again with the same target, so the mapped buffer is consumed in order.","If concurrent readbacks are needed, allocate a separate ReadbackBuffer instance per outstanding read instead of reusing one.","If you no longer need the buffer, call target.dispose() to free the GPU resource and reset _mapped."],"exampleFix":"// before\nconst rb = new THREE.ReadbackBuffer( byteLength );\nsetInterval( () => renderer.getArrayBufferAsync( attr, rb ), 16 ); // throws on 2nd tick\n\n// after\nconst rb = new THREE.ReadbackBuffer( byteLength );\nsetInterval( async () => {\n  await renderer.getArrayBufferAsync( attr, rb );\n  consume( rb.buffer );\n  rb.release(); // unmap so rb can be reused next tick\n}, 16 );","handlingStrategy":"validation","validationCode":"// Before reusing a ReadbackBuffer, ensure the previous readback is done and released.\nfunction canReuseReadback( renderer, readback ) {\n  return readback.isReadbackBuffer === true && readback._mapped === false;\n}\n\n// Usage:\nif ( canReuseReadback( renderer, rb ) ) {\n  await renderer.getArrayBufferAsync( attr, rb );\n} else {\n  // previous read still in flight or not released; allocate a fresh buffer\n  const tmp = new THREE.ReadbackBuffer( byteLength );\n  await renderer.getArrayBufferAsync( attr, tmp );\n}","typeGuard":"import { ReadbackBuffer } from 'three/src/renderers/common/ReadbackBuffer.js';\n\nfunction isAvailableReadbackBuffer( target ) {\n  return target instanceof ReadbackBuffer\n    && target.isReadbackBuffer === true\n    && target._mapped === false;\n}","tryCatchPattern":"try {\n  await renderer.getArrayBufferAsync( attr, rb );\n} catch ( err ) {\n  if ( /ReadbackBuffer must be released/.test( err.message ) ) {\n    // rb is still mapped from a prior read: release then retry once\n    rb.release();\n    await renderer.getArrayBufferAsync( attr, rb );\n  } else {\n    throw err;\n  }\n}","preventionTips":["Always pair each getArrayBufferAsync(rb) with rb.release() after consuming rb.buffer.","Await the readback promise before issuing the next one with the same ReadbackBuffer.","Use a pool of ReadbackBuffer instances (one per concurrent read) instead of one shared buffer.","Treat _mapped === true as an invariant you own; log a warning if you ever see it set when you did not expect a read in flight."],"tags":["webgpu","readback","gpu-buffer","compute","resource-lifecycle"],"backgroundTag":null,"analyzedSha":"da05705fa31f02710c19772a2aa70c7454807f0c","analyzedAt":"2026-08-12T22:51:37.160Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}