Creating writing and reading from buffer wgpu

1.4k Views Asked by At

I have just recently started learning about how to compute on a GPU and I have decided to start with WGPU as I'm familiar with rust and it can be run on pretty much every GPU. As far as my current understanding goes first I have to create a buffer that is accessible to my CPU, which I have done with the following code.

    let array_buffer = device.create_buffer(&wgpu::BufferDescriptor{
        label: Some("gpu_test"),
        size: (arr.len() * std::mem::size_of::<[i32; 5]>()) as u64,
        usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
        mapped_at_creation: false,
    });

After that I have wrote some random data to this buffer with the following line.

queue.write_buffer(&array_buffer, 0, &[1,2,3,4]);

As of right now I have no errors but the problem appears when I now want to read the data in this buffer, there is no example of how to read the data off a buffer in wgpu docs and I didn't see one in webGPU docs too.

In addition how do I know if the buffer is accessible on CPU or GPU webGPU docs talk about it but they don't have an explicate example of how to define a buffer for each one.

2

There are 2 best solutions below

3
Kevin Reid On BEST ANSWER

First, in order to be able to read a buffer, it must have BufferUsages::MAP_READ. You've already done that. If you wanted to read a buffer that can't have that usage, you'd need to copy data to a temporary buffer first.

Given that prerequisite, the steps are:

  1. Call BufferView::map_async()
  2. Call Device::poll()
  3. Confirm that the map_async() callback has been called with a successful Result
  4. Call BufferView::get_mapped_range()

Here's the code I've used for that, which maps the buffer in the variable temp_buffer:

let (sender, receiver) = futures_channel::oneshot::channel();
temp_buffer
    .slice(..)
    .map_async(wgpu::MapMode::Read, |result| {
        let _ = sender.send(result);
    });
device.poll(wgpu::Maintain::Wait); // TODO: poll in the background instead of blocking
receiver
    .await
    .expect("communication failed")
    .expect("buffer reading failed");
let slice: &[u8] = &temp_buffer.slice(..).get_mapped_range();

Note that as per the TODO, this particular code is a work in progress halfway between two sensible states:

  • If you intend only to block on the buffer being ready to read, then you don't need a channel, just a OnceCell because the message will always have arrived when poll(Maintain::Wait) returns.
  • If you want to be truly async, then something needs to be calling either poll(Maintain::Poll) or Queue::submit() repeatedly in the background to trigger checking for completion and thus the callback.
0
Engineer On

WebGPU's API is cross-platform. So I will give you a solution from Javascript WebGPU.

A canon alternative to Kevin's approach is (GPU)CommandEncoder.copy_buffer_to_buffer() / copyBufferToBuffer(). This is described in the MDN docs, which I paraphrase and embellish:

// Create an output buffer to read GPU calculations to, and a 
// staging buffer to be mapped for CPU (JavaScript, Rust) access.
const gpuBuffer = device.createBuffer({
  size: BUFFER_SIZE,
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});

const cpuReadableBuffer = device.createBuffer({
  size: BUFFER_SIZE,
  usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});

// ...

// Create GPUCommandEncoder to encode commands to issue to the GPU.
const commandEncoder = device.createCommandEncoder();

// ...your compute dispatches...

// Copy output buffer to staging buffer.
commandEncoder.copyBufferToBuffer(
  gpuBuffer,
  0, // Source offset
  cpuReadableBuffer,
  0, // Destination offset
  BUFFER_SIZE,
);

// ...vertex and fragment shader dispatches...

// Send all commands off to the GPU for execution.
device.queue.submit([commandEncoder.finish()]);

// Finally, map and read from the CPU-readable buffer.
cpuDebugBuffer.mapAsync
(
    GPUMapMode.READ,
    0, // Offset
    debugBufferSize // Length
).then( () => { //resolves the Promise created by the call to mapAsync()
    const copyArrayBuffer = cpuDebugBuffer.getMappedRange(0, debugBufferSize);
    const data = copyArrayBuffer.slice();
    cpuDebugBuffer.unmap();

    console.log(new Float32Array(data)); //display the information.
});

I added the last parts so you can see the full workflow (lacking in MDN docs). Naturally, your (GPU)BindGroups and (GPU)BindGroupLayouts will need to match the buffers created, as will your compute shader's bindings.