javascriptwebgpuwgsl

How to write to a uniform buffer using mapAsync with WebGPU


I have a uniform buffer that is created like this:

const temp = new Float32Array([0, 0, 0, 0, 0, 0, 0, 0]);
const positionBuffer = device.createBuffer({
    size: temp.byteLength,
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_SRC,
    mappedAtCreation: true,
});

const positionArray = new Float32Array(positionBuffer.getMappedRange());
positionArray.set(temp);
positionBuffer.unmap();

When I want to update the position uniform, I try to run positionBuffer.mapAsync(GPUMapMode.WRITE) but it fails with the following warning telling me to add the MAP_WRITE flag:

The buffer usages (BufferUsage::(CopySrc|Uniform)) do not contain BufferUsage::MapWrite.

But when I then add MAP_WRITE, I get a different warning:

Buffer usages (BufferUsage::(MapWrite|CopyDst|Index)) is invalid. If a buffer usage contains BufferUsage::MapWrite the only other allowed usage is BufferUsage::CopySrc.

This is stating that with MAP_WRITE, the only other allowed flag is COPY_SRC, meaning I would have to remove my UNIFORM flag.

I obviously need the uniform flag and probably the copy_dst flag (or maybe not?). I would assume that it would be better to map the buffer and directly write to GPU memory rather than any other way, so how would I achieve this?


Solution

  • I am learning WebGPU currently and had this same question. As mentioned before, the MDN documentation says that a MAP_WRITE usage can only be paired with a COPY_SRC usage and nothing else. Similarly, MAP_READ can only be paired with a COPY_DST.

    In Vulkan (I believe), you are free to create a buffer with all of these usages (if the implementation supports it). However, it is commonly not very performant to do so, and so instead people use staging buffers. This design pattern has you create a main buffer which is only GPU accessible -- this is the one you will use in your shader. You also create a second buffer accessible from both the CPU and GPU. This one is your staging buffer. You will write into your staging buffer from the CPU, then copy it to your main buffer on the GPU, and then use your main buffer in your shader.

    WebGPU seems to require this design pattern based on the restrictions to the usage flags. So to get around your issue, create a staging buffer and a main buffer like so:

    stagingBuffer = device.createBuffer({
        size:  BUFFER_SIZE,
        usage: GPUBufferUsage.MAP_WRITE |
            GPUBufferUsage.COPY_SRC,
    });
    
    mainBuffer = device.createBuffer({
        size:  BUFFER_SIZE,
        usage: GPUBufferUsage.UNIFORM  |
            GPUBufferUsage.COPY_DST,
    });
    

    You can now write data from the CPU to your staging buffer like so:

    await this.stagingBuffer.mapAsync(GPUMapMode.WRITE);
    
    let data = new Float32Array(this.stagingBuffer.getMappedRange());
    data.set(new Float32Array([ /* Whatever you like here */ ]));
    stagingBuffer.unmap();
    

    Then, during your command buffer creation, you'll want to add in the copy operation:

    commandEncoder.copyBufferToBuffer(
        stagingBuffer, 0,  // buffer, offset
        mainBuffer,    0,  // buffer, offset
        BUFFER_SIZE,
    );
    
    // Then you would use commandEncoder.beginRenderPass for example
    

    Note that I'm also a beginner with this API, so I may have made mistakes or use suboptimal design patterns. If that's the case please let me know and I'll update my answer. Hope this helps anyone out there who didn't see a clear way forward with the existing answer.