🐙

「What's next for WebGPU」(2024年11月)で語られていた内容について調べてメモる

2025/01/15に公開

去年の11月に GPU for the Web ワーキンググループで話し合われた内容のサマリが Chrome のブログ記事になっています。知らない概念ばっかりだったので、勉強がてらメモっていきます。

https://developer.chrome.com/blog/next-for-webgpu

現状

WebGPU 仕様の GitHub レポジトリの Milestone 0をクローズして問題ないか、というのがこの会議の重要な議題でした。ちなみに結論としては問題なかったのでクローズとなったらしいです。

このマイルストーンは、W3C の「Candidate Recommendation」(勧告候補)に達するかどうか、という基準らしいです。W3C のプロセスでは、以下の4つのフェーズがあり、ここから W3C 勧告となるにはここからさらに2つのステップが残っています。

  1. Working draft
  2. Candidate recommendation
  3. Proposed recommendation
  4. W3C Recommendation

参考

新機能

会議の場では、新機能の優先順位についても話し合われていました。
出ていたキーワードを気になった順に見ていきます。

Bindless

https://hackmd.io/PCwnjLyVSqmLfTRSqH0viA?view

現状の WebGPU(や WebGL、OpenGL)では、テクスチャなどのリソースはバインドしてからでないと使えません(setBindGroup() みたいなやつ)。バインドは CPU 側から操作する必要があり、ここがボトルネックになりえます。特に、たくさんのテクスチャを切り替えながら使いたい、みたいな時に問題になります(テクスチャアトラスはこのためのワークアラウンドの一つ)。そこで bindless です。

bindless というのは、リソースに対して1:1でバインディングをつくるのではなく、texture array みたいなものとしてまとめて渡す、というやり方です。これだけなら「texture array でもよくない?」という感じがしますが、通常の texture array と違ってあらかじめサイズを決めなくてもよく、格納する texture のサイズもばらばらでいい?という違いがあるみたいです。この動画を見てなんかわかった気持ちになった。

https://www.youtube.com/watch?v=YTfdBSjitd8

↑の proposal に載っていた WGSL のコード例がわかりやすかったので引用します。ちなみに binding_array はすでに reserved keyword らしいです。

@group(0) @binding(0) var<storage, read_only> materials : array<Materials>;
@group(0) @binding(1) var textures : binding_array<texture_2d<f32>>;

var<immediate> materialId : u32;
fn fs(...) -> vec4f {
    let material = materials[materialId];
    let albedoTexture = textures[material.albedoId];
    let specularTexture = textures[material.specularId];
    
    // Do something with the textures.
}

見た目は簡単そうですが、安全な API にするにはいろいろ考えることがあるみたいです。バインディングに変更を加える際に GPU 側での処理と競合しないようにしないといけない、とか。以下の wgpu の開発者によるメモも併せて読むと理解が深まりそうです(難しかったのでまだちゃんと読んでない)。

https://hackmd.io/@cwfitzgerald/wgpu-bindless

Multi-draw indirect

そもそも draw indirect とは?

draw call のパラメータを、直接指定するのではなく、GPU 上のバッファの値を参照させられる、という機能のことです。compute shader で行った計算(culling、LOD の選択など)結果をこの機能を通して使うことで、CPU を挟まず GPU だけで描画することができます。

具体的には、Vulkan の API でいうと、通常版がこういう引数なのに対して、

void vkCmdDrawIndexed(
    VkCommandBuffer  commandBuffer,
    uint32_t         indexCount,
    uint32_t         instanceCount,
    uint32_t         firstIndex,
    int32_t          vertexOffset,
    uint32_t         firstInstance);

indirect 版の API はこういう引数になっています。buffer 上にある値を offset で参照することができます。(drawCount については後述)

void vkCmdDrawIndexedIndirect(
    VkCommandBuffer  commandBuffer,
    VkBuffer         buffer,
    VkDeviceSize     offset,
    uint32_t         drawCount,
    uint32_t         stride);

Multi-draw indirect とは?

複数の draw call をまとめる機能です。

https://github.com/gpuweb/gpuweb/pull/1949

pull request の説明に書かれているように、概念としては2種類あるらしいです。

  • Allows submitting multiple draws with a single API call (multi-draw).
  • Allows the GPU to determine the number of draw calls (draw count).

実は、前者については Vulkan でいうと上と同じ API です。drawCount が 2 以上の場合、複数の draw call がひとつにまとめられることになります。後者は、この drawCount についても GPU 上のバッファから指定できるようにする、というもので、以下の API が用意されています。countBuffercountBufferOffset がそのパラメータになっています。

void vkCmdDrawIndexedIndirectCount(
    VkCommandBuffer  commandBuffer,
    VkBuffer         buffer,
    VkDeviceSize     offset,
    VkBuffer         countBuffer,
    VkDeviceSize     countBufferOffset,
    uint32_t         maxDrawCount,
    uint32_t         stride);

たぶん、culling とかやろうとすると、インスタンス数も可変になるのでこっちの API を使うことになるようです。

実例としてはこんな感じで使うらしいです。任意形状に配置するのとか面白いテクニック。

https://www.klab.com/jp/blog/creative/2024/tag-shader.html

ちなみに、WebGPU の仕様になっていないからといって実装されていないわけではありません。Firefox の状況はわかりませんでしたが、少なくとも wgpu には multi_draw_indirect()multi_draw_indirect_count() もすでに実装されています。Chrome では、公式ブログ によると v131 ですでに実装されているらしいです(draw count はまだ?)。ということで、これに関しては正式に仕様に反映されるまでにはそんなに時間はかからないかもしれません。

(参考)GPU-driven rendering

bindless も multi-draw indirect も「GPU-driven rendering」と呼ばれるテクニックの一部です。概観としては、以下の Vulkan のチュートリアルの「GPU Driven Rendering」の説明がわかりやすかったです(deprecated になってるけど、概念は変わってないはず)。

https://vkguide.dev/docs/gpudriven/gpu_driven_engines/

この資料では、

On this guide we will not use bindless textures as their support is limited, so we will do 1 draw-indirect call per material used.

ということで bindless は使っていません。texture array でもある程度は似たようなことができる(少なくとも texture をひとつひとつバインドしていくよりは効率的)ので、そこでがんばる、ということらしいです。

64-bit atomics

https://github.com/gpuweb/gpuweb/issues/4329

曰く、「compute shader rasterization (e.g. Nanite)」にはこれが必要らしいです。なんだそれは?と調べてみたところ、WebGPU での実装方法を解説しているドキュメントを発見しました。

あまり内容わかってないですが、めちゃくちゃ数が多い点群データをレンダリングするときに、ラスタライズを compute shader でやると速いらしいです。その中のどこで atomic operation を使うかというと、重なっている要素の中からカメラに一番近いものを選ぶために、値を比較して小さい方を取る、という処理を繰り返します。この処理は並列で実行されているので、race condition を防ぐために atomic にやる必要がある、ということらしいです。

In this final step we will load triangles from a simple glTF model, and then use atomic operations to ensure the triangles closest to the camera are rendered on top.

ではなぜ 64-bit なのか?、というのはこのドキュメントには書かれていません。
実際 Nanite ではどういう使い方をしているのかというと、GDC 2024 での発表でこういう説明がありました。

We rasterize the geometry into a 64bit visibility buffer using atomic max (with
32bit depth in the high bits, and 32bit triangle and visible cluster ID in the low
bits)

実際の距離には 32-bit で十分だけど、その点の ID も同時に知る必要があるので後ろにメタデータとしてくっつけている、ということらしいです。頭いいなあ。

Nanite 以外で使われているのかは探したけどよくわかりませんでした。どれくらいポピュラーな手法なんでしょう。

Subgroups and subgroup matrices

subgroup も、atomic operation と同じく GPU スレッド間でデータを共有するための仕組みらしいです。subgroup 自体は大部分の GPU でサポートされている機能だけど、そのサポートの中身はけっこう違っていて、どう portable にするか、というのがけっこう悩むところらしいです。

https://github.com/gpuweb/gpuweb/issues/3950

Texel buffers

8ビット・16ビットの値が書き込めるテクスチャ。画像処理や ML でそういうユースケースがあるらしいです。

https://docs.google.com/presentation/d/1XR6TbSuJpZ04UA6Q7cwllJNabSagpUE7lZC3dJAkx-0/edit?usp=sharing

UMA buffer mapping

UMA = unified memory architecture な GPU、つまり iGPU の話らしいです。UMA の場合はメモリを共有しているから不要なコピーを減らせるはず、という感じの内容のようです。
アロケーションの話はけっこうややこしそうなので、仕様になるまでは通そうかも。

https://github.com/gpuweb/gpuweb/issues/2388

Discussion