DirectXの話 第187

Dynamic Resource

23/07/30 up

HLSL 6.6のDynamic Resource

HLSL 6.6から利用できる Dynamic Resource は当初 Agility SDK に含まれていた機能ですが、最新の Windows SDK ではすでに利用可能になっています。
どのバージョンから使用できるようになっているか不明ですが、10.0.2262.1.0では使用できるようになっていることを確認しています。
サンプルを実行する場合はこれ以上のバージョンのSDKを利用するようにしてください。

さて、D3D12の Root Signature はなかなか面倒な代物で、描画エンジンのRHIを作成する場合はこの部分をどう取り回すかが迷う部分になるのではないでしょうか。
現在のサンプルライブラリは第163回で解説したコピー戦略を利用しています。

この戦略を選択している理由については記事を参照していただきたいのですが、この戦略にはいくつか弱点があります。
コピー自体は無料ではありませんし、コピー元ヒープは実際に利用されるViewの数だけあればいいのですが、コピー先はDraw Call数によってかなりのサイズのヒープを確保しなければなりません。
また、テクスチャ配列を使用する場合はコピー戦略をそのまま使うのは危険で、ちょっと工夫する必要があります。
なお、私のサンプルライブラリはそのへんの工夫を行っていないので、真似してはいけません。

そんな中で生まれたのが Dynamic Resource です。
この機能はHLSL 6.6から各シェーダで利用できるようになった機能で、簡単に言ってしまえば Descriptor Heap のインデックス番号からViewへのアクセスが可能となる機能です。
仕様については以下のサイトをご参照ください。

https://microsoft.github.io/DirectX-Specs/d3d/HLSL_SM_6_6_DynamicResources.html

この機能は完全な Bindless Resource を実現できる機能と言えるでしょう。
まったくレジスタを利用しないわけではないのですが、インデックスを渡すための定数バッファさえレジスタにバインドすれば、他のリソースをレジスタにバインドする必要はなくなります。

仕様にもある通り、シェーダ側では以下のような指定をするだけでリソースを利用することができます。

C++側はRoot Signatureを作成する際にインデックスを利用できるようにするフラグを指定するくらいです。
注意点はありますが、詳細はコード解説に委ねることにしましょう。

インデックス戦略

当然ですが、ヒープのインデックスをシェーダに渡す必要があります。
サンプルライブラリではルート定数を利用することにしました。

ルート定数は Root Signature の64DWORDの内、1DWORDを利用して32ビットデータをシェーダに渡すことが可能な機能です。
サンプルライブラリでは使用してこなかったのですが、Dynamic Resource によって日の目を見ました。
実際のゲームエンジンでも使っているものはあるようですが、UEでは使用されていないようです。

今回はルート定数を利用して、各シェーダの0番レジスタに対してインデックスを直接渡すようにしました。
この手法は最も簡単だと思いますが、弱点として64個のインデックスしか渡すことができない点です。
足りない場合は、インデックスを提供する定数バッファを参照するためのインデックスを渡す方が良いかもしれません。

この図のように、ダブルポインタのようになってしまいますが、Root Signatureを固定しやすくもなるでしょう。

実装

それでは簡単ですが、実装を見てみます。
今回はサンプルライブラリのRenderTestアプリ、そしてPathTracerのレイトレーシングで Dynamic Resource に対応してみました。
PathTracerはサンプルライブラリを参照していますので、落とすのはPathTracerだけでも大丈夫です。

https://github.com/Monsho/SampleLib12
https://github.com/Monsho/PathTracerD3D12

まずはRenderTestのメッシュ描画パイプラインの頂点シェーダを例に見てみましょう。

ENABLE_DYNAMIC_RESOURCE という定義で通常のリソースバインドと Dynamic Resource を切り替えています。
ピクセルシェーダ側も同様の定義で切り替えを行っています。
頂点シェーダがインデックス2つ、ピクセルシェーダ側がインデックス5つなので64個にはかなりの余裕があります。

では、C++側も見ていきましょう。
まず、Dynamic Resource が利用できるかどうか確認する必要があります。

SM6.6が使用可能なこと、ResourceBindingTierがフェーズ3以上であることを確認します。

Descriptor Heapはシェーダから見えるようにしておく必要があります。
コピー戦略の場合、コピー元のヒープはシェーダから見えるようにしてはいけません。
これはD3D12の仕様の問題であり、D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE フラグを立てているヒープをコピー元にすると、デバッグレイヤーがエラーを返します。
ただし、NVIDIA GPUについてはエラーを無視しても動作することを確認しています。
サンプルライブラリではコピー戦略用とDynamic Resource用の2種類を用意し、Dynamic Resourceをサポートしている場合は両方にViewを登録するという無駄な仕様にしてしまっていますが、致し方なし…

Root Signatureの作成は通常とは別の関数を用意しました。
ルートパラメータが大きく違うので、分岐を利用するより別関数を用意するほうがスッキリするためです。

D3D12_ROOT_SIGNATURE_FLAGSD3D12_ROOT_SIGNATURE_FLAG_CBV_SRV_UAV_HEAP_DIRECTLY_INDEXEDD3D12_ROOT_SIGNATURE_FLAG_SAMPLER_HEAP_DIRECTLY_INDEXED を指定しています。
ルートパラメータは各ステージで利用するリソースインデックスの数を指定する形です。
この例は通常のVS-PSパイプライン(テッセレーション含む)のものですが、Mesh Shader・Compute Shaderでも同様な処理を行っています。

レイトレーシング用も基本は変わりませんが、注意点が2つあります。

1つはTLASで、TLASはViewを持たないため、Dynamic Resourceでの参照ができません。
こちらはDescriptor Tableも使用できないため致し方ないので、SRVのGPUアドレス直接指定を利用します。
直接指定は2DWORD必要なので、レイトレーシングではリソースインデックスが62個まで(ASが複数あるならそれより下がる)となります。

もう1つはローカルのRSで、こちらは前述のフラグを指定してはいけません。
ただし、グローバルRSに前述のフラグが指定されているのであれば、ヒットグループ側で Dynamic Resource が利用できます。
グローバルでは利用しないのにローカルでは利用する、という利用方法は普通やらないと思いますので問題ないでしょう。

最後に Root Signature と定数バッファの設定は以下の関数で行っています。

特に難しいことをやっているわけではないのですが、注意として設定順序が重要です。
どうやらヒープのインデックスの関連付けは Set~RootSignature関数内で解決されるらしいので、この関数を呼び出す前に利用するヒープを設定しておく必要があります。
また、ルート定数はRSの設定後にやる必要があったかと思います。
なので、

ヒープ設定 → RS設定 → ルート定数設定

の順番で設定しましょう。

レイトレーシングもグローバルリソースは同様に設定します。
ローカルリソースはシェーダテーブルにルート定数を入れておけばOKです。

描画自体は特に変化はありませんので、違いがあるのはここまでです。

なお、RenderTestもPathTracerもC++ソースの ENABLE_DYNAMIC_RESOURCE 定義を1に設定することで Dynamic Resource が有効になります。
リアルタイムで設定できるようにしていませんので、試したい方はこちらの定義を変更してビルドしてください。

ISA比較

では、NVIDIA RTX4080でのISAを比較してみます。
リソース解決している前半部分のみを、Dynamic Resourceなしとありを掲載しておきます。

通常のリソースバインドの場合、createHandleFromBinding を利用してハンドルを作成、実際に参照する前に annotateHandle を呼び出すという流れのようです。
この場合はハンドル作成が1命令で行えています。

これに対して Dynamic Resource の場合、最初にインデックス用の定数バッファは createHandleFromBinding で対応していますが、ヒープからの参照は createHandleFromHeap を用いています。
この際にインデックスを使用するわけですが、そのインデックスをレジスタに書き込んでから createHandleFromHeap で参照しています。
そのため、リソースを利用できるようになるまでの命令数が増えてしまうという状態になっているようです。
実際、Dynamic Resource版の方が8命令ほど多めです。

これがパフォーマンスに与える影響は決して大きくはないのですが、パストレーサーレベルになると少々差が出るようです。
これはCHS/AHSの命令数が増えるからではないかと思いますが、4サンプルで試したところ、1ms前後の差が出ていました。
比較すると大きな違いはあまりないものの、RTCOREとSMのThroughputが下がっていました。
命令数が増えたことによる問題ではないかと考えられます。

残念ながら、GPUパフォーマンスを考慮するのであれば Dynamic Resource は使わないほうが良さそうです。

デバッグについて

もう1つの問題として、GPUプロファイラーを利用したデバッグの問題があります。
現在のNsight Graphicsでは、Dynamic Resource に対応していないため、デバッグが非常にやりづらいです。

下の画像はRenderTestのメッシュ描画コマンドのピクセルシェーダのリソース状況になります。

テクスチャを利用しているのですが、まったく参照されていません。

ただし、PIXは対応しており、こちらではリソースの参照が可能です。

インデックス番号もわかるので、描画の不具合があっても確認はしやすいです。

ただ、Descriptor HeapとRoot Signatureの設定順序を間違えて描画に失敗していたときに、PIXではテクスチャなどが正常に見えていました。
PIX上でも描画は失敗していたのですが、参照されるリソースについてはPIX内部で描画コマンド参照時に解決されているのかもしれません。

最後に

現状のDynamic Resourceは、少なくともNVIDIA GPUではパフォーマンスに不安があり、デバッグも大変という問題があるため、あまり推奨できる手法ではないかなと思います。

また、シェーダコードを専用に書き換える必要があります。
D3D12だけを考えるのであれば問題ないのですが、マルチプラットフォームで同じシェーダコードを使うのであれば使いにくいです。

Bindless Resourceは状況によっては使える手法ですし、その状況で Dynamic Resource を使いたいという気持ちも自分自身あるのですが、今のところ無理に使うことはないと考えています。