おはようございます。
エンジニアの yamamoto-mo です。
お久しぶりです!…本当にお久しぶりです…
気付けば 1 年半経ってますよ…信じられないですね…
何かの間違いでは?と思いましたが、これまでの記事を読み直していたところ、
正直色々忘れてしまっていたので、「やはり 1 年半経ってるんだな…」と改めて痛感しました。
この記事を作成している今、ゴールデンウィーク真っ只中です。
連休中に少し時間があるので、
どうせなら放置(?)してしまっていた続きを少し進めようと思います!
さて、前回はコンピュート処理とインスタンシング描画を組み合わせて、
大量のオブジェクトの描画負荷を軽減する所までやりましたよね。
今回のゴールはずばり
ちゃんとしたインダイレクト描画をやる
です。
では早速行きましょう。
サンプルプログラム
サンプルプログラムはこちらです。
前回のプログラムを少し変更して作成しています。
デフォルトでは GPU カリング、
1 キーを押下している間だけ CPU カリングなのも同じです。
インダイレクト描画とは
まず必要なのは「何をどうすればインダイレクト描画といえるのか」を把握する事です。
前回のブログでも触れていますが、インダイレクト描画を実現するには、
CPU と GPU の行き来を減らすことが必要になります。
前回のサンプルプログラムでは、
コンピュートシェーダでカリングと描画インスタンス数を計算した後、
その描画数を CPU で受け取り、CPU から DrawCall を行う形になっています。
ところがこの方法だと、
CPU から Dispatch → GPU でカリング計算 → GPU の計算完了待ち → CPU で結果取得 → CPU から個数指定で DrawCall → GPU 描画完了待ち
の流れになり、描画前に必ず「GPU の計算完了待ち → CPU で結果取得」という
CPU の待機時間が発生してしまいます。
これが無駄なんですよね。
GPU で計算した内容を、CPU を経由せずそのまま GPU の描画処理に渡せた方が早いわけです。
それが出来れば晴れて「インダイレクト描画を実現した」といえる感じですね!
というわけで「GPU の計算完了待ち → CPU で結果取得」を無くす方法を考えます。
描画関数の変更
「CPU の待機時間を無くす」と簡単に言ってますが、
描画するインスタンス数は GPU の計算を待って結果を受け取らないと分かりません。
となると CPU が描画数を知らないまま DrawIndexedInstanced を呼び出す事になります。
DrawIndexedInstanced の引数で描画数を指定している限り無理では?と思われるかもしれません。
結論から言うとその通りです。
インダイレクト描画を行う場合は DrawIndexedInstanced を使いません。
代わりに ExecuteIndirect を利用します。
ExecuteIndirect は描画数などを引数で直接指定せず、
「引数の情報をまとめたバッファ」を引数として指定する関数です。
また、引数で指定するバッファは GPU リソース(UAV として書き込み可能なバッファ)です。
そのため、コンピュートシェーダで書き込んだ内容を CPU に戻すことなく、
そのまま ExecuteIndirect に渡すことができます。
つまり、コンピュートシェーダで計算した描画数をそのバッファに書き込み、
ExecuteIndirect の引数に指定できれば、CPU を介さずに描画数を指定できるという事になります。
ExecuteIndirect を利用できる形に整えていく必要があります。
順に対応内容を確認していきます。
ExecuteIndirect の引数バッファ作成
まず必要なのは引数に指定する「引数バッファ」の作成です。
最初にバッファを定義します。
今回、引数バッファには下記二つの情報を載せます。
・描画するインスタンスインデックスリストの GPU 仮想アドレス
・DrawIndexedInstanced に対応する引数情報構造体
引数バッファ1つ目の情報
1つ目は drawInstanceIndexes の GPU 仮想アドレスです。
サンプルでは初期化以降アドレスが変わる事がないのでここで代入しています。
前回まで drawInstanceIndexes は、
コンピュートパスでは UAV としてコンピュートシェーダから書き込まれ、
描画パスでは SRV として描画対象の取得に使われていました。
(上記リンクは前回のサンプルです)
今回、コンピュートパスは UAV として書き込みが必要なのでそのままですが、
描画パスでは引数バッファのフォーマットに含めた事で、
SRV の作成や描画パスでのディスクリプタ設定が必要なくなりました。
ただしこの構成に変更する場合、
描画用ルートパラメータの変更も必要になりますので注意して下さい。
引数バッファ2つ目の情報
次に後者ですが、これが描画関数の引数に対応する構造体です。
D3D12_DRAW_INDEXED_ARGUMENTS 型の内容を確認すればわかりますが、
DrawIndexedInstanced の引数と同じ情報が入れられるようになっています。
必ずこの構造体を利用するわけではなく、置き換える描画関数によって構造体は変わります。
今回は DrawIndexedInstanced のインダイレクト描画を行いたいから、
この構造体を指定している感じですね。
仮に DrawInstanced のインダイレクト描画なら D3D12_DRAW_ARGUMENTS を利用する必要があります。
これらを考慮して引数バッファの構造体を定義し、フォーマットを決めます。
フォーマットが決まれば、コンピュートシェーダから書き込めるように、
UnorderedAccess リソースを作成します。
コマンドシグネチャの作成
引数バッファは出来たので、これを ExecuteIndirect の引数に渡せば OK!…とはなりません。
ExecuteIndirect に引数バッファを正しく解釈させる為のシグネチャである、
コマンドシグネチャが必要になります。
そのシグネチャの定義がこちらです。
この関数を呼び出し、その中で今回の引数バッファに対応したシグネチャを作成しています。
引数バッファ構造体には二つのメンバがあるので、
D3D12_INDIRECT_ARGUMENT_DESC も二つ用意し、
1つ目は「SRV として扱い、対応するルートパラメータ(シェーダスロット)番号を指定するもの」、
2つ目は「DrawIndexedInstanced の引数を指定するもの」を
それぞれ指定しています。
メンバの順番が変わる場合は、コマンドシグネチャ作成時の指定も変わる感じですね。
1つ目の指定の為に、描画用ルートシグネチャも指定して作成します。
描画処理の流れ
ここまで来たら次は実際の描画処理です。
今回のインダイレクト描画で必要なのはあくまで描画インスタンス数ですので、
コンピュートシェーダで更新するのも、
引数バッファ内の描画数を格納するメンバ変数のみです。
初期化
まずは毎フレーム描画数をゼロにリセットします。
カメラが動かず、各インスタンスも移動しないので、
この例では毎フレームリセットしなくても良い気がしますが…
コンピュートパス
ここからコンピュートパスが始まります。
コンピュートシェーダから書き込む為、UAV でディスクリプタ設定を行います。
コンピュートシェーダ側にも、引数バッファに対応する構造体の定義が必要です。
その後カリング計算を経て、インスタンス数をカウントアップします。
前述の通り描画数以外は何も変更していません。
最後に Dispatch を行い、コンピュート用キューでコマンドを実行します。
これでコンピュートパスが終わりますが、前回はここで CPU を止めていました。
(上記リンクは前回のサンプルです)
ただ今回は CPU を待機させません。
その代わり描画キューで GPU を待機させます。
コンピュートキューと描画キューは別物で、独立して動作しています。
CPU を止めていないという事は、コンピュート処理の完了を待たずに、
描画キューにコマンドが積まれて実行される事になります。
ExecuteIndirect は描画キューのコマンドの一つであり、
この ExecuteIndirect においてコンピュート処理の結果を利用しています。
CPU は止めたくありませんが、コンピュートキューのコマンドの完了は待ちたいわけです。
その為、コンピュートキューの完了に合わせてフェンス値を更新し、
描画キューはそのフェンス値に到達するまで待機するようにします。
こうする事で CPU 処理は待機せず、描画キューの GPU 処理のみ待機させる事が出来るようになります。
描画パス
次に描画パスです。
主な変更点は
・引数バッファのリソースバリアの追加
・DrawIndexedInstanced から ExecuteIndirect に変更
・前述した drawInstanceIndexes の SRV ディスクリプタ設定がなくなっている
くらいかなと思います。
最後に
さて結果ですが…前回と比べて負荷面ではあまり変化がありませんでした…w
まぁ単純な事しかしてませんしね…
ただ、全く差が無いわけではなさそうですので、
追々ちゃんとプロファイラで確認してみるのがよさそうです。
ここまでやって漸くインダイレクト描画といえるところまで来たかなと思います。
いや~長かったですね。
とはいえこれも基本を押さえた程度でしかないんですけどね…
当然ですが製品で利用する場合はもっと工夫しなければなりません。
というより、複数の異なるメッシュを対象にしたマルチインダイレクトや、
GPU 駆動をより根本的に実現する MeshShader など、
昨今のトレンド技術に進む為の事前知識をやっと知れた程度なのかなと思います。
まだまだ先は長いですね…!