This explainer describes the operations that are available on the GPUBuffer object directly.
They are mapWriteAsync, mapReadAsync and unmap which are memory mapping operations.
Assuming there is a single queue, there are two types of commands in WebGPU:
- "Buffered commands": any commands on a
GPUCommandBuffer,GPUComputePassEncoderorGPURenderPassEncoder. - "Unbuffered commands": all other commands.
Assuming there is a single queue, there is a total order on the unbuffered commands: they all execute atomically in the order they were called.
GPUQueue.submit is special because it atomically executes all the commands stored in its commands argument.
The MAP_READ and MAP_WRITE buffer creation usage flags need to be specified to create a buffer mappable for reading (resp. for writing).
An additional validation constraint is that the MAP_READ and MAP_WRITE may not be used in combination.
partial interface GPUBufferUsage {
const u32 MAP_READ = 1;
const u32 MAP_WRITE = 2;
}TODO: should MAP_WRITE be allowed only with read-only usages?
It would allow clearing the buffer only on creation and not on every map.
Buffers have an internal state machine that has three states:
- Unmapped: where the buffer can be used in queue submits
- Mapped: after a map operation and the subsequent
unmapwhere the buffer cannot be used in queue submits - Destroyed: after a call to
GPUBuffer.destroywhere it is a validation error to do anything with the buffer.
In the following a buffer's state is a shorthand for the buffer's state machine.
Buffers created with GPUDevice.createBuffer start in the unmapped state.
Buffers created with GPUDevice.createBufferMapped start in the mapped state.
State transitions are the following:
- Unmapped to destroyed: with
GPUBuffer.destroy - Mapped to destroyed: with
GPUBuffer.destroy - Unmapped to mapped: with any successful
mapReadAsyncormapWriteAsynccall. - Mapped to unmapped: with any successful
unmapcall.
The mapping operations for buffer mapping are:
partial interface GPUBuffer {
Promise<ArrayBuffer> mapReadAsync();
Promise<ArrayBuffer> mapWriteAsync();
};These calls return a promise of a "mapping" that is an ArrayBuffer that represents the content of the buffer for reading (for mapReadAsync) or writing (for mapWriteAsync).
The promise will settle before signals for the completion of follow-up unbuffered commands.
Upon success the buffer is put in the mapped state.
The following must be true or the call fails and will return a promise that will reject:
buffermust have been created with theMAP_READusage flag formapReadAsyncand theMAP_WRITEflag formapWriteAsyncbuffermust be in the unmapped state.
A buffer can be unmapped with:
partial interface GPUBuffer {
void unmap();
};Upon success the buffer is put in the unmapped state. Any associated ArrayBuffers are neutered, and any pending mapping promises are rejected.
The following must be true or the unmapping call on buffer fails:
buffermust have been created with theMAP_READor theMAP_WRITEusage flags.buffermust not be in the destroyed state (this means it is ok to callunmapon an unmapped buffer).
Calling GPUBuffer.destroy on a buffer with the MAP_READ or MAP_WRITE usage flags contains an implicit call to GPUBuffer.unmap.
Note that the mapping isn't detached when the GPUBuffer is garbage-collected, so this means that mappings keep a reference to their buffer.
What happens with the content of mappings depends of which function was used to create it:
- Mappings created with
mapReadAsyncrepresents the content of the buffer after all previous unbuffered operations before the call tomapReadAsynccompleted. Nothing happens when the mapping is detached. - Mappings created with
mapWriteAsyncare filled with zeros. When they are detached, it is as ifbuffer.setSubData(0, mapping)was called.
A buffer can be created already mapped:
partial interface GPUDevice {
(GPUBuffer, ArrayBuffer) createBufferMapped(GPUBufferDescriptor descriptor);
};GPUDevice.createBufferMapped returns a buffer in the mapped state along with an write mapping representing the whole range of the buffer.
These entry points do not require the MAP_WRITE usage to be specified.
The MAP_WRITE usage may be specified if the buffer needs to be re-mappable later on.
The mapping starts filled with zeros.
const readPixelsBuffer = device.createBuffer({
size: 4,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
// Commands copying a pixel from a texture into readPixelsBuffer are submitted
readPixelsBuffer.mapReadAsync().then((data) => {
checkPixelValue(data);
// Unmap if we want to reuse the buffer
readPixelsBuffer.unmap();
});// model is some 3D framework resource.
const size = model.computeVertexBufferSize();
const stagingVertexBuffer = device.createBuffer({
size: size,
usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
});
stagingVertexBuffer.mapWriteAsync().then((stagingData) => {
model.decompressVerticesIn(stagingData);
stagingVertexBuffer.unmap();
// Enqueue copy from the staging buffer to the real vertex buffer.
});function bufferSubData(device, destBuffer, destOffset, srcArrayBuffer) {
const byteCount = srcArrayBuffer.byteLength;
const [srcBuffer, arrayBuffer] = device.createBufferMapped({
size: byteCount,
usage: GPUBufferUsage.COPY_SRC
});
new Uint8Array(arrayBuffer).set(new Uint8Array(srcArrayBuffer)); // memcpy
srcBuffer.unmap();
const encoder = device.createCommandEncoder();
encoder.copyBufferToBuffer(srcBuffer, 0, destBuffer, destOffset, byteCount);
const commandBuffer = encoder.finish();
const queue = device.defaultQueue;
queue.submit([commandBuffer]);
srcBuffer.destroy();
}As usual, batching per-frame uploads through fewer (or a single) buffer reduces overhead.
Applications are free to implement their own heuristics for batching or reusing upload buffers:
function AutoRingBuffer(device, chunkSize) {
const queue = device.defaultQueue;
let availChunks = [];
function Chunk() {
const size = chunkSize;
const [buf, initialMap] = this.device.createBufferMapped({
size: size,
usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
});
let mapTyped;
let pos;
let enc;
this.reset = function(mappedArrayBuffer) {
mapTyped = new Uint8Array(mappedArrayBuffer);
pos = 0;
enc = device.createCommandEncoder({});
if (size == chunkSize) {
availChunks.push(this);
}
};
this.reset(initialMap);
this.push = function(destBuffer, destOffset, srcArrayBuffer) {
const byteCount = srcArrayBuffer.byteLength;
const end = pos + byteCount;
if (end > size)
return false;
mapTyped.set(new Uint8Array(srcArrayBuffer), pos);
enc.copyBufferToBuffer(buf, pos, destBuffer, destOffset, byteCount);
pos = end;
return true;
};
this.flush = async function() {
const cb = enc.finish();
queue.submit([cb]);
const newMap = await buf.mapWriteAsync();
this.reset(newMap);
};
this.destroy = function() {
buf.destroy();
};
};
this.push = function(destBuffer, destOffset, srcArrayBuffer) {
if (availChunks.length) {
const chunk = availChunks[0];
if (chunk.push(destBuffer, destOffset, srcArrayBuffer))
return;
chunk.flush();
this.destroy();
while (true) {
chunkSize *= 2;
if (chunkSize >= srcArrayBuffer.byteLength)
break;
}
}
new Chunk();
availChunks[0].push(destBuffer, destOffset, srcArrayBuffer);
};
this.flush = function() {
if (availChunks.length) {
availChunks[0].flush();
availChunks.shift();
}
};
this.destroy = function() {
availChunks.forEach(x => x.destroy());
availChunks = [];
};
};