mrdoob/three.js · error · Error
THREE.WebGPUAttributeUtils: ReadbackBuffer must be released
Error message
THREE.WebGPUAttributeUtils: ReadbackBuffer must be released before being used again.
What it means
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.
Source
Thrown at src/renderers/webgpu/utils/WebGPUAttributeUtils.js:392
* @return {Promise<ArrayBuffer|ReadbackBuffer>} A promise that resolves with the buffer data when the data are ready.
*/
async getArrayBufferAsync( attribute, target = null, offset = 0, count = - 1 ) {
const backend = this.backend;
const device = backend.device;
const data = backend.get( this._getBufferAttribute( attribute ) );
const bufferGPU = data.buffer;
const byteLength = count === - 1 ? bufferGPU.size - offset : count;
let readBufferGPU;
if ( target !== null && target.isReadbackBuffer ) {
const readbackInfo = backend.get( target );
if ( target._mapped === true ) {
throw new Error( 'THREE.WebGPUAttributeUtils: ReadbackBuffer must be released before being used again.' );
}
target._mapped = true;
// initialize the GPU-side read copy buffer if it is not present
if ( readbackInfo.readBufferGPU === undefined ) {
_bufferDescriptor.label = `${ target.name }_readback`;
_bufferDescriptor.size = target.maxByteLength;
_bufferDescriptor.usage = GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ;
readBufferGPU = device.createBuffer( _bufferDescriptor );
_bufferDescriptor.reset();
// release / dispose
const releaseCallback = () => {View on GitHub (pinned to da05705fa3)
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.
Example fix
// before
const rb = new THREE.ReadbackBuffer( byteLength );
setInterval( () => renderer.getArrayBufferAsync( attr, rb ), 16 ); // throws on 2nd tick
// after
const rb = new THREE.ReadbackBuffer( byteLength );
setInterval( async () => {
await renderer.getArrayBufferAsync( attr, rb );
consume( rb.buffer );
rb.release(); // unmap so rb can be reused next tick
}, 16 ); Defensive patterns
Strategy: validation
Validate before calling
// Before reusing a ReadbackBuffer, ensure the previous readback is done and released.
function canReuseReadback( renderer, readback ) {
return readback.isReadbackBuffer === true && readback._mapped === false;
}
// Usage:
if ( canReuseReadback( renderer, rb ) ) {
await renderer.getArrayBufferAsync( attr, rb );
} else {
// previous read still in flight or not released; allocate a fresh buffer
const tmp = new THREE.ReadbackBuffer( byteLength );
await renderer.getArrayBufferAsync( attr, tmp );
} Type guard
import { ReadbackBuffer } from 'three/src/renderers/common/ReadbackBuffer.js';
function isAvailableReadbackBuffer( target ) {
return target instanceof ReadbackBuffer
&& target.isReadbackBuffer === true
&& target._mapped === false;
} Try / catch
try {
await renderer.getArrayBufferAsync( attr, rb );
} catch ( err ) {
if ( /ReadbackBuffer must be released/.test( err.message ) ) {
// rb is still mapped from a prior read: release then retry once
rb.release();
await renderer.getArrayBufferAsync( attr, rb );
} else {
throw err;
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- THREE.Renderer: .compileComputeAsync() expects a ComputeNode
- THREE.Renderer: "getArrayBufferAsync()" offset and count mus
- THREE.Renderer: .compute() expects a ComputeNode.
- THREE.WebGPURenderer: ReadbackBuffer must be released before
- THREE.CubeCamera.updateCoordinateSystem(): Invalid coordinat
AI-assisted analysis of mrdoob/three.js@da05705fa3 (2026-08-12).
Data as JSON: /api/errors/b6ec0400e7952ace.
Report an issue: GitHub.