「What's next for WebGPU」(2024年11月)で語られていた内容について調べてメモる
去年の11月に GPU for the Web ワーキンググループで話し合われた内容のサマリが Chrome のブログ記事になっています。知らない概念ばっかりだったので、勉強がてらメモっていきます。
現状
WebGPU 仕様の GitHub レポジトリの Milestone 0をクローズして問題ないか、というのがこの会議の重要な議題でした。ちなみに結論としては問題なかったのでクローズとなったらしいです。
このマイルストーンは、W3C の「Candidate Recommendation」(勧告候補)に達するかどうか、という基準らしいです。W3C のプロセスでは、以下の4つのフェーズがあり、ここから W3C 勧告となるにはここからさらに2つのステップが残っています。
- Working draft
- Candidate recommendation
- Proposed recommendation
- W3C Recommendation
参考
新機能
会議の場では、新機能の優先順位についても話し合われていました。
出ていたキーワードを気になった順に見ていきます。
Bindless
現状の WebGPU(や WebGL、OpenGL)では、テクスチャなどのリソースはバインドしてからでないと使えません(setBindGroup()
みたいなやつ)。バインドは CPU 側から操作する必要があり、ここがボトルネックになりえます。特に、たくさんのテクスチャを切り替えながら使いたい、みたいな時に問題になります(テクスチャアトラスはこのためのワークアラウンドの一つ)。そこで bindless です。
bindless というのは、リソースに対して1:1でバインディングをつくるのではなく、texture array みたいなものとしてまとめて渡す、というやり方です。これだけなら「texture array でもよくない?」という感じがしますが、通常の texture array と違ってあらかじめサイズを決めなくてもよく、格納する texture のサイズもばらばらでいい?という違いがあるみたいです。この動画を見てなんかわかった気持ちになった。
↑の 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 の開発者によるメモも併せて読むと理解が深まりそうです(難しかったのでまだちゃんと読んでない)。
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 をまとめる機能です。
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 が用意されています。countBuffer
と countBufferOffset
がそのパラメータになっています。
void vkCmdDrawIndexedIndirectCount(
VkCommandBuffer commandBuffer,
VkBuffer buffer,
VkDeviceSize offset,
VkBuffer countBuffer,
VkDeviceSize countBufferOffset,
uint32_t maxDrawCount,
uint32_t stride);
たぶん、culling とかやろうとすると、インスタンス数も可変になるのでこっちの API を使うことになるようです。
実例としてはこんな感じで使うらしいです。任意形状に配置するのとか面白いテクニック。
ちなみに、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 になってるけど、概念は変わってないはず)。
この資料では、
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
曰く、「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 にするか、というのがけっこう悩むところらしいです。
Texel buffers
8ビット・16ビットの値が書き込めるテクスチャ。画像処理や ML でそういうユースケースがあるらしいです。
UMA buffer mapping
UMA = unified memory architecture な GPU、つまり iGPU の話らしいです。UMA の場合はメモリを共有しているから不要なコピーを減らせるはず、という感じの内容のようです。
アロケーションの話はけっこうややこしそうなので、仕様になるまでは通そうかも。
Discussion