WGSL のバインドグループを整理する
パイプラインが増えても破綻しないバインドグループ設計を、失敗例を交えて。
WebGPU では、シェーダから見えるリソースをバインドグループという単位でまとめます。
最初は何も考えずに @group(0) に全部詰めていて、パイプラインが 5 本を超えたあたりで
破綻しました。その反省です。
何が問題だったか
全部を 1 つのグループに入れると、リソースが 1 つでも違えば別のバインドグループが必要になります。
カメラ行列(全パイプライン共通)、マテリアル(パイプラインごと)、モデル行列(描画ごと)を 同じグループに置いていたため、オブジェクトを 1 つ描くたびにカメラ行列まで含めた バインドグループを作り直していました。
更新頻度で分ける
解決策は単純で、更新頻度ごとにグループを分けることです。
| グループ | 中身 | 更新のタイミング |
|---|---|---|
@group(0) |
カメラ、時刻、画面サイズ | フレームごとに 1 回 |
@group(1) |
マテリアル、テクスチャ | パイプライン切り替え時 |
@group(2) |
モデル行列、インスタンス情報 | 描画ごと |
@group(0) @binding(0) var<uniform> frame: FrameUniforms;
@group(1) @binding(0) var<uniform> material: MaterialUniforms;
@group(1) @binding(1) var albedo: texture_2d<f32>;
@group(2) @binding(0) var<uniform> model: ModelUniforms;
こうしておくと、setBindGroup(0, ...) はフレームの頭で 1 回呼ぶだけで済みます。
描画ループの中で作り直すのは group 2 だけです。
WebGPU の仕様上、バインドグループは最低 4 つ使えることが保証されています。 3 つに収める設計なら、どの環境でも動きます。
レイアウトは共有する
GPUBindGroupLayout は、パイプライン間で使い回せます。
group 0 のレイアウトは全パイプラインで同じものを渡すべきです。
const frameLayout = device.createBindGroupLayout({ entries: [...] });
const pipeline = device.createRenderPipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [frameLayout, materialLayout, modelLayout],
}),
// ...
});
layout: 'auto' は最初の 1 本を書くときは楽ですが、パイプラインごとに
別のレイアウトオブジェクトが生成されるため、バインドグループを共有できません。
2 本目を書き始めたら明示的なレイアウトに切り替えるのが結果的に早いです。
動的オフセットを使うかどうか
描画ごとのモデル行列は、大きなユニフォームバッファに全部詰めて、 動的オフセットで参照先を切り替える手もあります。
pass.setBindGroup(2, modelBindGroup, [i * alignedSize]);
バインドグループの生成を 1 回に減らせるので効きますが、オフセットは
minUniformBufferOffsetAlignment(多くの環境で 256 バイト)の倍数である必要があります。
64 バイトの行列 1 つのために 256 バイト使うことになるので、
描画数が少ないうちは素直にバインドグループを分けたほうが読みやすいです。