閲覧日:2021/03/17
拙訳
Introduction
DX12 APIは過去のいずれのDirectX APIよりも多くの責任をプログラマーに負わせる。これはリソースステートバリアに始まり、コマンドキューを同期するためのフェンス使用へと続く。その上、非合法なAPI使用がDXランタイムやドライバーに捕捉されたり補正されたりはしない。きちんと把握するには、開発者はデバッグランタイムをがっつり活用したり、報告されるいずれのエラーにも細心の注意を払う必要がある。また、DX12の機能仕様を徹底的に使いこなせるようにしておく必要もある。
Engine Architecture/Structure
やること
- 並列な描画のサブミットのためのタスクグラフ・アーキテクチャを推奨する
- こうすると、リソースやコマンドキューの依存性をしっかりと尊重させつつ、描画サブミットに関して十分な並列性を達成し得るだろう
- コマンドリストの記録やリソースの生成、パイプラインステートオブジェクト(PSO)のコンパイルのための複数の「ワーカースレッド」とともに処理のサブミットのための「マスターレンダースレッド」を検討する
- そのアイデアとは、ワーカースレッドにコマンドリストを作らせ、マスタースレッドがこれらを受け取ってサブミットする、ということである
- IHV minimumごとに別個のレンダーパスを整備してほしい
- アプリは下層にあるハードウェアをもっとも効率的に操る方法に関するドライバーの推論を置き換える必要がある
やらないこと
- ドライバーがドライバースレッドでなにかしらのDirect3D 12処理を並列化することを当てにしない
- DX11では、ドライバーはやれそうならドライバーのワーカースレッドに非同期タスクをdoes farm offするが、これはDX12ではもうやらない
- DX12における処理のサブミットの総コストは削減されているが、アプリケーションのスレッドで計測される処理の量は、ドライバのスレッディングがなくなる関係で、より大きくなるかもしれない。より効率的なそれがCPUの並列ハードウェアコアを用いて並列に処理をサブミットできれば、ドローコールの送信パフォーマンスに関するより大きなメリットを期待することができる
Work Submission - Command Lists & Bundles
やること
- GPU/CPU並列性を達成および制御する責任を負うということを受け入れる
- コマンドリストに処理をサブミットしてもGPUでの処理は始まらない
- ExecuteCommandLists()を呼び出してやっとこさGPUでの処理が始まる
- 複数のコマンドリストへ並列かつスレッド/コア間で均等に処理をサブミットする
- コマンドの記録はCPU負荷の高い操作なのだが、ドライバースレッドはまったく手を貸してくれない
- コマンドリストはどのスレッドでも使えるわけではなく、並列な処理のサブミットとは複数のコマンドリストへのサブミットを意味する
- コマンドリストのセットアップやリセットに関連するコストが存在するということに気をつける
- 効率の良い並列な処理のサブミットには適切な数のコマンドリストが依然として必要である
- フェンスは様々な理由によりコマンドリストの分割を強いる(複数のコマンドキューで、クエリの結果を受け取るとか)
- 15から30の間またはそれ以下で適当なコマンドリスト数を狙ってみる。これらのCLをフレームあたり5~10回のExecuteCommandLists()の呼び出しにまとめてみる
- できればバンドルに記録された断片を再利用する
- 再びCPU時間を消費しなくて良くなる
- sparselyにバンドルのリソースバインディングの継承を使う
- 更にしっかりとこしらえたバンドルを容易にするので、これはバンドルをより少ないオーバーヘッドで再利用できるようにする
- 個別のコンピュートコマンドキューが本当に有益かどうかを注意深く確認する
- グラフィクスタスクと並列に実行することが理論上可能であるコンピュートタスクでさえも、GPUでの並列処理の実際のスケジューリングの詳細は望み通りの結果を生まないかもしれない
- 非同期のコンピュートおよびグラフィクス処理は一緒にスケジュールされ得ることに意識する。正しい処理同士をペアにするためにフェンスを使う
- 非同期のコンピュートおよびグラフィクス処理の並列動作を狙いたい場合、すべてのフレームに対してリングバッファのようなCBV/SRV/UAV/デスクリプタヒープを1つだけ用いる
やらないこと
- 少なくないドローコールを記録するのにバンドルを使わない(ドローコール12個くらいが丁度いい)
- そうでないと、普通はバンドルの再利用性が制限される
- 3Dキューのコンピュート処理を専用の非同期コンピュートキューの処理とオーバーラップさせない
- これは非同期コンピュートキューのバブルを引き起こすかもしれない
- 可能であれば今回の場合にはコンピュート処理をグラフィクス処理に切り替える
- 極めて小さなコマンドリストをサブミットしない
- 小さなコマンドリストは時折、CPU上のOSスケジューラーが新しいやつをサブミットできるようになるより先に完了し得る。これは結果として無駄なアイドル状態のGPUサイクルとなる可能性がある
- OSが以前のExecuteCommandListsの呼び出しのあとにコマンドリストをスケジュールするのに50~80マイクロ秒かかる
- GPUViewを使ってバブルを確認する
- とても少ないコマンドリストだけにシーンの全部または大部分を記録しない
- これはすべてのCPUコアを完璧に活用できなくしてしまう
- また、少数の大きなコマンドリストを構築することは、GPUをアイドル状態にしないでおくことがおそらくは難しくなるであろうことも意味する
- すべてを記録し終えたあとにフレームの終わりだけでサブミットしない
- 他のコマンドリストの記録と並列にGPUを動作させ続ける機会を無駄にし得る
- リストの多くを再利用しようとは考えない
- 物体の可視性などの関係で通常では多数のフレーム毎の変化が存在する
- ポストプロセッシングは例外なのかもしれない
- 多すぎるスレッドや多すぎるコマンドリストを生成しない
- 多すぎるスレッドはCPU資源を過度に使い込んでしまうだろうし、多すぎるコマンドリストはオーバーヘッドをたくさん蓄積してしまうかもしれない
Pipeline State Objects (PSOs)
やること
- PSOをワーカースレッドで非同期に生成する
- PSO生成ではシェーダのコンパイルや関連するストールが起こる
- まずは(すぐにコンパイルできる汎用シェーダを持つ)汎用PSOを多めに用いることから始めて、のちに特殊化したものを生成する
- 最もoptionalなPSO/シェーダを動かしていなくても高速に動作する
- シェーダの特殊化を生成するのはあなたの仕事である — ドライバは裏でconstantな最適化したシェーダのバリエーションを生成しないだろう
- ストールを引き起こす可能性がすこぶる高いので、PSOの実行時コンパイルを避ける
- とはいえ、ドライバが管理するシェーダのディスクキャッシュが助けてくれるかもしれない
- PSO間のステート変更をできる限り最小化する
- PSOはGPUにおいてアトミックなステート変更に必ずしも対応しない
- 可能ならどこでも、気にしないフィールドに同一のsensibleな既定値を用いる
- PSOの再利用の可能性を高めることができる
- 可能であれば
/all_resources_bound/D3DCOMPILE_ALL_RESOURCES_BOUNDのコンパイルフラグを用いる- コンパイラがテクスチャアクセスの最適化でより良い仕事ができるようになる。このフラグをオンにしたとき1%以上のフレームレートの改善を確認したことがある
やらないこと
- 絶対に必要な以上に同一コマンドキュー上でコンピュートとグラフィクスを切り替えない
- これはいまだにめちゃくちゃ手間がかかるものである
- 絶対に必要な以上にテッセレーションのオン/オフを切り替えない
- これもまた、いまだにめちゃくちゃ手間がかかる
- PSOの生成はシェーダがコンパイルされ、ストールが引き起こされる場所であることを忘れない
- 非同期に、そして使われるよりも先にPSOを生成することは本当に重要である
- 急ぎの用のときは優先度を一時的に引き上げることを検討する
Root Signatures
やること
- NVIDIAハードウェアではできればルートシグネチャに直接、定数やCBV(あれば、SRVやUAV)を配置する
- ピクセルステージのエントリから始める
- ルートに直接置いた定数はNVIDIAハードウェアでは大幅にピクセルシェーダを高速化できる — uberシェーダの一部を切り替えるシェーダ定数で特に検討する
- ルートシグネチャに置いたCBVもNVIDIAハードウェアでは大幅にピクセルシェーダを高速化できる
- Carry on with decreasing execution frequency of the shader stages
- ルートシグネチャのCBVを使うと、CBVデスクリプタを格納するためのデスクリプタヒープやエントリのバージョン管理、余分な間接参照が必要なくなる(⇒
CreateConstantBufferView()を呼び出す必要がなくなる) - ルートのビューが境界チェックやその他の制限を持たないことを覚えておく
- ピクセルステージのエントリから始める
- CPUメモリにルートの定数、CBV、SRV、UAVの現在の値をキャッシュし、真の変更を検知したときにだけルートシグネチャの中身を変更する
- 適切な変更管理によって大幅なスピードアップを確認したことがある
- CBV、SRV、UAVのシェーダ可視性を必要なステージのみに制限する
- ドライバ内やGPU上でこれらのビューを参照する必要があるステージごとにオーバーヘッドがある
DENY_*_ACCESSフラグを用いて、リソースのシェーダ可視性を明示的に制限する
- ルートシグネチャの変更の数を最小化する
- ルートシグネチャの変更自体は問題ないが、そのような変更のあとのルートシグネチャのエントリの初期化に付随するコストが通常では存在する
- Tier 1ではCBV、UAV、SRV、サンプラのデスクリプタを、Tier 2ハードウェアではCBVとUAVのデスクリプタを丁寧に扱う
- これらのTierでは、アプリケーションはコマンドリストが実行されるまでにルートシグネチャ(と使用されるデスクリプタテーブル)で定義されるすべてのデスクリプタを埋めなければならない。シェーダがこれらすべてのデスクリプタを参照しないであろう場合でさえも然り
- Tier 3では、使わないデスクリプタをバインドしたままで良い — ステートスラッシングのボトルネックを容易にもたらし得るので、バインドを解除して時間を無駄にしないこと
やらないこと
- 異なる更新頻度を持つCBVデスクリプタテーブルにCBVをグループ化しない
- 理想としては、1つのテーブルにあるすべてのCBVは同じタイミングでの更新を必要とするであろう
- 再利用できるようにルートシグネチャやデスクリプタテーブルを肥大化しない
- マテリアルのまとまりごとに最小限のエントリのセットを用いるよう目指してみる
- ルートテーブルのエントリで同じシェーダステージに対してvisibleとdenyのフラグを同時にセットしない
- 現在のドライバでは、denyフラグはD3D12_SHADER_VISIBILITY_ALLがセットされたときにのみ機能する
- SRVとUAVは、それらを使う可能性がある大量のドロー/ディスパッチがある場合を除いて、ルートシグネチャに直接配置しない
- ルートシグネチャの変更後にリソースバインディングを未定義のままにしない
- ルートシグネチャにおける変更は前のルートシグネチャで使われたすべてのリソースバインディングを取り除く/クリアする
Allocators and Lists
やること
- 似たような大きさのドローコール列でアロケータを再利用する
- リストを予め使えるようにしておいたとき、アロケータは高速になる
- 最低限、
2*T+N個のアロケータを使う2*- 最後のフレームによるリスト/アロケータのセットはGPUによって消費されている最中であり、もうひとつは現在のフレームで構築/使用されるT= コマンドリストを生成するスレッドの数 — アロケータがスレッドに関係なく使えるはないことに是非とも気をつけたいN= バンドル用の追加プール
- 別のフレームで再利用する前にAllocator::Resetを呼び出す
- そうしないと、アロケータはメモリを使い切るまで伸長し続けるだろう
やらないこと
- アロケータとリストがGPUメモリを消費することを忘れない
- 大きすぎるアロケータは他の望ましくない方法でGPUのワーキングセットを制限するかもしれない
- アロケータを生成/破棄しない、その代わりに再利用する
- アロケータの生成/破棄のオーバーヘッドを節約する
- 異なるサイズのどローコール列で再利用しない
- これはワーストケースの大きさのアロケータを引き起こす
- 一連のコマンドリストをリセットするときに対応するアロケータをリセットするのを忘れない
- アロケータをリセットしないってことはメモリがリークするってことだからね!
- アクティブなコマンドリストでまだ使用中のアロケータを解放/再利用しない
- これはイリーガルであり、コマンドリストがまだ使っているメモリを解放もしくは上書きするかもしれない
Resources
やること
- ビデオメモリの使いすぎを避ける
IDXGIAdapter3::QueryVideoMemoryInfo()を用いて利用可能なビデオメモリについての正確な情報を得る- フォアグラウンドのアプリケーションは全部を、または、ビデオメモリの大きな%を、必ずしも割り当てなくて良い
- OSによるバジェットの変更に応える
IDXGIAdapter3::RegisterVideoMemoryBudgetChangeNotificationEventの使用を検討する- 利用可能なメモリに基づいたグラフィック設定の上限を検討する
- システムメモリにオーバーフローヒープを生成し、ビデオメモリヒープからリソースを移す
- DX12はここでDX11ドライバ以上のメモリ管理のアドバンテージをアプリにもたらす
- それぞれが参照するメモリ量がビデオメモリに合うようにコマンドリストを分割する
- CLごとに何に使われるかを追跡し続ける
- ビデオメモリのバジェットを上回ろうとしているとき、コマンドリストの実行前/後にMakeResident/Evictの使用を検討する
- ドライバにさらなる知識を与えるためにできればどこでもcomittedリソースを用いる
- これはドライバがGPUメモリをもっと上手に管理できるようにする
- placedリソースの良いユースケースは、例えば、ストリーミング中に使い、それらの生存期間中にリードオンリーのテクスチャの異なるセットを保持するリソースヒープである
- MakeResidentの呼び出しをバッチする(ページテーブルの更新に対するCPUおよびGPUのコストを予期する)
- これはドライバやGPUの内部のオーバーヘッドを下げる
- MakeResident/MakeUnresidentを用いて与えられたメモリバジェットで作業を行う
- 必要とあらばtiledリソースのmipレベルを削る
- MakeResidentが失敗するケースを扱う必要がある
- 特定のリソースタイプがヒープ内で異なるアライメント規則を持つという事実に気をつける
- デバイスの機能レベルの中で様々なリソースバインディングTiersに対処する方法を編み出す
- ステージ全体でのUAVの個数は8や64に制限されるかもしれない
- CBVの個数はステージあたり14に制限されるかもしれない
- サンプラの個数はステージあたり16に制限されるかもしれない
- ヒープでのエイリアシング規則に注意する
- 良いroll-upのためにtiledリソースの仕様を確認しよう
- リソース、SRV、DSV、等々に対して異なるヒープタイプが存在するという事実に注意する
- いくつかのヒープtiersでは、他のよりも多くの制約があるかもしれない
- リソースヒープtierの能力をチェックしよう
- 深度ステンシルテクスチャをコピーするときにCopyTextureRegion()を使うときは、注意してD3D12_TEXTURE_COPY_LOCATIONで埋める
- リソースの深度の部分だけのコピーは遅い方法で行われるかもしれない
やらないこと
- 深度ステンシルやレンダターゲットのリソースに対してplacedリソースを再利用しすぎない
- レンダリングされ得る前にこれらのリソースをクリアする必要があるのに加えて、これらの切り替えを高価にする他のハードウェア依存の記録を取る操作があるかもしれない
- tiledリソースの可用性を当てにしない(能力のビットをチェックする)
- 依然として様々なDX12ハードウェアのクラスについて考える必要がある
- すべてのGPUメモリを一挙に割り当てられるということを当てにしない
- 下層にあるGPUアーキテクチャに依存して、メモリは分割されていたりされていなかったりするかもしれない
- MakeUnresidentの呼び出しに対する即時コストを期待しない
- コストは他のMakeResidentの呼び出しが使われるまでに遅延されるかもしれない
- GPUViewの解析を使って遅延したページング要求を探し出す
- コストは他のMakeResidentの呼び出しが使われるまでに遅延されるかもしれない
- 回避できる場合にはリソースの破棄や生成を行わない
- できればどこでもMakeUnresidentやMakeResidentを用いる方が良い
- リソースの生成や破棄のオーバーヘッドを節約する
Barriers, Fences & Hazards
やること
- バリアやフェンスの利用を最小化する
- DX11からDX12への移植での主要なパフォーマンス問題として、冗長なバリアや関連するアイドル操作に対する待機を確認している
- DX11ドライバはバリアを削減する素晴らしい仕事を行っている — 現在、DX12ではあなたがこれを行う必要がある
- いずれのバリアやフェンスも並列性を制限し得る
- DX11からDX12への移植での主要なパフォーマンス問題として、冗長なバリアや関連するアイドル操作に対する待機を確認している
- リソースの使い方フラグの最小セットを常に用いる
- このフラグの組み合わせにセットされるすべてのフラグひとつひとつが本当に必要な場合を除いて、D3D12_RESOURCE_USAGE_GENERIC_READの使用を避ける
- 冗長なフラグは冗長なフラッシュおよびストールや不必要にゲームの速度低下を引き起こすかもしれない
- 繰り返しになるが、DX11からDX12への移植での主要なパフォーマンス問題として、冗長であったり過度に保守的であったりするバリアフラグや関連するアイドル操作の待機を確認している
- ID3D12CommandList::ResourceBarrierにて最小のターゲットのセットを指定する
- 誤った依存性を追加すると、冗長性を追加することになる
- ID3D12CommandList::ResourceBarrierへの呼び出し1つにバリアをまとめる
- こうすると、すべてのバリアを順次通過するのではなく、ワーストケースが選ばれ得る
- できればどこでも分割バリアを用いる
_BEGIN_ONLY/_END_ONLYフラグを使う- これはドライバがより効率的な仕事を行う助けになる
- ExecuteCommandListsの呼び出しを横断してイベントをシグナルしたり先に進めたりするのにフェンスを用いる
やらないこと
- 冗長なバリアを挿入しない
- これは並列性を制限する
- 間にドローコールのないD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCEからD3D12_RESOURCE_STATE_RENDER_TARGETへの遷移やその逆は冗長である
- read-to-readのバリアを避ける
- リソースをあとに続くすべての読み出しに対して正しいステートにする
- ちゃんとした理由もなくD3D12_RESOURCE_USAGE_GENERIC_READを使用しない
- write-to-readステートの遷移では、遷移対象が書き込みへの次の遷移のまえに必要となるすべての必須読み出しステートを含んでいることを保証する。これは読み出しステートフラグを組み合わせることでAPIによって行われる — そして、後続のResourceBarrierの呼び出しにおけるread-to-readの遷移よりも推奨される
- バリア1つのみにID3D12CommandList::ResourceBarrierを逐次呼び出さない
- これはドライバが一連のバリアのワーストケースを選択できなくする
- ExecuteCommandListsの呼び出しごとに1回より細かい粒度でシグナルしたり先に勧めたりを引き起こすことを期待しない
Multi GPU
やること
やらないこと
Swap Chains
やること
-
FLIPモードのスワップチェインを使う
-
真のimmediate independent flip modeに切り替えるため、(ボーダーレス)フルスクリーンウィンドウとFLIPモードスワップチェインと合わせてSetFullScreenState(TRUE)を使う
- これは現時点で、Microsoftによると、Present(0, 0)を呼び出すときにD3D12でティアリング付きの解き放たれたフレームレートを得ることができる唯一のモードである
- 他のいずれのモードもティアリング付きの無制限のフレームレートは不可
-
わかった上でDXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCHフラグを使う
- このフラグはウィンドウサイズが現在のスクリーン解像度と合っている場合に無制限のフレームレートを達成する必要がない(下記を参照)
- このフラグがセットされる場合、SetFullScreenState(TRUE)の呼び出しの前にResizeTarget()を用いた解像度の変更はうまく機能し、上限なしのFPSを達成するだろう。このフラグがセットされない場合、SetFullScreenState(TRUE)の呼び出しの前にResizeTarget()を用いた解像度の変更はディスプレイ解像度を変化させない。ターゲットは現在の解像度に引き伸ばされ、FPSは制限されるだろう
-
フルスクリーン状態(真のimmediate independent flip mode)でなければ、望みのFPSやレイテンシのために注意してスワップチェイン中のレイテンシやバッファ数を制御する
- IDXGISwapChain2::SetMaximumFrameLatency(MaxLatency)を用いて、望みのレイテンシをセットする
- これを機能させるには、DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECTフラグをセットしてスワップチェインを生成する必要がある
- 0のsync intervalは「今表示しようとしているバッファはコンポジションが起こる次の時点で利用可能な最も新しいバッファである」ということを示す。しかし、表示はコンポジションが発生するまで先に進まない。これが起こるのは、現在ではVSyncのみである。
- DXGIはMaxLatency-1回を表示したあとでPresent()をブロックし始めるだろう
- デフォルトのレイテンシである3では、これはFPSがリフレッシュレートの2倍以上になり得ないことを意味する。つまり、60Hzのモニタでは、FPSは120FPSを上回ることができない
- (コマンドアロケータや動的データや関連するフレームフェンスという意味での)フレームをキューに入れるつもりより1か2くらい多いスワップチェインのバッファを使ってみる、そして、このスワップチェインのバッファ数に”最大フレームレイテンシ”をセットする
- IDXGISwapChain2::SetMaximumFrameLatency(MaxLatency)を用いて、望みのレイテンシをセットする
-
フルスクリーン状態(真のimmediate independent flip mode)でなければ、より高いFPSを生成するためにWaitForSingleObjectEx()とともにwaitable objectスワップチェインを用いることを検討する
- これは一瞬もまったく見えないフレームをもたらすだろうが、ベンチマークにとっては良い解答となるかもしれないことに注意すること
- waitable objectスワップチェインとGetFrameLatencyWaitableObject()を用いると、レンダリングしたり表示したりする前にバッファが利用可能かどうかをテストすることができる — 以下の選択肢が取れる:
- 追加のオフスクリーンサーフェスを使う
- オフスクリーンサーフェスにレンダリングする。バッファが利用可能かどうかを確認するために0秒タイムアウトでwaitable objectをテストする。バッファが使えれば、スワップチェインのバックバッファにコピーしてPresent()する。使えるバッファがなければ、はじめからもう一度フレームを開始する
- フレーム開始時、waitable objectをテストする。成功すれば、使えるスワップチェインのバッファにレンダリングする。失敗すれば、オフスクリーンサーフェスにレンダリングする。
- 3か4つのバッファのスワップチェインを使う
- バックバッファに直接レンダリングする。Present()を呼び出す前に、waitable objectをテストする。成功すれば、Present()を呼び出す。そうでなければ、はじめからスタートする
やらないこと
- DXGIがPresent()でブロックし始めるまでにキューに入るフレームがスワップチェインあたり3つに制限されることを忘れない
- スワップチェインの生成でDXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECTフラグをセットし、この既定値を変更するためにIDXGISwapChain2::SetMaximumFrameLatencyを用いる
- SetFullScreenState(TRUE)を用いる真のimmediate indepent flip modeに切り替えた後にResizeBuffers()を呼び出すことを忘れない