mapAsync()

Reading Data Back with mapAsync()

Results come back through a MAP_READ | COPY_DST buffer: copy into it, submit, then await buffer.mapAsync(GPUMapMode.READ), which resolves once the GPU has finished the submitted work that uses it. mapState tracks the steps (the two-argument copyBufferToBuffer() copies a whole buffer):

A read-back and its map statesHTMLLive
<script type="module">
const device = await (await navigator.gpu.requestAdapter()).requestDevice();
const { STORAGE, COPY_SRC, COPY_DST, MAP_READ } = GPUBufferUsage;
const prices = device.createBuffer({ size: 24, usage: STORAGE | COPY_SRC | COPY_DST });
device.queue.writeBuffer(prices, 0, new Float32Array([14.99, 39.5, 24, 18.75, 16.2, 21.3]));
const read = device.createBuffer({ label: 'read', size: 24, usage: MAP_READ | COPY_DST });
const encoder = device.createCommandEncoder();
encoder.copyBufferToBuffer(prices, read);                    // whole buffer
device.queue.submit([encoder.finish()]);
const mapped = read.mapAsync(GPUMapMode.READ);
console.log(`after mapAsync(): ${read.mapState}`);
await mapped;
console.log(`after await: ${read.mapState}`);
const view = new Float32Array(read.getMappedRange());
const copy = [...view].map((p) => p.toFixed(2));             // copy before unmap()
read.unmap();
console.log(`after unmap(): ${read.mapState}, view.length ${view.length}, copy ${copy}`);
</script>

unmap() detaches the ArrayBuffer, so its views drop to length 0: copy what you need first. A mapped buffer is off-limits to the GPU, and a second mapAsync() rejected ("Buffer already has an outstanding map pending"). Never await a read-back inside a frame loop (Frame Statistics).