mrdoob/three.js · error · Error

THREE.WebGPURenderer: ReadbackBuffer must be released before

Error message

THREE.WebGPURenderer: ReadbackBuffer must be released before being used again.

What it means

Thrown when calling readback with a ReadbackBuffer target that is still mapped (`_mapped === true`) from a previous readback. WebGL has no native mapped-buffer concept, so three emulates it; a ReadbackBuffer must be released (its 'release'/'dispose' event fired) before it can be reused for another readback. The error prevents overwriting a buffer the caller may still be consuming.

Source

Thrown at src/renderers/webgl-fallback/utils/WebGLAttributeUtils.js:289

		const { gl } = backend;

		const bufferAttribute = attribute.isInterleavedBufferAttribute ? attribute.data : attribute;
		const attributeInfo = backend.get( bufferAttribute );
		const { bufferGPU } = attributeInfo;

		const byteLength = count === - 1 ? attributeInfo.byteLength - offset : count;

		// read the data back
		let dstBuffer;
		if ( target === null ) {

			dstBuffer = new Uint8Array( new ArrayBuffer( byteLength ) );

		} else if ( target.isReadbackBuffer ) {

			if ( target._mapped === true ) {

				throw new Error( 'THREE.WebGPURenderer: ReadbackBuffer must be released before being used again.' );

			}

			const releaseCallback = () => {

				target.buffer = null;
				target._mapped = false;
				target.removeEventListener( 'release', releaseCallback );
				target.removeEventListener( 'dispose', releaseCallback );

			};

			target.addEventListener( 'release', releaseCallback );
			target.addEventListener( 'dispose', releaseCallback );

			// WebGL has no concept of a "mapped" data buffer so we create a new buffer, instead.
			dstBuffer = new Uint8Array( new ArrayBuffer( byteLength ) );
			target.buffer = dstBuffer.buffer;

View on GitHub (pinned to da05705fa3)

Solutions

  1. Release the ReadbackBuffer after consuming its data: `readbackBuffer.release()` (or let it dispose) before reusing.
  2. Await the prior readback promise to completion before issuing the next one with the same target.
  3. Use a pool/rotation of two or more ReadbackBuffers to overlap reads instead of reusing one synchronously.
  4. Pass `null` as target to allocate a fresh ArrayBuffer each readback if reuse isn't required.

Example fix

// before
const rb = new ReadbackBuffer();
renderer.readback(attr, 0, -1, rb);
renderer.readback(attr, 0, -1, rb); // throws - still mapped

// after
const rb = new ReadbackBuffer();
renderer.readback(attr, 0, -1, rb);
rb.release(); // unmap
renderer.readback(attr, 0, -1, rb);
Defensive patterns

Strategy: validation

Validate before calling

function assertReadbackBufferFree(rb) {
  if (rb && rb.isReadbackBuffer && rb._mapped === true) {
    throw new Error('ReadbackBuffer is still mapped; call release() before reuse');
  }
}

Type guard

function readbackBufferIsFree(rb) {
  return !rb || !rb.isReadbackBuffer || rb._mapped !== true;
}

Try / catch

try {
  renderer.readback(attr, 0, -1, rb);
} catch (e) {
  if (/ReadbackBuffer must be released/.test(e.message)) { rb.release(); renderer.readback(attr, 0, -1, rb); }
  else throw e;
}

Prevention

When it happens

Trigger: Calling renderer.compute/com readback APIs (e.g. readbackBuffer, readback) twice in succession with the same ReadbackBuffer target without releasing it first. Holding the ReadbackBuffer across multiple frames and re-submitting it each frame without awaiting/completing the prior read.

Common situations: GPU readback loops (transform feedback, compute buffer readback) where the buffer is reused. Async readback where the caller doesn't await resolution before issuing the next readback. Forgetting to call release()/dispose() on the ReadbackBuffer after consuming its data.

Related errors


AI-assisted analysis of mrdoob/three.js@da05705fa3 (2026-08-12). Data as JSON: /api/errors/9264fc35f454f216. Report an issue: GitHub.