Reading GPU Compute Results to CPU with WebGPU MAP_READ
Use a COPY_SRC | MAP_READ buffer, copy compute results into it, map asynchronously, read via getMappedRange, then unmap.
14 Nov 2025, 01:03 UTC

Problem and takeaway
You have finished a compute dispatch and the result sits in a GPUBuffer on the GPU. Your JavaScript code needs those numbers, but WebGPU does not expose GPU memory directly to the page. The practical takeaway is to create a separate readback buffer, copy the compute result into it with a command encoder, then map that buffer asynchronously for CPU access. Do not map the compute buffer itself unless you have confirmed the implementation allows it, and do not expect readback to be fast enough for every frame.
Requirements
- A
GPUDeviceobtained fromnavigator.gpu.requestAdapter()andrequestDevice(). - A secure context (HTTPS or localhost) with WebGPU enabled in the browser.
- A source buffer containing the compute result, created with a compute-compatible usage such as
GPUBufferUsage.STORAGE. - A readback buffer created with
GPUBufferUsage.COPY_SRCandGPUBufferUsage.MAP_READ.
Smallest suitable design
Run this on the main thread after the compute pass has finished and its commands have been submitted. Replace device, computeBuffer, and byteLength with your own values.
const readbackBuffer = device.createBuffer({
size: byteLength,
usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_READ,
});
const encoder = device.createCommandEncoder();
encoder.copyBufferToBuffer(computeBuffer, 0, readbackBuffer, 0, byteLength);
device.queue.submit([encoder.finish()]);
await readbackBuffer.mapAsync(GPUMapMode.READ, 0, byteLength);
if (readbackBuffer.mappingState !== 'mapped') {
throw new Error('Buffer did not reach mapped state');
}
const mappedArray = new Uint8Array(
readbackBuffer.getMappedRange(0, byteLength)
);
const result = new Uint8Array(mappedArray);
readbackBuffer.unmap();
GPUMapMode.READ is the WebGPU enum value for read access; MAP_READ is the buffer usage flag that permits the browser to expose the buffer to JavaScript. getMappedRange() returns an ArrayBuffer view of the mapped memory. The mapping is asynchronous because the browser must wait for the GPU to finish using the buffer and for driver-level synchronization to complete.
Trust and data boundaries
Mapping crosses a trust boundary between the GPU process and the renderer process. The browser validates buffer ownership, usage flags, and device state before exposing an ArrayBuffer to the page. JavaScript never receives a raw GPU pointer; it receives a memory region that the browser has already validated. Treat the mapped range as ordinary memory, not as direct device memory.
Operational checks
- Confirm
navigator.gpuexists before callingrequestAdapter(). - Create the readback buffer with exactly
COPY_SRC | MAP_READ; missingMAP_READcausesmapAsyncto reject. - Submit the copy command before mapping, and await
mapAsync()before reading. - Check
mappingState === 'mapped'after the promise resolves. - Read through
getMappedRange(), then callunmap()to release the mapping so the GPU can use the buffer again. - Listen for
device.lostand handle rejection of pending map operations.
Failure modes
device.lostinvalidates pending maps;mapAsyncmay reject and the buffer becomes unusable.- Calling
mapAsyncwhile a mapping is pending rejects with an error. - Reading from a mapped buffer while the GPU still expects to write to it can generate validation errors or be ignored, depending on the implementation.
- Buffer size and alignment constraints vary by browser; very large readback buffers may fail due to memory pressure.
Conditions that would change the design
- Readback every frame. A single readback buffer stalls the pipeline while the CPU reads. Use double- or triple-buffered staging buffers, or keep data on the GPU and only read back on demand.
- Image-like data. If the result is a texture, use
copyTextureToBufferinto aCOPY_SRC | MAP_READbuffer instead ofcopyBufferToBuffer. - Low-latency synchronous access. WebGPU does not support synchronous mapping. If your algorithm cannot tolerate asynchronous readback, redesign to avoid CPU readback.
Limitations and verification
WebGPU implementations in Chrome, Firefox, and Safari are still evolving. Exact error messages, mapping limits, and device-lost semantics may differ. This note describes well-established WebGPU behavior, not a specific browser version.
Practical check: after unmapping, create a new command encoder and issue a copy that uses readbackBuffer as COPY_SRC. If the submission succeeds without a validation error, the mapping was fully released. If you see a validation error or device loss, the buffer may still be mapped or the device may be compromised.
Rollback: mapping changes buffer state. If mapAsync rejects or you abandon the read, call unmap() only if mappingState is 'mapped'; otherwise let the rejected promise drop and recreate the buffer if the device remains valid.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.