DirectXの話 第198回
Visibility Bufferの一部処理をWork Graphに変更する
24/12/20 up
今回はVisibility Bufferサンプルの一部処理をWork Graphで実装してみました。
また、この動作を検証し、パフォーマンス的な問題やWork Graphの挙動についても考察してみようと思います。
どこをWork Graph化するか?
今回Work Graph化した場所はVisibility BufferからGBufferを生成する部分です。
この処理はVisibility Bufferの記事を参照してもらえれば実際のコードについての解説も行われています。
一応ここでも簡単に解説しておきます。
Visibility Bufferの元論文ではGBufferを生成しないのですが、グラフベースマテリアルのようにテクスチャだけで対応できない多くのマテリアルが存在するような場合にはGBufferを生成したほうが有利な場合が多いです。
また、GBufferを利用するポストプロセスも多くあるので、その点でもGBufferが欲しくなるでしょう。
そこで、メッシュを描画するタイミングではVisibility Bufferを生成し、その後にGBufferに変換するという処理をすることがあります。
わかりやすい事例としてはUnreal Engine 5がありますね。
この変換方法はいくつかありますが、私のサンプルではUE5と同じような手法を用いています。
まずVisibility Bufferから各ピクセルのマテリアルIDを取得し、これを深度バッファに書き込みます。
深度バッファといいますが、深度として利用するのではなく、マテリアルIDを深度に変換するという方法となります。
この処理は深度バッファへの書き込みを行うのでグラフィクスパイプラインを用います。
次に画面をタイル分割し、そのタイルごとにマテリアルの分類を行います。
そのタイルに存在するマテリアルIDを調査し、そのマテリアルIDのインダイレクト描画変数に書き込みを行います。
この処理はコンピュートパイプラインです。
最後に各マテリアルを前段で生成したインダイレクト描画変数を用いて描画します。
これによって、そのマテリアルが存在するタイルのみただの板が描画されます。
板の深度値はマテリアルIDを深度値に変換した値を利用しますので、最初のパスで生成した深度バッファと板の深度値が同一になるピクセルのみシェーダが起動します。
この処理は当然グラフィクスパイプラインを用います。
GBuffer生成処理は3パスで構成されており、途中でタイル描画に利用する深度バッファやインダイレクト描画変数のバッファなどが必要になります。
しかし、このような何らかの情報から分類を行い、その分類ごとに処理を行うのはWork Graphで期待されるユースケースとしては一般的なものになります。
というわけで、こちらをWork Graphで実装していこうと思います。
Work Graphシェーダの実装
実装サンプルは今までのVisibility Bufferサンプルと同様です。
ライブラリの更新も行われていますので、必要に応じてこちらも最新に更新してください。
今回のWork Graphノードは3つで、エントリーポイントではマテリアルIDを求め、そこからマテリアルの種類に応じて起動するノードを変更します。
といっても、マテリアルノードは2つだけ、しかもほぼ1つしか使っていないという状態です。
実質、ノードは2つということになります。
まずはエントリーポイントとなるDistributeMaterialNodeは以下となります。
このノードは Broadcasting Node となり、画面のピクセル数分だけ起動します。
マテリアルIDを深度値に変換する material_depth.p.hlsl シェーダと同様に、まずマテリアルIDをVisibility Bufferから取得しています。
その後、マテリアルによって実行するノードが切り替わりますので、一旦マテリアルデータを取得します。
マテリアルデータには使用するノードインデックスが shaderIndex パラメータに格納されているので、これをそのままマテリアルノード配列のインデックスとして用います。
マテリアルノードに渡すのはマテリアルIDとピクセル位置のみです。
次にマテリアルノードです。
といっても、こちらはほとんど material_tile.p.hlsl をコンピュートシェーダに変換しただけのものです。
マテリアルノードは Thread Node となります。
これはメインとなるマテリアルで、テクスチャをサンプルして各情報を計算している部分は省略しています。
マテリアルタイルとの違いはコンピュートシェーダで実行されているという点、そしてテクスチャはテクスチャ配列を利用しているという点です。
コンピュートシェーダなのでGBufferをUAVとして利用できるようにしているのですが、SRGBフォーマットではUAVとして利用することができないため、ベースカラーの’フォーマットをSRGBなしとしています。
あとはWork Graphを使用するパスを追加し、マテリアルタイルとWork Graphのどちらを利用するかを選択することができるようにしてやればOKです。
こちらの実装は特に難しいことはしていませんので省略します。
パフォーマンスについて
それではパフォーマンスを見てみましょう。
利用するのはいつもどおりにNsightのGPU Traceです。
上部が従来のマテリアルタイル描画手法、下部がWork Graph手法です。
従来手法は合計が0.28msです。
内訳としては、マテリアルIDを深度バッファに書き込むのが0.04ms、分類が0.04ms、タイル描画が0.2msです。
深度バッファパスと分類パスは解像度依存になるので、同一解像度であればあまり大きく違いは出ないのではないかと思います。
といってもポリゴン情報へのアクセスがあるので、ポリゴン数が増えるとその分キャッシュヒットミスが発生するなどでパフォーマンスに差が出るかもしれません。
タイル描画はマテリアルの複雑さ、マテリアル数に依存します。
今回はシェーダ自体は同一、テクスチャリソースは切り替えてDraw Callを行っています。
マテリアルの計算は比較的簡単ですが、Visibility Bufferから頂点シェーダ出力と同じものを取得するための計算がそれなりに複雑です。
実際、マテリアルタイルパスではSM Throughputがトップとなっています。
これに対してWork Graph版は単体で1.05msです。従来版の4倍近くとなっています。
Virtual Geometryのサンプルでもそうでしたが、現在のNVIDIA GPUではWork Graphが十分な速度で動作するという状態ではなさそうです。
考察
Work Graph版のGPU Trace結果はなかなか興味深い状態になっています。
Top-Level Throughputの結果を見てみると、だいぶギザギザした結果が出ていることがわかります。
通常、単一のシェーダではこのような結果は出ませんし、メッシュ描画でもある程度の上下はあるものの、このような短期での振動は普通は見かけません。
ギザギザの頂点になっている部分の赤紫はSM Throughputです。
高い場合は60%ほどになっていて、マテリアルタイルパスと近い値です。
そこから、この場所ではマテリアル計算が行われていると考えられます。
SM Throughputが高い場所では2番目に重いのがL2 Throughputになっていることがほとんどですので、テクスチャサンプルもそれなりに行われていると考えられますし、この考えは間違っていないと思います。
これがギザギザに出ているということは、DistributeMaterialNodeが実行され、ある程度のThread Node起動が溜まったらWarpに固めて実行する、という流れになっているのだろうと思います。
これは理屈上では正しそうですが、GPUの挙動の面で見るとだいぶ無駄が多いように思います。
可能であれば、DistributeMaterialNodeがすべてのピクセルに対して実行されてからマテリアル計算用のThread Nodeが起動してほしいのですが、そのような動作になっていないのだろうと思います。
もちろん、そのように動作させるのであれば中間メモリも大量に必要になりそうなので、メモリ的な利点が失われる可能性が高いです。
この考察は当たらずとも遠からずという感じではないかと思います。
まとめ
Work Graphへの書き換えは慣れてくれば比較的簡単だと感じますが、リソースの取り扱いは変更が必要な場面が多いです。
この辺はDynamic Resourceを利用することでだいぶ簡単になるのではないかとは思いますが、従来のレジスタ方式では少し面倒です。
可能なかぎりバインドレスリソースに変更すべきでしょう。
しかしパフォーマンス面は問題があり、現段階では検証に使うくらいしかできません。
今後様々な機能が追加されたり、ドライバがこなれてくれば十分に高速に動作するかもしれません。
あと、AMD GPUでもテストしてみたい気持ちはありますね。
GPU自体はちょっと古めのは持っているのですが、指すためのPCがなくてですね…
まあ、そのへんはおいおい。
Work Graphはまだ現段階ではNVIDIA GPUについては高速とは言えないようです。
もちろん、1つのメッシュだけでなくもっと多くのメッシュを一度に処理するようにすればもう少し改善するかもしれませんが、CS版とパフォーマンスが逆転するかどうかは不明です。
AMD GPUであればもっと良い結果が得られるかもしれませんが、どうなんでしょうか?
まとめ
LOD選択をGPUで実装してみましたが、期待しているWork Graphはまだまだ発展途上のようです。
他の方の検証でもWork Graphはパフォーマンスが悪くなっているようなので、期待通りの結果が得られるのはもうちょっと先になりそうです。
今のところは素直にIndirect Draw/Dispatchを使っておきましょう。