DirectXの話 第176回

Acceleration StructureのCompaction

21/06/30 up

BLASのサイズを小さくしよう

BLASは頂点座標とインデックスから生成される空間分割情報ですが、使用した頂点バッファやインデックスバッファはAS構築後には不要になります。
つまりAS内部には頂点座標が保存されているわけで、最低でも頂点座標情報+空間分割情報が保存されているわけです。

これがどの程度のサイズになるかというと、1つ1つを見るならさほど大きくない、というくらいではないかと思います。
もちろん、メッシュの頂点数やポリゴン数に依存することにはなるので、大きなBLASが構築されることもあるでしょう。

それに比べるとTLASはバウンディングボックスの空間分割情報です。
BLASのポリゴン数と比べるとTLASのインスタンス数は少ないでしょうし、実際、TLASのサイズが問題になることは稀です。

つまりASのサイズが問題に浮上するとするなら、その問題はBLASに集約されると言っても過言ではないでしょう。

しかも昨今のゲームはテクスチャもメッシュも高精細化してきているため、VRAMが増えてもリソースの増加に対応できているとは言い難いです。
その上でレイトレーシング用のデータが増えるとなるとメモリ管理者は頭を抱えることになるでしょう。

そこで、BLASサイズを小さくするための手段の1つとして、今回はASのCompactionについて解説します。

今回のサンプルはすでにコミット済みです。

D3D12Samples

Sample024のBLAS構築をCompactionバージョンに変更しています。

Compactionの流れ

Compactionはフラグを追加するだけでは対応できません。
ここではCompactionを行うための処理の流れを見ていきます。

  1. ビルド時のフラグとしてCompactionフラグを追加する
    D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_ALLOW_COMPACTION

  2. ASをビルドする

  3. ASビルドの完了待ち(バリア、もしくはフェンス)

  4. ビルド後情報をバッファに取得する
    取得するのはCompactionサイズ

  5. サイズを取得したバッファをリードバックバッファにコピー

  6. コピー完了を待ってリードバックバッファからサイズを取得する

  7. 新しいASバッファを指定サイズで確保する

  8. 元のASバッファから新しいASバッファにCompactionコピーを行う

手順を見ると結構面倒だなと思われるかもしれませんが、実際面倒です。

特に問題なのがリードバックバッファからサイズを取得する部分です。
この部分とそのサイズから新しいASバッファを取得するまでの部分は当然CPUの処理です。
ということは、一旦GPUの処理が完了するのを待ってからリードバックバッファからサイズを取得しなければならないということになります。

ここからもわかることですが、Compactionを終えるには数フレームの処理が必要になるわけです。

ただし朗報もあります。
Compactionフラグを使ってビルドしたASはそのままASとして使用することができます。
つまり、Compactionが完了するまでは元のASを使ってレイトレーシングが可能ということです。

欠点はもちろん複数フレーム必要であるという点と、一時的にASのバッファが2重になるという点です。
もしも1フレームでシーン全体のCompactionを行おうとすると、一時的に倍近いVRAMを確保することになります。

それらやCompactionコピーのパフォーマンスを考慮すると、1フレームに大量のCompactionを行うのは避けたほうが良いでしょう。

ソースコード解説

それではソースコードを見ていきます。

ASビルド

最初はCompactionフラグを指定してASビルドを行います。
その後にビルド後情報を取得するわけですが、そこまでの処理の流れを見ていきます。

このコードは BottomAccelerationStructure::Build() 関数内のコードです。
エラー対応などを少し減らしていますが、基本的な流れは同じです。

まず、ビルドフラグに D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_ALLOW_COMPACTION があるかどうかを確認します。
フラグが設定されていたらCompaction用のコードが走るようになっています。

Compaction用のコードの最初はバッファ生成です。
バッファは2つ用意し、Compactionサイズである D3D12_RAYTRACING_ACCELERATION_STRUCTURE_POSTBUILD_INFO_COMPACTED_SIZE_DESC を取得するためのバッファと、それをリードバックするためのバッファです。

ビルド後情報を取得するにはリソースステートをUAVにする必要があります。
内部ではCompute Shaderが走っているのではないかと考えられますね。

ビルド後情報の取得方法は2つあります。

1つは今回のように BuildRaytracingAccelerationStructure() 関数の第2,3引数を使って D3D12_RAYTRACING_ACCELERATION_STRUCTURE_POSTBUILD_INFO_DESC を渡す方法です。
大体の場合はこちらのほうが簡単なのでオススメです。

もう1つは EmitRaytracingAccelerationStructurePostbuildInfo() 関数を使用する方法です。
この方法はビルド完了済みのASから情報を取得する場合に使用します。
この手法ではASのビルドが完了していなければなりません。つまり、関数を発行する前にバリアかフェンスが必要になるということです。

ビルド後情報はCompaction情報だけではなく、シリアライズした場合のサイズなんかも取得できます。
一応、ASはシリアライズ/デシリアライズが可能なので、シリアライズしてディスクに保存したい、というような用途では Emit~ 関数を使うほうがいいかもしれません。
なお、シリアライズしたASが有効なのは同じGPU、同じドライバの場合に保証されているだけらしいので、あらかじめビルドしておくということはできません。

さて、ビルド後にはバリアを張ってAS構築が完了するのを待ちます。
BottomAccelerationStructure::Build() 関数はバリアを張るか張らないかは外から指定できるようになっていますが、Compactionの場合は強制的にバリアを張るようにしています。
理由としてはビルド後情報を取得したバッファからリードバックバッファへのコピーを行っているからです。
当然ですが、リードバックバッファへコピーする際にはビルド後情報バッファのステートを変更する必要がありますので、ビルド完了を待つUAVバリアの後にバッファステートのTransitionバリアを張っています。

さて、ここで問題です。
ASビルドを非同期コンピュートで行いたいのでこの命令を非同期コンピュート用のコマンドリストに積みました。
どうなるでしょう?

答え:
Transitionバリアが非同期コンピュートで実行できないのでエラーになる。

つまりCompactionを用いる場合は非同期コンピュートが使用できないという関数仕様となってしまっています。
この問題については現状はサンプル作成ということで勘弁していただいて、のちにASを管理するクラスを作成する予定なのでそちらで対応したいと思います。

このように、AS構築からCompactionを行うまでにはGPU側も非同期コンピュートだけでは対応できないという問題があるわけです。

ASのCompactionコピー

ここまでのコマンドを発行したらGPUの処理を待ちます。
待たなければASのビルドも終わりませんし、Compationサイズもコピーされないので取得できません。

GPU処理が完了すればリードバックバッファからCompactionサイズを取得できるので取得します。

このサイズの新しいASバッファを生成したらCompactionコピーを行うだけです。

CopyRaytracingAccelerationStructure() 関数はASのコピーを行う命令ですが、第3引数の指定によってコピーの仕方が変わります。
今回は D3D12_RAYTRACING_ACCELERATION_STRUCTURE_COPY_MODE_COMPACT を指定しているのでCompactionコピーとなります。
シリアライズ/デシリアライズを行う場合もこの命令を用います。

また、ASを単純にコピーしたい場合はCloneコピーをすることになります。
単純なバッファコピーではダメなのか?という点ですが…試したことがないのでなんとも。
ただ、内部でアドレス的なものを管理している可能性があるので、バッファコピーでは正常動作しない可能性があります。
これはハードやドライバによっても変わると思いますので、ある特定のハードでバッファコピーでも動作したからと言って利用するのは危ないと思います。

Compactionコピー命令の発行後はやはりUAVバリアを張ります。
これを行わないとコピー完了を待たずに次の処理を実行してしまう可能性があります。

Compactionの処理はこれで終わりです。
処理の流れは面倒そうに見えるのですが、コードを見るだけだとさほど面倒ではない、というか割と簡単に見えますね。
実際、コード量としてはさほどではないのですが、一番の問題はやはりGPU完了待ちが必要という点です。
今回はフェンスの利用をしていませんが、より正確な処理を実行するならフェンスを使って確実にGPUの処理完了を待つようにしましょう。

Compactionの結果

で、Compactionの結果ですが、今回のCrytek Sponzaでは以下のような結果となりました。

驚きの50%OFF!

とはいえ、これは大きなサイズのASだからであって、小さなASだとここまで縮小しないとは思います。
ですが、サイズが大きなものほど効果が高いのであれば非常に期待が持てます。

しかしながら、ASがどのようなデータ構造になっているかはハードウェアとドライバによるところが大きいです。
今回は Geforce RTX 2080 での結果ですが、Radeonでは別の結果になるかもしれません。
今後は Intel のディスクリートGPUも対応予定とのことなので、各ハードでどの程度のサイズになるのか確認してみたいですね。