WebGPU (Dawn) でバックバッファを画像に保存する



[Dawn](https://dawn.googlesource.com/dawn) の API が [Chrome 128](https://developer.chrome.com/blog/new-in-webgpu-128?hl=ja) あたりでいくつか変わったようで、[WebGPU C++ Guide](https://eliemichel.github.io/LearnWebGPU/basic-3d-rendering/input-geometry/playing-with-buffers.html) をはじめとしたサンプルが動きませんでした。新しい API についての情報がほとんどなかったので、作業記録を投稿します。

基本的にはこちら↓を参考にしています。

- [WebGPU Headers](https://webgpu-native.github.io/webgpu-headers/Asynchronous-Operations.html)

環境は次の通りです。

- [WebGPU-distribution](https://github.com/eliemichel/WebGPU-distribution)
- `dawn-7069` タグ

なお本件は画像比較によるビジュアルテスト用に画像を取得するために作ったものですので、実行速度への影響は考慮していません。



## 非同期処理 API の変更

以前はこんな感じでした。

```cpp
auto onBuffer2Mapped = [](WGPUBufferMapAsyncStatus status, void* pUserData) {
    // wgpuBufferGetConstMappedRange() などでマップされたメモリアドレスを得て処理する。
};
wgpuBufferMapAsync(buffer, WGPUMapMode_Read, 0, size, onBuffer2Mapped, (void*)&context);
```

これが次のようになるようです。

```cpp
auto onBuffer2Mapped = [](WGPUMapAsyncStatus status,
                          struct WGPUStringView message,
                          void* userdata1,
                          void* userdata2) {
    // wgpuBufferGetConstMappedRange() などでマップされたメモリアドレスを得て処理する。
};
WGPUBufferMapCallbackInfo callbackInfo = WGPU_BUFFER_MAP_CALLBACK_INFO_INIT;
callbackInfo.mode = WGPUCallbackMode_WaitAnyOnly;
callbackInfo.callback = onBuffer2Mapped;
callbackInfo.userdata1 = &context;
WGPUFuture future = wgpuBufferMapAsync(buffer, WGPUMapMode_Read, 0, size, callbackInfo);

// Wait
WGPUFutureWaitInfo waitInfo = WGPU_FUTURE_WAIT_INFO_INIT;
waitInfo.future = f;
waitInfo.completed = 0;
WGPUWaitStatus result = wgpuInstanceWaitAny(instance, 1, &waitInfo, 2000);
```

WGPUCallbackMode のフラグは次の3つです。

- WGPUCallbackMode_WaitAnyOnly
  - 上記の通り、 `wgpuInstanceWaitAny()` で待機する。
- WGPUCallbackMode_AllowProcessEvents
  - `wgpuInstanceProcessEvents()` を定期的に呼び出してポーリングする。
- WGPUCallbackMode_AllowSpontaneous
  - 待機処理は不要だが、これを使ってよいと明記されている API 以外は使ってはならない。

いくつかのサンプルでは `wgpuDeviceTick()` でポーリングしていますが、それだとコールバックは呼び出されませんでした。

なお上記の API を使うためには Instance に対して capabilities の指定が必要です。

```cpp
WGPUInstanceDescriptor desc = {};
desc.nextInChain = nullptr;
desc.capabilities.timedWaitAnyEnable = 1;   // これが必要
desc.capabilities.timedWaitAnyMaxCount = 8; // 無くても動いたけど、指定したほうがよさそう
m_instance = wgpuCreateInstance(&desc);
```

## バックバッファキャプチャの前準備

`wgpuSurfaceConfigure()` で `WGPUTextureUsage_CopySrc` の指定が必要です。

```cpp
WGPUSurfaceConfiguration config = WGPU_SURFACE_CONFIGURATION_INIT;
config.usage = WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_CopySrc;
// config. ... = ...
wgpuSurfaceConfigure(m_wgpuSurface, &config);
```

## キャプチャ処理

適当に書き殴ったので、コンパイルエラー等あるかもしれません。雰囲気を見ていただければと思います。

Vulkan と同じく、VRAM からホスト RAM に転送するための一時バッファが必要です。

```cpp
// 先に、描画コマンドを wgpuQueueSubmit() しておく。
// ...

WGPUInstance nativeInstance = /* ... */;
WGPUDevice nativeDevice = /* ... */;
WGPUQueue nativeQueue = /* ... */;
uint32_t pixelSize =  /* ... */;
uint32_t width =  /* ... */;
uint32_t height =  /* ... */;
uint32_t size = width * height * pixelSize;

// 一時バッファ作成
WGPUBufferDescriptor bufferDesc = WGPU_BUFFER_DESCRIPTOR_INIT;
bufferDesc.usage = WGPUBufferUsage_MapRead | WGPUBufferUsage_CopyDst;
bufferDesc.size = size;
bufferDesc.mappedAtCreation = 0;
WGPUBuffer nativeBuffer = wgpuDeviceCreateBuffer(nativeDevice, &bufferDesc);

// バックバッファから一時バッファへ転送
WGPUTexelCopyTextureInfo sourceInfo = WGPU_TEXEL_COPY_TEXTURE_INFO_INIT;
sourceInfo.texture = m_nativeTexture;
sourceInfo.mipLevel = 0;
sourceInfo.origin = { 0, 0, 0 };
sourceInfo.aspect = WGPUTextureAspect_All;
WGPUTexelCopyBufferInfo destInfo = WGPU_TEXEL_COPY_BUFFER_INFO_INIT;
destInfo.buffer = nativeBuffer;
destInfo.layout.offset = 0;
destInfo.layout.bytesPerRow = width * pixelSize;
destInfo.layout.rowsPerImage = height;
WGPUExtent3D copySize = { width, height, 1 };
WGPUCommandEncoder commandEncoder = wgpuDeviceCreateCommandEncoder(nativeDevice, nullptr);
wgpuCommandEncoderCopyTextureToBuffer(
    commandEncoder,
    &sourceInfo,
    &destInfo,
    &copySize);
WGPUCommandBuffer commandBuffer = wgpuCommandEncoderFinish(commandEncoder, nullptr);
wgpuCommandEncoderRelease(commandEncoder);
wgpuQueueSubmit(nativeQueue, 1, &commandBuffer);
wgpuCommandBufferRelease(commandBuffer);

// Queue を待機する 
auto onDone = [](WGPUQueueWorkDoneStatus status, void* userdata1, void* userdata) {
    printf("Done!!!");
};
WGPUQueueWorkDoneCallbackInfo callbackInfo1 = WGPU_QUEUE_WORK_DONE_CALLBACK_INFO_INIT;
callbackInfo1.mode = WGPUCallbackMode_WaitAnyOnly;
callbackInfo1.callback = onDone;
WGPUFuture future1 = wgpuQueueOnSubmittedWorkDone(nativeQueue, callbackInfo1);
WGPUFutureWaitInfo waitInfo1 = WGPU_FUTURE_WAIT_INFO_INIT;
waitInfo1.future = future1;
waitInfo1.completed = 0;
wgpuInstanceWaitAny(nativeInstance, 1, &waitInfo1, 2000);

// Map して読み取る
struct Context {
    WGPUBuffer buffer;
    size_t size;
};
Context context = { nativeBuffer, static_cast<size_t>(size) };
auto onBuffer2Mapped = [](WGPUMapAsyncStatus status,
                          struct WGPUStringView message,
                          void* userdata1,
                          void* userdata2) {
    Context* context = reinterpret_cast<Context*>(userdata1);
    if (status != WGPUMapAsyncStatus_Success) return;
    const uint8_t* mapping = (uint8_t*)wgpuBufferGetConstMappedRange(context->buffer, 0, 

    // ここで mapping のデータを保存する！

    wgpuBufferUnmap(context->buffer);
};
WGPUBufferMapCallbackInfo callbackInfo2 = WGPU_BUFFER_MAP_CALLBACK_INFO_INIT;
callbackInfo2.mode = WGPUCallbackMode_WaitAnyOnly;
callbackInfo2.callback = onBuffer2Mapped;
callbackInfo2.userdata1 = &context;
WGPUFuture future2 = wgpuBufferMapAsync(nativeBuffer, WGPUMapMode_Read, 0, size, callbackInfo2);
WGPUFutureWaitInfo waitInfo2 = WGPU_FUTURE_WAIT_INFO_INIT;
waitInfo2.future = future2;
waitInfo2.completed = 0;
wgpuInstanceWaitAny(m_device->nativeInstance(), 1, &waitInfo2, 2000);

// この後、このフレームを wgpuSurfacePresent() する。
```

データの保存は [こちら](https://github.com/samdauwe/webgpu-native-examples/blob/master/src/examples/screenshot.c#L586) などを参考にしてください。

とりあえずデバッガで止めてみたところ、青でクリアした色が入っていたので大丈夫だと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129567/40d1c72e-3006-4414-a14c-ca03bec2c9b0.png)

## 注意点とか

### 必ず wgpuSurfacePresent() の前でキャプチャする

`wgpuSurfacePresent()` は内部でバックバッファの Textrure を破棄して再生成するような動作をすることがあるらしく、しばらく動かしていると次のようなエラーが発生することがありました。

```
[Error] WebGPU(2): Destroyed texture [Texture "of [Surface]"] used in a submit.
```

### wgpuQueueOnSubmittedWorkDone が必要かわからなった

Vulkan の頭なので Map する前に Queue の実行を待機する必要があるのかなと思ったのですが、 WebGPU C++ Guide などいくつかのサンプルでは `wgpuQueueOnSubmittedWorkDone()` している様子はありませんでした。

実際に無くして Map したところ問題なさそうでしたが、それっぽい仕様を見つけることができなかったので、上記コードは念のため `wgpuQueueOnSubmittedWorkDone()` を入れています。

## さいごに

何かコメントや間違いなどありましたらお願いします🙏

本当に、本当に、🙏

情報が無くて、かなり苦しみながら楽しんでいます。