---
title: "Read a GPU buffer back to the CPU in C#"
url: https://recursiveai.org/onegpu/cookbook/read-back-a-gpu-buffer/
description: "Copy a OneGpu buffer into a C# array with ReadAsync or Read. Run a complete Vulkan example, read a subrange and understand when the result is ready."
version: "1.0.0"
---

# Read a GPU buffer back to the CPU in C#

**OneGpu** is a free, open-source GPU compute and rendering library for .NET from Recursive AI, on Vulkan, Metal and WebGPU. MIT or Apache-2.0, no bundled native binaries.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneGpu.Backends) · [GitHub](https://github.com/recursive-ai-oss/onegpu-csharp) · [Docs](https://recursiveai.org/onegpu/docs/) · [Download example (.zip)](https://recursiveai.org/onegpu/cookbook/read-back-a-gpu-buffer/read-back-a-gpu-buffer.zip)

Readback copies values from a GPU buffer into a C# array. This example uses Vulkan on Windows or Linux, with a Vulkan 1.3 driver.

On Apple silicon Macs, `GpuBackends` selects Metal automatically; the buffer and readback calls stay the same.

```shell
dotnet new console
dotnet add package RecursiveAI.OneGpu.Backends
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using RecursiveAI.OneGpu;

using var device = GpuBackends.CreateDevice();
using var buffer = device.CreateBuffer<int>(4);
buffer.Write([10, 20, 30, 40]);
device.Submit(device.BeginCommands());
var values = new int[4];
await buffer.ReadAsync(values);
Console.WriteLine(string.Join(", ", values));
```

Prints the uploaded integers after readback:

```text
10, 20, 30, 40
```

`Write` queues an upload; the empty submission sends it to the GPU before the readback. A read extending beyond the buffer throws `ArgumentOutOfRangeException`.

Unzip [the example](https://recursiveai.org/onegpu/cookbook/read-back-a-gpu-buffer/read-back-a-gpu-buffer.zip), then `dotnet run`.

## Variations

With the same submitted buffer and destination, replace the await with a blocking read:

```csharp
buffer.Read(values);
```

```text
10, 20, 30, 40
```

To read only the middle two elements, replace destination creation and the await:

```csharp
var values = new int[2];
await buffer.ReadAsync(values, elementOffset: 1);
```

```text
20, 30
```

## Related recipes

- [Run a compute shader in C#](https://recursiveai.org/onegpu/cookbook/run-a-compute-shader-in-csharp/)
- [Choose a Vulkan physical device in C#](https://recursiveai.org/onegpu/cookbook/pick-a-vulkan-device/)
- [GPU matrix multiplication in C#](https://recursiveai.org/onegpu/cookbook/multiply-matrices-on-the-gpu/)

## The short answer

Use `await buffer.ReadAsync(destination)` to copy a typed GPU buffer into a C# array without blocking the calling thread while the device finishes the submitted work. Use `buffer.Read(destination)` when a blocking call is appropriate. In both cases the destination length determines how many elements are copied.

The sample deliberately uploads known integers and reads them back without a shader. That isolates the transfer and its ordering from any calculation. In a compute application, submit the pass that writes the buffer in place of the empty command submission. The same readback call then retrieves that pass’s output.

## How do you await GPU readback?

Create the destination before starting the readback, pass it to `ReadAsync`, and await the returned `ValueTask` before inspecting or reusing it. The [typed buffer API](https://github.com/recursive-ai-oss/onegpu-csharp/blob/5d3317caecb82fedd96642e54b0ab33bcb9759c5/src/RecursiveAI.OneGpu/GpuBufferOfT.cs) says the destination “must stay valid and unused until the task completes.” An array satisfies the lifetime requirement as long as your application does not modify it concurrently.

On Vulkan, readback records a copy into host-visible memory after the work submitted so far. A helper thread waits for that submission, rather than waiting for all future work. The calling thread and device can continue working; later submissions are not delayed by the wait. The task completes when the bytes have reached the supplied destination. The [readback documentation](https://recursiveai.org/onegpu/docs/compute/) also specifies completion in submission order and that device disposal finishes pending readbacks first.

Keep the device and buffer alive through the await. Do not start a readback, leave their `using` scope and then try to reuse either resource. If other application work must run before awaiting, it must not touch the destination. This API is asynchronous data transfer, not permission to share one destination between unfinished operations.

For a partial read, `elementOffset` selects the first source element and the destination length selects the count. The variation reads two integers starting at index one: twenty and thirty. There is no need to allocate a full-size destination and discard unwanted elements afterward.

## Why is GPU readback not working?

First check submission order. `Write` queues an upload to run before the next submitted commands; it does not mean that a GPU calculation has already run. The sample explicitly submits commands after `Write` and before `ReadAsync`. A compute dispatch must likewise be submitted before requesting its result.

Next check the buffer’s usage flags. Readback needs `BufferUsage.CopySource`, while uploads need `BufferUsage.CopyDestination`. The default typed compute buffers used here include both. If you replace the default usage with a custom set, keep the flags needed by your transfers and shader accesses.

Finally, check the range and the element layout. Offsets on `GpuBuffer<T>` count elements, not bytes. A request extending past the buffer throws `ArgumentOutOfRangeException` before the copy. `T` must be unmanaged and its layout must match what the shader writes. A completed copy cannot repair a mismatched shader struct or turn float bit patterns into integer values.

`Read` is the synchronous alternative: it waits for all submitted work and copies into a span. The docs recommend `ReadAsync` in a render loop or a browser. Choose the method based on where waiting is acceptable, not on whether the GPU is discrete or integrated.
