[Rodrigues 2017Rodrigues, T. 2017. Moving to DirectX 12: Lessons Learned. Game Developers Conference. https://www.gdcvault.com/play/1024656/Advanced-Graphics-Tech-Moving-to.]
url
Talk goals#
- 多くのトークがより直接的なAPIの使い方のアドバイスにすでに重点を置いている。[Thomas and Dunn 2016Thomas, G. and Dunn, A. 2016. Practical DirectX 12 - Programming Model and Hardware Capabilities. Game Developers Conference. https://gpuopen.com/download/Practical_DX12_Programming_Model_and_Hardware_Capabilities.pdf.; Hodes et al. 2016Hodes, S., Baker, D. and Oldcorn, D. 2016. Right on Queue. Game Developers Conference. https://gpuopen.com/download/GDC_2016_D3D12_Right_On_Queue_final.pdf.; Chajdas 2016Chajdas, M. G. 2016. D3D12 and Vulkan: Lessons learned. Game Developers Conference. https://gpuopen.com/download/d3d12_vulkan_lessons_learned.pdf.; Sorbo 2015Sorbo, B. 2015. DirectX 12: Improving Performance in Your Game. Game Developers Conference. https://learn.microsoft.com/en-us/shows/gdc-2015/better-power-better-performance-your-game-on-directx12.; McMullen 2015McMullen, M. 2015. DirectX 12: Advanced Graphics and Performance. Game Developers Conference. https://learn.microsoft.com/en-us/shows/gdc-2015/advanced-directx12-graphics-performance.; Hart and Oldcorn 2015Hart, E. and Oldcorn, D. 2015. Getting The Best Out Of D3D12. Game Developers Conference. https://www.slideshare.net/slideshow/getting-thebestoutofd3-d12/46162216.; Oldcorn and Hodes 2015Oldcorn, D. and Hodes, S. 2015. D3D12 A New Meaning for Efficiency and Performance. Game Developers Conference. https://www.slideshare.net/slideshow/d3-d12-anewmeaningforefficiencyandperformance/46169256.; NVIDIA Developer 20162016. DX12 Do's and Don'ts. NVIDIA Developer. https://developer.nvidia.com/dx12-dos-and-donts.; Ruiz 2016Ruiz, M. L. 2016. Understanding all_resources_bound in HLSL. Marcelo's WebLog. https://learn.microsoft.com/en-us/archive/blogs/marcelolr/understanding-all_resources_bound-in-hlsl.; Haar and Aaltonen 2015Haar, U. and Aaltonen, S. 2015. GPU-Driven Rendering Pipelines. Advances in Real-Time Rendering in Games course. ACM SIGGRAPH. https://advances.realtimerendering.com/s2015/index.html.]
- このアドバイスのいくつかを実装するためのより高レベルな戦略に焦点を当てる。
Background : Anvil Next#
- Anvil(内製エンジン)は直近の10年で8つのAssassin’s Creedをリリースした。Anvil NextはAssassin’s Creed Unityのために開発されたメジャーアップグレードであった。
- ACUでは、我々が”Anvil Next”と呼ぶ、重大なアップグレードを行った。このバージョンでリリースされた一番最近のACでその例が確認できる。
- CPU処理の削減とGPUパフォーマンスの改善のため、描画バッチ処理と’GPUサブミッション’を行う。[Haar and Aaltonen 2015Haar, U. and Aaltonen, S. 2015. GPU-Driven Rendering Pipelines. Advances in Real-Time Rendering in Games course. ACM SIGGRAPH. https://advances.realtimerendering.com/s2015/index.html.]
- GFXコンピュートを積極的に利用する。(非同期コンピュートや
MultiDrawIndirectも)
- 我々はACUに対してドローコールのバッチ処理と(描画/インスタンス/トライアングルクラスタ/トライアングルのカリングのためにGPUを利用する)‘GPUサブミッション’に関する多くの仕事をこなした。この話題の詳細はそれを題材にしたGDC2015のUlrichのプレゼンテーション[Haar and Aaltonen 2015Haar, U. and Aaltonen, S. 2015. GPU-Driven Rendering Pipelines. Advances in Real-Time Rendering in Games course. ACM SIGGRAPH. https://advances.realtimerendering.com/s2015/index.html.]から得られる。
- 我々は、(GPUカリングのために非同期コンピュートや
MultiDrawIndirectといった)ある種の機能がPCで利用できなかったために、レンダリングパイプラインにおいてコンソール固有の最適化も行った。DX12ではこれらの機能がPCでも利用可能になる。
Background : moving to DX12#
- パフォーマンスのボトルネックを測りつつAPIへの理解を深めるため、‘ナイーブなポート’から始めた。
- 結果は、予想通り、ひどいパフォーマンスだった。特にGPUで(DX11の倍くらい)
- DX12APIの詳細は昔のレンダラインターフェイスに隠蔽する形で、とても基本的なポートから始めた。
- 予想通り、極めてひどいパフォーマンスであった(GPU時間はDX11の最大200%となった)。主にハードウェア抽象化レイヤにおいてリソース状態の視野が非常に低レベルで狭いため、一般的なAPIの使い方のアドバイスを実装することが単純に難しすぎる。
- 主なGPUパフォーマンスの問題:
- バリアの乱用。
- メモリの過剰要求。
- 主なCPUパフォーマンスの問題:
- PSOコンパイルによるレンダリングスレッドのヒッチング。
- デスクリプタのコピー量。
- バリアの乱用:
- コミット間のバッチ処理時でさえ、大量の個別のバリア呼び出しを行っていた。
- (大量の不必要な中間の遷移を引き起こす)初期状態の強制化をあらゆるところにばら撒くことをせずに、マルチスレッド化されたコマンドリストレコーディングでバリアを管理するのは極めて難しい。
- メモリ関係:
- (エイリアシングやタイルリソース/MIPのストリーミングが足りていないため、)大幅にオーバーコミットしていた。
- コマンドリストの管理と再利用による問題もあった。
- PSOとデスクリプタの管理:
- PSOのコンパイルにより、レンダリングスレッドでヒッチングを起こす。
- すべてのユースケースを扱うために共通のroot signatureを使っていたため、デスクリプタの大量コピーが起こっていた。
- 以下に基づいてレンダラの再設計を計画し始めた。
- 初期ポートからの知見。
- Ubisoftの他のチーム。
- DX12に関する様々なトークからのアドバイス。
API guidance recap:#
- リソースバリアの最小化とバッチ処理は非常に重要である。
- さもないと、処理のシリアライズや不必要なキャッシュのフラッシュによってGPUパフォーマンスを損なうことになる。
- DX11ドライバは何年もかけて水面下でこれを最適化してきた。DX12はここで多少の手助けを依然として提供している: 単一の呼び出しでバリアをバッチする場合、その呼び出しにおけるすべての遷移に対する最小のバリアアクションの集合を生み出そうとする。
- DX12は、良好なCPUパフォーマンスを得るために不可欠である、完全に機能する低オーバーヘッドな並列コマンドリストレコーディングも提供する。
- 小さなコマンドリストが大量にあったり、
ExecuteCommandListsを呼び出しすぎたりすると、CPUパフォーマンスを損なう恐れがある、といった気に留めておくべき注意事項が存在する。
- コピーエンジンや非同期コンピュートキューへのアクセスも可能である。DX11ドライバとのすり合わせをしたいなら、おそらくはそれらを使わなければならないだろう。
- また、コンソール開発者として機能セットに関して言えば、PCの機能はかなり近くなっている。これはエンジン設計の視点から見れば素晴らしいことである。
API guidance recap(2):#
- 実行時処理を最小化するために効率的に事前コンパイルしたレンダステートを使うこともCPUパフォーマンスに対して極めて重要である。
- APIは事前コンパイルされたステートブロブに重点を置いているため、高価な処理を一度だけ行い、それをキャッシュして、バインド時にオーバーヘッドを最小にすることが可能になる。
- メモリ管理は大きなトピックであり、私が導入するシステムに関係するいくつかの面のみを述べる。
- APIはユーザがメモリの割り当てや管理を制御できるようになっている。(このため、例えば、メモリのエイリアシングが実装できる。)
Systems#
- Producer System
- Shader Input Groups
- Pipeline State管理
Producer System : Motivation#
- レンダリングパイプラインはますます複雑化している。
- リソースのメモリとステートの管理は今やAPIユーザの責任である。
- ユーザに広範囲の低レベル知識を持たせずに新たに開示されたGPU機能を開拓する。
- ACUの開発の終了時、レンダリングパイプラインの複雑さが増し続けたため、レンダリングパスアーキテクチャの限界を強く感じ始めた。
- 同時に、近い将来やって来る新しいグラフィクスAPIは、最大限の利点を得るためにアーキテクチャやクロスプラットフォームのレベルで対処されるべき制御を、すべてのプラットフォームにおいて違う次元で本当に可能にするだろうと感じた。
- この新しいレベルの制御はリソースのメモリやステートの管理が今やユーザの考慮事項であることを意味する。我々は、エンジンユーザに大きな負担を負わせることを避けると同時に、APIを効率的に操ることを望んでいる。
Complex Rendering Pipelines#
- プロデューサーはGPUリソースのライター/リーダーである。
- リソースの依存性はGPUでの実行順を決定する。
- レンダリングパイプラインのタイアグラムの例。
- プロデューサー(ダイアグラム中の四角)が確認できる。これらは本質的にはGPUリソースのライターとリーダーである。
- そして、GPUの実行順を定義するこれらの間の依存性を持つ。
- いくつかのGPUキューも持つ。これは、リソースの寿命管理や同期などの観点から一層複雑にする。
- このグラフの静的な構成を見ただけでもすでにかなり複雑だが、さまざまな有効化した機能セットをサポートするために別の組み合わせのレイヤーを追加しなければならない(例えば、ローエンドマシンでのパフォーマンススケーリング用)。
- パイプラインをいくつかのユースケースに対して再構成することは、手動/明示的なスケジューリングを幾分実用的でなくしてしまう多くの連鎖反応がある。
- 十分な高レベル情報をキャプチャして、そこから低レベルAPIを効率的に操ることを可能にするが、パフォーマンスを犠牲にしない程度の自動化を必要とした。
Producer System : Design Goals#
- 以下を操るためにリソースの依存性を使う。
- リソースメモリ寿命。
- キュー間同期。
- リソースステート遷移。
- コマンドリストの実行順やバッチ処理。
- このシステムの重要な設計の側面のひとつはリソース依存性を明示的にすることであった。
Resource Dependency Tracking#
- 明示的なリソース依存性が各プロデューサーごとに指定される。
- この例では、一連のプロデューサーと、GPUで実行するときのリソースとそれらの関係を指定する方法を確認できる。
- 明示的な依存性はGPUリソースの寿命を自動的に設定することを可能にする。
- これらの依存性に基づいて、各GPUリソースの寿命を自動的に導くことができる。
Resource Memory Aliasing#
- 導かれたリソース寿命を使って、フレームの至るところでメモリを再利用する。
- Placed Resourcesはユーザによるメモリ割り当て制御を可能にする。
- フレームのGPUリソースのメモリフットプリントを削減する。
- Placed Resourcesと導かれたリソース寿命を使うと、フレームの至るところでメモリを再利用することができ、GPUメモリのフットプリントを削減できる。
- メモリエイリアシングはリソース寿命と次のキュー間同期ポイントに基づいて自動的に導かれる。ダイアグラムでは、使用期間がGPU実行の寿命においてオーバーラップしないために同じメモリを共有する、2つのGPUリソースの例が確認できる。
Resource Access Synchronization#
- 明示的なリソース依存性。
- 明示的な同期をサポートする。
- フェンスのリソース依存性を介して指定する。
- ユーザーが実行窓を定義できるようにする。
- プロデューサーシステムを自動化することのひとつの側面は、リソースアクセスの同期である。
- すべてのプロデューサーは、グラフィクス/非同期コンピュート/コピーのうち、そのコマンドリストを実行したいGPUキューを指定する。(デバッグやその他の構成の目的のために実行時に変えることができる。)
- 各プロデューサーごとに明示的に指定されたリソース依存性に基づいて、必要なキュー間同期(フェンス)を導出し、GPUでのコマンドリストの正しい実行順を保証することができる。
- フェンスリソースを介した明示的な同期を依然としてサポートしており、ユーザーは実行窓による制御権を持つ。(例えば、異なるキューで実行しているGPUワークロードをより良く一致させるために。)
- コンピュートで生み出され、GFXキューで消費されるSSAOバッファ。
- 単純な必要なキュー間同期の例。
- コンピュートキューで動作するSSAOプロデューサー(赤)はGバッファの深度に依存し、Gバッファの深度はグラフィクスキューでGバッファプロデューサー(青)により生成されることが確認できる。
- コンピュートキューにあるSSAOはGバッファのレンダリングが終了するのを待たなければならない。
- 初めに必要な同期は、Gバッファパスがシーンの深度を書き終わる前に開始できないSSAOプロデューサーの前である。
- グラフィクスキューにあるディファードライティングはSSAOが終了するのを待たなければならない。
- 逆に、コンピュートキューにあるSSAOプロデューサーとSSAOマスクの初めてのコンシューマーとの間にもフェンスが必要である。今回の場合、グラフィクスキューにあるディファードライティングプロデューサー(ダイアグラム中の緑)がそれである。
- この例は極めて単純であり、現実のフレームスケジュールでは、例えば、Gバッファの後にシーン深度へ書き込むような、もっと多くのプロデューサーがあるかもしれない。もしこれらのプロデューサーのひとつが移動し、同期がユーザーによって手動で行われた場合、捕捉とデバッグを時折し辛いタイミング依存のグリッチが発生する可能性がある。
- ユーザーは(フェンスリソースを介して)ワークロードをより良く一致させるために手動同期を追加することができる。
- 異なるキューでGPUワークロードをより良く一致させるため、ユーザーは非同期コンピュートのワークロードが実行される窓を明示的に定義したいと考えるかもしれない。
- 例えば、頂点ヘビーなシャドウマッププロデューサー(ダイアグラム中の黒)。
- 手動同期はフェンスリソースに依存することで単純に行われる。(フェンスは他のものと同様にプロデューサーのリソースである。)
- 明示的なフェンシングはリソースであるので、代替の構成で簡単にコピーできる。(例えばロード時/遅延で更新するあるレベルでの静的シャドウマップでは、システムはシャドウプロデューサーが毎フレームスケジュールされないときに自動同期を自動的に追加する。)
- 残りのフェンシングが動的に導かれたため、ユーザーのフェンシングを計算に入れ、グラフィクスワークロードの実行順による余分な同期の一切を排除することができる。(ダイアグラム中では、GバッファのプロデューサーとSSAOの間の自動同期が存在しないことにより確認できる。)
- この目的のための手動フェンシングの代替のひとつは、頂点ヘビー/帯域幅ヘビー/ALUヘビーであることを示すために高レベルでプロデューサーをタグ付けして、自動的にそれらに合うようにプロデューサーをスケジュールさせることである。
Resource Transitions#
- APIはバリアを経由した明示的な遷移を必要とする。
- 伸長、キャッシュのフラッシュ、アイドル待機、などを管理する。
- 最適に近いパフォーマンスを得やすい。
- 多すぎ、汎用的すぎるステート、不必要な中間ステート。
- リソース遷移はシステムを自動的に管理するもうひとつの側面である。
- APIで、これらはバリアを経由して指定される。これは、リソースの伸長、キャッシュのフラッシュ、アイドル待機、などのような処理を管理する。
- 以前に言及したように、とても狭いリソースの視点を持てば、最適に近いGPUパフォーマンスを得ることは容易い。これは、多すぎる個別のバリア呼び出し、汎用的な又は不必要な中間ステート、などを引き起こす可能性がある。
- プロデューサーのリソース依存性を使用して:
- プロデューサー境界で遷移をバッチ処理する。
- マージしたステートの最小セットを決定する。
- バリアを自動分割する。
- リソース依存性を使う。
- 実際にドライバで処理される処理を最小化するためにプロデューサー境界でバリアをバッチ処理する。
- リソース依存性グラフを知っていると、遷移するための最良ステートセットを前もって知ることができるので、不必要な中間ステート変更を避ける。
- リソースを生み出し終わる時と最初に使うために実際に必要となる時の知識を持つことはバリアを分割し、ドライバ内部処理を潜在的に隠蔽することが可能になる。
- この例では、いくつかのプロデューサー(紫)と、深度の伸長を起動できる、2つの”深度書き込み”から”ピクセルシェーダリソース”へのバリア(赤矢印)を確認できる。
- 以前に言及したように、プロデューサーの終わりにバリアを発行する。そして、グラフで次に必要なステートが何かを知っているので、早期に次に必要なステートは遷移させることができる。
- 即時のバリアの代わりに、リソースにアクセスしないことを保証する窓を定義する分割バリアを使うことができ、ドライバにヒントを出すことで、窓での潜在的に高価な内部処理を’隠す’ことができる。
- 以前に言及したように、遷移のリストを持ち、内部ドライバの副次的効果を最小限に削減するために単一の呼び出しでそれらをバッチ処理する。
Producer System : Implementation#
- リソース識別子。
- 2段階: リソースを集める。コマンドバッファを記録する。
- スケジューリング。
Resource Identifiers#
- リンク時依存性を持たないグローバルなリソース識別子。
- 特定の時点でのリソースビューを素早く得るためにハンドルを提供する。
- コンパイル時でインターフェースを制限するために型付けされる。
- 識別子は異なるプロデューサーから特定のリソースを論理的に対処するために使われる。
- リソースIDを使うため、プロデューサーはコンパイル時にお互いに依存する必要がないので、リソースメモリエイリアシングを実装することを可能にする。以前に示したように、ここでは、異なるIDがGPU実行タイムライン中で異なる時間に同じメモリを指している。
- 静的初期化時に、関連するリソースビューを効率的に得るためにプロデューサーシステムで使うことができるインデックスにこれらの識別子を’ベイク’する。
- 有意義なコンパイル時エラーチェックを提供し、関数のオーバーロードを可能にするためにこれらのIDは強く型付けもされる。
Resource Identifiers: examples#
DEFINE_ID_DS(CascadedShadowMap);
DEFINE_ID_RT(LightingDiffuse);
DEFINE_ID_SB(LightTiles);
DEFINE_ID_FE(VisibilityWindowStart);
DEFINE_ID_RC(SetupMaterialTable);
DEFINE_ID_IC(AmbientLightingInputs);
- リソースIDの例。
- DS = 深度バッファ。
- RT = レンダターゲット。
- SB = 構造化バッファ。
- FE = 明示的なフェンスリソース。
- RC = レンダコールバック(他のプロデューサーから関数を呼び出すのに使える)。
- IC = 入力コールバック(いくつかの他の入力依存性を一緒にグループ化する)。
Producer system: interface#
class GfxProducer {
public:
virtual void GetInputOutput(GfxScheduleContext& context);
virtual void Record(GfxRenderContext& context);
};
- その中核では、プロデューサーのインターフェースはかなり単純であり、主なエントリポイントは2つしかない。
Producer System: Gather Resources#
- 以下を指定する。
- リソース依存性。
- 新しいリソース。
- 手動同期。
- リソース識別子エイリアシング。
GatherResource()内で、
- ユーザーはリソース依存性(と、読み書きや深度テストなどの必要とするアクセスタイプ)を指定する。
- また、ここで新しいリソースを指定する(初期ステートのパラメータを定義する)。
- 必要とする手動同期。
- リソース識別子のエイリアシング(本質的にはプロデューサーがスケジュールされた後に他のものにリソースIDを指すこと)。
void TestProducer::GetInputOutput(GfxScheduleContext& context) {
context.Write(ID_DS(DepthBuffer));
context.WriteRead(ID_SB(MaterialTable));
context.Read(ID_RT(GBufferNormal));
context.Input(ID_IC(AmbientLightingState));
context.SignalAfter(ID_FE(ShadowWindow));
context.WaitFor(ID_FE(VisibilityWindow));
context.Alias(ID_RT(ShadowESRAM), ID_RT(Shadow));
}
- 例。
- 上段:リソース依存性とアクセスタイプの指定。
- 中段:手動同期。
- 下段:リソース識別子のエイリアシング。
void TestProducer::GetInputOutput(GfxScheduleContext& context) {
context.New(ID_RT(BloomBuf), width, height, GfxFormat::HDRColor, flags);
context.New(ID_SB(MaterialTable), sizeof(MaterialTableData), count, flags);
context.New(ID_IC(AmbientLightingState), &TestProducer::SetAmbientState);
}
void TestProducer::SetAmbientState(GfxInputCallbackContext& context) {
context.Read(ID_DS(GICascades));
context.Read(ID_DS(LocalCubeMaps));
}
- 例。
- 新しいリソースの生成。
- 入力コールバックの例。
- 同時のリソース数に依存するショートカットを提供できる所。
Producer System: Record#
- 各プロデューサーはユニークなリソースコンテキストが割り当てられる。
- プロデューサーはコマンドリストで使うリソースビューを得るためにこのコンテキストにインデックス付けする。
- 記録ステップ中は、リソースIDを用いてプロデューサー固有のリソースコンテキストを経由して要求したリソースにアクセスできる。
- このリソースコンテキストはすべてのプロデューサーでユニークであり、このプロデューサーのコマンドリストがGPUで実行する時点で割り当てられたリソースビューを含む。
void TestProducer::Record(GfxRecordContext& context) {
cmdList.SetRenderTarget(0, context.Get(ID_RT(LightingBuffer)));
cmdList.Set<PS>(0, context.GetSR(ID_RT(ClipDepth)));
cmdList.Set<PS>(1, context.GetSR(ID_SB(MatTableData)));
context.Call(ID_RC(TestCallback), context);
}
- IDを用いてリソースビューを得る例。
- レンダターゲットビュー。
- シェーダリソースビュー。
- 他のプロデューサーへのコールバック(それが所有するコマンドのセットを発行するかもしれない)。
Producer system: Scheduling#
- 明示的な骨組みスケジュール。
- 識別子エイリアシングのために、CPUの走査順を決定する。
- キューごとのGPU実行順を決定する。
- 依存するリソースのプロデューサーは自動的に追加される。
- 望ましい最終出力とプロデューサー依存性に基づくだけでGPU全体のスケジュールを理論的には構築できるかもしれない。
- 実際の実行順のさらなる制御を得るために、一部の明示的な骨組みをサポートすることを決めた。
- 少なくともいくつかの重要なプロデューサーを追加する(そこにより正確に相対的な実行順を設定する)。
- プラットフォーム/構成固有のプロデューサーが追加しやすくなる(例:コンソールでのメモリプール(ESRAM/DRAM)間の明示的なメモリ転送)。
- 一般的には、キュー間のワークロードを一致させるなどのために実行窓の境界を定義するプロデューサーを追加することでもある。
- この骨組みで指定されていない、必要なリソースのプロデューサーは他のプロデューサーからのリソース依存性によって自動的に引き込まれる。
- CPU: プロデューサーが並列に記録する。
- GPU: 骨組みスケジュールに基づいた順序付け & 導かれた又は明示的なキュー間同期。
- CPUでは、プロデューサーはそれらのコマンドリストを並列に記録して、利用可能なワークスレッドに渡ってCPU使用率を最大化する。(これはプロデューサー間のCPU依存性を取り除くことも強制する。プロデューサーが他のプロデューサーによって使われるパラメータセットを埋めることを可能にするシェーダパラメータシステムがある。)
- GPUでの実行順は骨組みスケジュールの順序付けとキュー間の導出された、または、明示的な同期に基づいている。
- 依存性の発見。
- 各プロデューサーごとの入力と出力を見つける。
- 依存するプロデューサーを引き込む。
- 参照カウントを作る。
- 高レベルステートW->R/R->W遷移を作る。
- スケジュールを構築する最初のステップは依存性を見つけることである。(これは本質的にプロデューサーごとのすべての入力と出力を得ることである。)
- このプロセス中に、まだスケジュールされていないリソースのプロデューサーを引き込み、後のリソースメモリ割り当てを操るためにリソースごとに参照カウントを作る。
- プロデューサー境界をまたぐ、リソースの「書き込み後読み込み」や「読み込み後書き込み」の遷移を追跡し続け、ひとつのキューで動くプロデューサーから出力するリソースは他のキューのプロデューサーの入力として要求されるので、キューをまたぐときに必要なGPU同期のリストを生成する。
- リソース割り当て & 寿命。
- 出力にエイリアスしたメモリを割り当てる。
- プロデューサーのコンテキストに入力を割り当てる。
- グラフで次に必要になるステートに基づいてプロデューサーの終わりにバリアリストを作る。
- 他に渡したり、適切なプロデューサーでリソースを割当解放したりする十分な情報があるので、正しいビューでリソースコンテキストを埋める。
- 最後に、リソースタイプや高レベルなリソース遷移に基づくバリアリストを生成することができる。
- スケジューリング、メモリ寿命、グラフ。
- ゲーム内プロデューサー入力/出力レンダターゲット/テクスチャビュアーUI。
- 検証: フェンスデッドロック、循環依存検出、など。
- この種のシステムの需要なアドバンテージのひとつは、明示的に定期された依存性と自動的なスケジューリングから得られるデータを開拓する、とてもリッチなデバッギングツールを開発できることである。
- 我々が開発した最初のツールは大量の役に立つ情報を含む生成したスケジュールのグラフである。
- メモリエイリアシングの簡単な可視化のための他のグラフの数を生成する。
- ほかには、インゲームリソースビュアーがある。ここでは、(リソースステートをキャプチャするためにスケジュールにリソースコピーを注入することで)プロデューサー境界でのリソースの中身を検査することができる。
- 最後に、ユーザーが引き起こしたデッドロックや循環依存を検出するのに役立ついくつかの検証ツールがある。
Schedule Graph (autogenerated)#
- ここでは、我々のレンダリングパイプラインのある構成のダイアグラムが確認できる。
- GPU実行順は上から下へ。
- GPUキューは青色で表されている(グラフィクスは左側、コンピュートは中央、コピーは右側)。
- プロデューサーはオレンジ色で表されている。
- グラフは詳細を大量に開示しているのでかなり情報密度が高い。
- 図の右側では、いくつかの非同期コンピュート処理を中心とした、スケジュールの窓を確認できる。
- 図の左側では、メモリプールによって色分けされたリソース寿命のバーが確認できる。(上から下へ)
- 右側では、オレンジ色の非同期にコンピュートキューで実行しているいくつかのプロデューサーが確認できる。
- プロデューサーからリソースへの依存性はリソース寿命のバーと接続している横線で表されている。
- それぞれの要素はホバー時に有用な情報を表示するツールチップを持っている。例えば、
- プロデューサーが生成したり依存するリソースのリスト。
- リソースステート遷移のパラメータ。
- フェンシングパラメータ。
- など。
Producer system: Stats#
| フレームごとの平均 | 注釈 |
|---|
| プロデューサー数 | 50 | コマンドリストがマージされる小さなものを含む。 |
| ResourceBarrier()の呼び出し | 150 | 依然として高い。主にプロデューサー内UAVやインダイレクトバッファ。 |
| フェンス数 | 5 | ほぼフレームのフェンスする用。 |
| ExecuteCommandLists() | 15 | バッチ処理による |
| プロデューサーリソース数 | 130 | 未だにプロデューサーの外にいくつかのリソースがある。 |
| リソースメモリフットプリント | 200MB(エイリアシングあり) | 375MB(エイリアシングなし) |
- 大量のプロデューサー(フレーム平均で最大50)があるが、その多くは記録する量が少ないのでコマンドリストをマージする。(最終的には同じコマンドリストに記録することにした。)
- プロデューサーへのバリアの割合は未だ高い。GPUカリング用プロデューサー内UAVバリアやインダイレクトパラメータバッファを埋めるディスパッチが主な理由である。
- 我々はコンソールにおいてこれに非同期コンピュートで取り組んだが、PCではフェンス粒度がこの種の同期のためには十分ではなく、より多くのメモリを使って、これらを減らすためにさらなる処理を行っている。
- メモリに関する寿命管理のほとんどがフレームフェンシングで行われるので、フェンス数はかなり少ない。
- (永続的やタイルのリソースを数えない)1080pでは、ほぼ50%のメモリ節約かメモリエイリアシングを使わないかである。(メモリエイリアシングには、リソースタイプ隔離に関するTier1のヒープ制約といくつかのリソースタイプのためのかなり大きなアライメント要求、といった未だいくつかの注意事項がある。)
- 期待されるように、リソースメモリフットプリントの大半はMIPのストリーミングで取り組む(プロデューサーシステムでは追跡していない)リードオンリーテクスチャによるものである。
Producer system: Summary#
- プロデューサーがAPI呼び出しを最適化するためにリソースとどのように関連しているかという高レベルの知識を活用する。
- 単純なユーザーインターフェイスでリソース遷移とキュー間同期を自動化する。
- 単純なユーザーインターフェイス: プロデューサーと、それらが、どのキューで実行されるか、どの入力に依存するか、どの新出力を生み出すか。このインターフェイスはユーザーをバリアの発行やキュー間同期のようなAPIの責任からこれらを自動化することで救い出す。
- 実行グラフを動的に再構成することでセットアップ特有の変化からユーザを守る。
- オーバーラップしないリソースメモリをエイリアシングすることでメモリフットプリントを減らす。
- (ローエンドマシンでパフォーマンススケーリングのためにあるパスを無効化するような)構成固有の機能から動的に実行グラフを再構成することでユーザーを守って、同期、バリア、などの観点で必然的に伴うであろう副次的効果のすべてを世話する。
- 並列にコマンドリストを記録してCPU使用率を最大化する。
- API呼び出しオーバーヘッドを減らすために小さなプロデューサーとコマンドリスト実行のバッチ処理を実装する。
- 利用可能なワーカースレッド上でコマンド記録を頒布することでCPU使用率を最大化して、同時に、小さなプロデューサーを同じコマンドリストにバッチ処理や合体して、頻繁な実行呼び出しコストのようないくつかの詳細を隠蔽する。
- DX12のバインディングモデルに合わせるためにシェーダパラメータをセットアップするシステム。
- 単純な抽象化を経由してユーザにこのバインディングモデルを開示する。
- シェーダパラメータバインディングインターフェイスは依然として明示的なスロットに基づく。
- 例:
cmdList.SetTexture(TexSlot0, texture);
- 数百の手書きシェーダを持つ、大きなコードベース。
- 我々はいくつかの課題を抱えていた。それらは主に以下に起因する。
- 既存のシェーダ入力バインディングインターフェイスはかなり細かく、依然として明示的なスロットに基づいていた。
- ポートしないといけない大量の手製シェーダがあった。(シェーダグラフからもっと多くの生成されていたが、これらに対してはコードジェネレーターを簡単にパッチすることができた。)
- 以前の粒度のインターフェイスでのシェーダパラメータ設定を例示するダイアグラムを示す。
- プロデューサーはコマンドリストを記録して、大量の細かい入力リソース変更を発行する。
- ワークはDraw/Dispatch時に行われる。(スロットベースのインターフェイスのためにパラメータのいずれかが変更する場合、この大量のワークは繰り返される)
- 変更レートで事前コンパイルされたグループとしてパラメータをバインドする。
- 自動生成されたヘッダを経由したすべてのAPIをまたぐユニフォームSet/Getインターフェイス。
- オフラインコンパイラ。
- ‘Shader Input Groups’(SIGs)の定義をパースして、CPP/HLSLヘッダファイルを生成する。
- ランタイム。
- SIGを不変なブロブにコンパイルする。
- ‘Shader Input Layout’のエントリへバインドする。
- パラメータの事前コンパイルされたブロブのアプローチに移行するために、我々が”Shader Input Group”と呼ぶものの定義をパースし、それぞれCPPとHLSLでパラメータの設定/取得を抽象化するヘッダファイルを生成するオフラインコンパイラを開発した。
- ブロブのコンパイル時に、以下が可能である。(DX12では)
- デスクリプタヒープを埋めて、デスクリプタテーブルを構築する。
- どうやって定数メモリがコピーされるかを隠蔽できる。例えば、直接アップロードヒープを使ったり、コピーキュー関連の遷移を減らすために更新をバッチ処理したりすることで。
- パラメータバインディングポイントは我々が”Shader Input Layout”と呼ぶもので表される。
ShaderInputLayout DefaultLayout <UpdateFreq=LowToHigh>
{
ShaderInputGroup Frame;
ShaderInputGroup Pass;
ShaderInputGroup Material;
ShaderInputGroup Instance;
static const SamplerState PointWrap = {
.filterMinMagMip = POINT_POINT_POINT;
.addressUVW = WRAP_WRAP_WRAP;
};
};
- DX12ではRoot Signatureを生成する。
- 他のAPIではスロット範囲を隔離する。
- レイアウト定義の例。
- 隔離された定数バッファですでに更新頻度により定数をグループ化してあり、このコンセプトをすべてのシェーダ入力パラメータに拡張する。
- DX12において、このレイアウトは同じエントリにバインドするShader Input Groupから必要なりソース最大数に基づくRootSignatureを生成する。
- (更新頻度で分離された)これらのバインドポイントの例。
- DX12では、各レイアウトのバインドポイントエントリはデスクリプタテーブルのセットを定義する(例えば、必要なら、CBV/SRT/UAVのテーブルとSAMPLERのテーブル)。
- サポートしていないプラットフォームのために必要なCPP側のコードを生成することで、APIをまたいで自動的に静的なサンプラを扱うこともサポートする。
- 各シェーダバインディングモード(HLSL文字列/CPP)ごとにRootSignatureを生成する。
- 以下のような詳細を隠す。
- 1.0/1.1のRootSignature。
- Tier制約/最適化(ヌルのCBV/UAV、テーブルのマージ処理、Pushパラメータ、など)。
- レイアウトでは、SIGコンパイラは以下を行う。
- 必要な可視性フラグを持つ各ステージの組み合わせごとにRootSignatureのいくつかのバージョンを生成する。
- ドライバへの実行時ヒント出しのために適切な静的フラグを持つ1.1バージョンのRootSignatureを生成する。これにより、パラメータ管理を最適化できる。
- Tierの制約、ルートテーブル数を減らすため他の最適化を実装すること、Rootに直接パラメータを配置すること、を扱う。
Include Test2;
ShaderInputGroup Test <BindTo=DefaultLayout::Frame>
{
static const uint testValue = 2;
float4 value;
Texture2D<float4> tex0; <Default=Black2D>
RWTexture2D<float4> uav0;
SamplerState sampler; <StaticSampler=PointWrap>
Test2 subType;
}
- HLSLライクなシンタックス。
- テクスチャ/バッファ。
- 一般的な定数型のすべて。
- サンプラ。
- ネストしたサブタイプ。
- リッチなアノテーションセット。
- デフォルト値。
- 静的な割り当て。
- デバッグコードの自動生成。
- “Shader Input Group”の定義は多くのHLSLシンタックスがあり、加えて、いくつかのアノテーションサポートがある。
- 定数と、リソースとサンプルの完全なセットを指定できる。
- 広いアノテーション範囲をサポートする。(例:固有のバインドポイント、リソースのデフォルト定義、デバッグコード生成、など)
- より簡単に分離したパラメータグループを管理するために、ネストしたタイプを指定できる。
#include "sig/test.h"
void GfxTestProducer::CompileParams(GfxDevice& device) {
sig::Test test;
test.SetValue(ubiVector4(1.0f, 2.0f, 3.0f, 4.0f));
test.SetTex0(testTexture);
test.SetUav0(testUAV);
m_CompiledTestParams = test.Compile(device);
// ...
cmdList.SetShaderInputGroup(m_CompiledPassParams);
}
- SIGは不変ブロブにコンパイルされる。
- コンパイル時に、デスクリプタをGPUにコピーする。
- パラメータのバインディングはコピーを伴わず、単なるルートデスクリプタテーブルの設定ハンドルである。
- 実行時に、自動生成されたセッターインターフェイスを使う。(これらは型情報やアノテーションに基づいた検証レイヤーを提供する。)
- 必要なシェーダーパラメータを埋めるためにSetメソッドを使う。
- これらをバインドできるようになる前に、これらを不変ブロブにコンパイルするよう要求する(この時点でデスクリプタのコピーを発行する)。
- 一度ブロブを持てば、デスクリプタテーブルのハンドルまわりを効率的に渡すだけで、複数回それを再利用できる。
#include "sig/test.hlsl"
void main() {
uint2 index = (uint2)g_Test.GetValue().xy;
float4 value = testFunc(index, testFunc.GetSubType());
g_Test.GetUav0()[index] = value;
}
float4 testFunc(in int2 index, in Test2 test2) {
return test2.GetTex0()[index];
}
- パラメータはGetメソッドを介してアクセスする。
- パラメータグループは構造化された方法で渡されることができる。
- 静的サンプラのような機能は透過的に扱われる。
- シェーダインターフェイスの観点では、これもかなり単純である。
- リソースや定数にアクセスするために自動生成されたGetメソッドを使う。
- ネストしたSIGタイプの使用はグローバルパラメータに依存したり個別のパラメータの長く大きなリストをわたしたりせずに簡単に再利用できるシェーダコードヘッダを記述することが可能になる。
- バッファからパラメータの構造をロードするために自動生成したローダ関数をサポートする。これは描画インスタンシングのコードで使う。
- アノテーションに基づいて自動生成されたGPUデバッグトレーシングコードもある。
| 平均値 | メモ |
|---|
| 読み込まれた静的デスクリプタ数 | 15000 | アセットロード時に一度だけコピーされる |
| Transientデスクリプタ数 | 5000 | 毎フレームコピーされる(mostly for pass SIGs) |
| ユニークなSIGレイアウト | 10 | |
| ユニークなSIG定義 | 300 | |
- このシステムに関係する統計データがある。
- ご覧の通り、ほとんどのシェーダ入力パラメータは静的である。(これらはその関連するデスクリプタや定数がアセットロード時に一度だけコピーされるマテリアルSIGに由来する。)
- ほとんどのTransientデスクリプタのコピー処理は個別のドローコールの間ではなく、プロデューサーの開始時にある。
- ユニークなSIGインスタンスは実際にデスクリプタテーブルの大きさに関してこれらの間で大まかに一致させることになる。なので、ほとんどが地形、水などのような非常に固有なレンダリングのためであり、ユニークなレイアウトはそう多くない。
- 下地のAPI詳細を抽象化する。
- RootSignature/DescriptorTable
- 事前コンパイルは早期最適化の機会を提供する。
- 変更頻度でデスクリプタのコピーする/定数バッファを更新するだけ。
- 更に単純な低レベルグラフィクスステート管理。
- まとめ。
- (RootSignatureやDescriptorTableのような)基礎となるAPIの詳細を抽象化する、ただしとても薄くする。
- デスクリプタはそれらが変更されるおおよその頻度で更新される。
- インターフェイスは個別のスロットではなくテーブルのポインタによる多くのハードウェアが動作する方法に近いため、オーバーヘッドが最小になり、最終的にバインドポイントは少量(5-6)になった。これは非常に単純な低レベルグラフィクスステート管理コードであることを意味する。
- どうやってデスクリプタが更新されるかについての内部的な知識はドライバへの最適化ヒントとなり、ヌルCBV/UAVのような内部Tier制限を実装するために更に最適なRootSignature1.1を生成することができるようになる。
- シェーダプロファイル5.1に伴う問題に用心する(レジスタスペースを使いたい場合)。
- ドライバの最適化にヒントを出すためにRootSignature1.1を扱う。OSのサポートを確認する。
- シェーダプロファイル5.1への移行は予想していた以上に”辛い”作業だった。(ルートシグネチャテーブルでスロットの管理の簡単化のためにレジスタスペースを主に使って行った。)
- 多くのコンパイラの問題やクラッシュがあった(そのほとんどは今では解決されている)が、それでもいくつかは残っている(例えば、リソースによる配列のインデクシング)。
- 異なるバインディングモデルはFXCの最適化ルールを変更する。つまり、DXBCをプロファイル5.0と同等にするために”/all_resources_bound”オプション(Marceloのブログを参照)の使用を調査する。
- 1.1以降のRootSignature管理は若干厄介でWindows10RS1からのみサポートする。つまり、古いバージョンのWindowsのシステムをサポートする必要がある場合は管理を気を付けて行う。
Pipeline State Management#
Pipeline State Objects#
- DX12の重大なインターフェイス変更。
- シェーダ、レンダステート、などを含む。
- 必要に応じてコンパイルするのは高価。
- APIは事前コンパイルしたブロブをロードする方法を提供する。
- PSOはDX12において重大なインターフェイスの変更であり、ほとんどのステートは単一のブロブを経由して束縛される。
- これにはシェーダのコンパイルが起こる可能性があるので、必要に応じてコンパイルするのはコストが高い(数百ms程度かかることも)。
- シリアライズされたブロブをロードすることができ、ドライバは内部キャッシングと派生ステート最適化を実装する。
- 我々にとって、エンジンの以下のところに由来する様々なソースにより、PSOのレンダステート部分が最も問題のひとつとなった。
- CPPコード、マテリアル、アーティスト駆動エフェクトスクリプト、など。
- ブロブのみのインターフェイスはとても大量のレンダリングコードをポートすることを要求する。
- データドリブンのマテリアルコードパスに基づくブロブ、レガシーコードドリブンパスのための既存のもの、の両方のインターフェイスを開示することを選択した。
- パフォーマンスについて考えたり、最適なシェーダを生成したりするとき、事前コンパイルされたレンダステート+シェーダは素晴らしい。
- 実践では、殆どのエンジンはレンダステートの変更の出所についてかなり緩いアプローチを取っており、アーティスト向けインターフェイスによるレンダステートの細かな変更すら許可していた。
- しかし、まったく異なる2つのレンダリングコードパスをエンジンに持っている。
- データドリブンで、事前コンパイルされたシェーダの順列に基づく、マテリアルベースベースレンダリング。(描画中の90%を網羅する)
- ディファードライティング、GPUカリング、ほとんどのポストエフェクト、などのような機能のための手製のHLSLベースコード。
- 我々はすべてのコードを一気にポートしなければならなくなることを避けるため、これに基づく新しいブロブと並行して古いインターフェイスを維持することにした。
Legacy granular interface#
- シェーダパラメータへの同様のやり方で、プロデューサーは非常に細かいステート変更を発行した。
- 内部のステートコンパイルは実際にドロー/ディスパッチを発行したその時まで遅延された。
- この時点でレンダステートやシェーダなどに基づいて適切なパイプラインステートブロブを探したり、必要であれば新しいものを生成したりする。これは、レンダリングスレッド、つまり、潜在的にはフレームレートに重大な影響を与えるヒッチングを生じさせる。
PSO Blob interface#
- 事前コンパイルされたステートグルーブのバインディングを必要とする。
- ステートプリセットを用いて行う事ができる制限された独立したステート変更。
- ロード時/オフラインブロブコンパイル時最適化の機会を設ける。
- ブロブアプローチでは、
- ステートのブロブはオフライン(マテリアルレンダリングコードパスの場合)か、ロード時(ローディングスレッドで)かのいずれかでコンパイルされる。
- 更に単純なステート設定インターフェイスを持つ。SetPipelineStateBlob(と、レンダターゲット/ビューポート、その他)。
- これの結果として、ステートキャッシュがより単純になり(実質的にブロブのポインタを管理する)、ドロー/ディスパッチ時のオーバーヘッドがより小さくなる(コマンドリストの記録スレッドでヒッチングが起こらない)。
PSO Blob interface: Challenges#
- Anvil Nextのマテリアルはデータドリブンなグラフに基づいている。
- ユニークなエフェクトを開発するためにアーティストに多大な柔軟性を提供する。
- 最適化されたシェーダのためのたくさんの自動生成されたマイクロコード順列(ディファード/フォワード/深度のみ/頂点フォーマット/など)。
- 初期の課題。
- データシェーダは頂点フォーマット、メッシュオプション(インスタンシング/クラスタリングモード)、いくつかのレンダパス(Gバッファ、フォワード、深度のみ、など)で最適化されたシェーダをサポートするために大量の順列を生成するデータグラフに基づいている。
- データベイク時に、前もってすべてのシェーダ順列を事前コンパイルする。
PSO Blob interface: Challenges (2)#
- AC:Syndicateの統計。
- マテリアルグラフ: 約500 (ほぼユニークなエフェクト用)
- マイクロコード順列: 約130000!
- ゲームを立ち上げてから約10分後には、
- 約125個のシェーダテンプレートがロードされる。
- “レンダステート+シェーダのハッシュマップ”は約650個のエンティティを持つ。
- いくつか見てみると、(AC:Syndicateの統計)
- 約500のマテリアルシェーダグラフ。
- これらのマテリアルのほとんどはエフェクトシェーダであり、特定のカットシーン/ミッションで使われる。
- ごくまれに使われる(または、実際にはゲームセッション中にまったく使われない)大量の順列が確認できる。
- マイクロコードの事前コンパイルに頼っている。
- なので、うまく定義されたデータのセットを持っているが、生成されなかった順列が必要な普通でない状況には対応できない。
- ご覧の通り、チェックされないでいると、順列の数が管理できなくなる可能性がある。
PSO Blob interface : Strategy#
- アーティストに開示しているインターフェイスは粒度が細かすぎる。
- アート側からの妥協が必要になる。
- アーティストはレンダモードに基づいたレンダステートのプリセットから選択する。
- シェーダ機能をより大胆に抜粋する。130000から10000へ。
- 我々が取ったごく初めのステップはシェーダ順列の数に取り組むことだった。
- 慣例的にAnvil Nextのアーティストは以下ができた。
- マテリアルごとに個別のレンダステートを指定する。
- エフェクトシステム経由で実行時にマテリアルのレンダステートをトグルする。
- 以下の制約を設けた。
- プリセットのレンダステートグループと機能の並べ替えのみを使う。
- 実行時に個別のレンダステートをトグルする機能を取り除き、代わりにマテリアルを入れ替えることができるようにする。
- これはマテリアルテンプレートごとの順列数を大幅に減少するのに役立った。
PSO Blob interface : Strategy (2)#
- 実行時、我々はすでにPSOを指すための理想の場所を持っている。Material。
- ローディングスレッドでPSOを関連付ける(disk load/compiled derived)。
- レンダリングはレンダリングモードに基づいたPSOを指す必要があるだけ。
- シェーダ順列側を少しだけ制御下に置いたのち、我々はシェーダのマイクロコードで行った方法と似たやり方でパイプラインステートの事前コンパイルに移動した。
- マテリアルインスタンスには、シェーダマイクロコード、レンダステートのリスト、などへの参照を以前に格納していた。これは、前の粒度のインターフェイスを経由して個別に適用されていたが、今は単にPSOの参照を必要とする。
- これらの順列のすべては前もって知っているので、ロード時やレンダ時にこれらをメッシュへ、現在のレンダモードに基づく単純な間接参照として、事前に関連付けることができる。
- これは事実上、マテリアルグラフごとにひとつ、PSOデータベースを持つこと意味する。これらのエンティティはロード時にインデックス付けされてキャッシュされる。
Pipeline state objects: Stats#
| フレームごとの平均値 | メモ |
|---|
| マテリアルPSOバインド(高速パス) | 2000 | ブロブはローディングスレッドでキャッシュされる |
| 非ネイティブPSOバインド(レガシーパス) | 200 | 残りのコードをネイティブPSOパスへゆっくりポーティング |
| Material::Bind()でのCPU節約率 | ~40% | ここでのコードのほとんどは細かなレンダステート設定が行われていた |
- PSO関連の統計。
- レンダリングの大部分が事前コンパイルされたPSOコードパスを使っていることが分かる。
- 依然として古いコードパスを使っているものが多少あるが、ヒッチングを抑制するためにウォームアップ可能なPSOキャッシュも使っている。これらのパスをブロブインターフェイスを使う方法へとゆっくりポートしている。
- 処理の多数はレンダステートを設定することと関連していたので、マテリアルでは良好な節約率を見せている。
Pipeline State Objects: Summary#
- ステートのブロブ化は極めて制約的である。
- マテリアルはブロブインターフェイスを簡単にポートするためにレンダリングコードパスをバッチする。
- データシェーダにいくつかの制約を追加することで。
- レンダリングの多数(ドローの90%以上)/パフォーマンスゲインのほとんど。
- ブロブ化は制約的である。より問題となるケースがいくつかある。
- カスタムのデプスバイアスを用いる(カットシーンでは、例えばセルフシャドウの問題を回避するために行われる大量の調整が存在する)。 --- 代わりにテストシェーダで行うようにすることにした。
- エフェクトによるレンダステート実行時トグルができないので、代わりにマテリアルを切り替えなければならない。
- デバッグモード(ワイヤフレーム、ピッキング、など)は大量のPSO順列を生成する可能性があるが、これは非リリースビルドに対してのみ考えればよいことである。
- CPUレンダリングに関するコストの大部分はマテリアル+メッシュのコードパスにあり、パフォーマンスゲインの多数でもあり、残りのコードは新しいブロブインターフェイスに徐々にポートされる。
- どの順列が利用可能かという知識を持つことで、ユーザのマシンでオフラインにコンパイルすることさえできる(インストール時や、ドライバの入れ替えを検知したとき、など、PSOキャッシュを再生成する)。
Pipeline State Objects: Summary (2)#
- マテリアルでないレンダリングコードの残りは重要ではない。
- しばらくの間、古いインターフェイスを維持する必要がある。
- ブロブコードパスはとても単純な低レベルステート管理を持つ。
- ブロブのポインタだけで、ハッシュ化やレンダリングスレッドでのシェーダコンパイルによるヒッチングが起こらない。
- 最終的には、DX12インターフェイスが要求する理想的なものに近い、とても単純な低レベルステート管理に仕上がった。
- しかし、長年エンジンを使ってきたユーザに制約を課すのは通常は一般受けするとは言えない。
Main lessons:#
- API使用を最適化するために高レベルなレンダパスの知識を活用する。
- 高レベルなプロデューサーシステムは多くのレンダリングエンジニアのデバッグ時間を節約する。
- より粒度の大きいブロブベースのレンダリングインターフェイスはCPUパフォーマンスゲインを最大化し、繰り返しの作業を回避する。
- アーキテクチャ的な仕事は他のプラットフォーム/APIで利益をもたらすことだろう。
- 我々の経験からの主な教訓。
- API使用を最適化するために高レベルなレンダパスの知識を活用できるシステムの実装に投資する。
- これらのシステムはレンダリングエンジニアの時間の観点で精算する。
- リッチな可視化ツールの助けによりデバッグを促進することで。
- レンダパスを分離し、モジュール化を進め、コードを綺麗に理解、デバッグ、再利用しやすくする。
- より粗いレンダリングユーザインターフェースは新しいグラフィクスAPIが期待する方向に進め、ステート事前バッチングを強制し、結果としてランタイムグラフィックステート管理を更に単純化される。
- ステートの事前コンパイルは早期(オフライン/ロード時)最適化で可能になり、繰り返しの作業を回避し、ローディングスレッドへ潜在的に高価な処理を移すことができる。
- 最後に、大量のこの高レベルの処理は他の似たようなグラフィクスAPIへのポートをよりしやすくする。古いものにも利益をもたらす。これは、主に最小化の方法について考え、ステート変更をグループ化することを強制したためである。
DX12 Gains:#
- 現在のDX12バージョン対DX11バージョンの高レベル統計。
- 現在のGPUゲインは小さい(約5%速い)ので、DX11ドライバのマッチングは実際にはごくわずかの処理であり、ここでのゲインのほとんどは主にいくつかのIHVでの非同期コンピュートによるものであった。(非同期コンピュートのインスタンス及びトライアングルカリングのようなコンソールで行われる非同期コードのいくつかの部分は細かい低オーバーヘッドなキュー間同期プリミティブが欠けているためにPCにうまくポートできない。これは結果としてグラフィクスキューのプロデューサー内の理想的なバリア数より未だに多くなる。我々はこれを克服しようといくつかの仕事を行っており、コードをリファクタリングすることで、より多くのメモリを犠牲に細かな同期への依存度を下げている。)
- CPUに関して、DX12は我々のレンダタスクで15%-30%のより良いゲインを見せている。
- これらは実行時の描画事前バッチ処理対APIコール数で行う処理の比率にかなり依存して変化する(バッチ処理が多くなるとゲインは少なくなる)。
- DX11にある他のUMD(ユーザモードドライバ)スレッドを持たないことによって得られるパフォーマンスはここには含まれなかった。
Conclusions:#
- DX11と同等のパフォーマンスを達成するのは大変な仕事である。
- パフォーマンスをすべてとは考えないで。
- 努力を以下への入口として見よう:
- 非同期コンピュート、mGPU、SM6、などのような機能をアンロックする。
- コンソールと同等の機能に近づける。
- エンジンアーキテクチャを改善するための機会。
- 他の同等のAPIへのポーティングは後により容易になる。
- 結論として、
- そのままのパフォーマンスを気にするだけの狭い視野を取るのなら、単にDX11と同等のパフォーマンスを得るだけにかかるリソースと努力の量におそらく満足しないだろう。
- 私は、より広い視野からものを見て、以下の入り口として見るべきであると考えている。
- 新しく開示された機能(非同期コンピュート、マルチGPU、シェーダモデル6、など)へのアクセスをアンロックする。
- コンソールの機能セットと多かれ少なかれ統一する。
- Vulkanのような他のAPIへポートするための基礎工事の大部分を行う。