拙訳
一般的なアドバイス(GENERAL ADVICE)
パフォーマンスの80%はそのハードウェアを理解することに起因する
20%はそのAPIを効率的に使用することに起因する
Direct3D 12のCPUパフォーマンス(DIRECT3D 12 CPU PERFORMANCE)
- Direct3D 12は低いCPUオーバーヘッドのために設計されている
- マルチスレッド化されたコマンドリスト記録を用いる
- 実行時のヒープの生成/破棄を回避する
- CPU/GPU同期ポイントを回避する
Direct3D 12のGPUパフォーマンス(DIRECT3D 12 GPU PERFORMANCE)
- Direct3D 11ドライバは過去8年かけて最適化されてきた
- はじめのDirectX 12移植はDirectX 11より大幅に低速である傾向がある
- すべてのDirectX 12の恩恵を受けられるようエンジンを再設計する
- 非同期キューはDirectX 11のパフォーマンスに打ち勝つのに役立つ
- アジェンダ
- 一般的なパフォーマンスのアドバイス
- デスクリプタセット
- 複数の非同期キュー
- バリアの理解
- メモリ管理のベストプラクティス
ズバリGCNとは(GCN IN A NUTSHELL)
- ハードウェアは変わっていない — Direct3D 11のアドバイスが依然として当てはまる
- 現在のAMDのハードウェアの要約
- いくつかのCompute Units (CU)
- FuryXでは64個
- CUあたり4つのSIMD
- SIMDあたり最大10個の実行中wave fronts
- wave frontあたり64個のスレッド
- いくつかのCompute Units (CU)
- VGPR数が多いと、wave front数が制限される可能性がある
- CodeXLを用いる
- 現在のAMDのハードウェアの要約
一般的なパフォーマンスのアドバイス(General PERFORMANCE ADVICE)
ほとんどのパフォーマンスアドバイスは依然として当てはまる
- カリング: GPUに必要のない作業を送らない
- コンピュートトライアングルフィルタリングを検討する
- 見に行こう: [Wihlidal 2016Wihlidal, G. 2016. Optimizing the Graphics Pipeline With Compute. Game Developers Conference. https://gdcvault.com/play/1023109/Optimizing-the-Graphics-Pipeline-With.]
- ソート: 不必要なオーバーヘッドを回避する
- パイプライン(やパイプライン内部に使われるPS)でドローをソートする
- 前から後へレンダリングする
- バッチ、バッチ、バッチ (ゴメンヤデ)
- 小さなドローコールはGPUを満たさない
Direct3D 12のパフォーマンスアドバイス --- プロファイリング(DIRECT3D 12 PERFORMANCE ADVICE - PROFILING)
エンジン内パフォーマンスカウンタを追加する
D3D12_QUERY_TYPE_TIMESTAMP- 結果を取得しようとしてストールしないように
D3D12_QUERY_DATA_PIPELINE_STATISTICSVSInvocations / IAVertices: 頂点キャッシュ効率CPrimitives / IAPrimitives: カリングレートPSInvocatons / レンダターゲット解像度: オーバードローPSInvocations / CPrimitives: Geometry bound?- 深度のみのレンダリングはPSを使わないことに気に留めておく
- 深度テストは
PSInvocationsを減少させる
デスクリプタセット(DESCRIPTOR SETS)
デスクリプタセット(DESCRIPTOR SETS)
- ルートシグネチャ
- 最大サイズ: 64 DWORD
- 以下を含むことができる
- データ(大量のスペースを要する!)
- デスクリプタ(2 DWORD)
- デスクリプタテーブルへのポインタ
- 単一のデスクリプタヒープを維持する
- リングバッファとして使う
- 静的サンプラを使う
- 最大2032個
- 64 DWORD制限にカウントしない
デスクリプタセット(DESCRIPTOR SETS)
- ドローごとに変化する、小さくて、すごく使う定数のみを直接ルートシグネチャに置く
- 更新頻度でデスクリプタテーブルを分ける
- もっとも変化しやすい要素を最初に置く
D3D12_SHADER_VISIBILITYフラグを用いる- マスクではなく
- 厳密な可視性を設定するためにエントリーを複製する
デスクリプタセット(DESCRIPTOR SETS)
- 起動時にルートシグネチャはSGPRにコピーされる
- コンパイル時に定義されたレイアウト
- 各シェーダステージに必要なもののみ
デスクリプタセット(DESCRIPTOR SETS)
- 起動時にルートシグネチャはSGPRにコピーされる
- コンパイル時に定義されたレイアウト
- 各シェーダステージに必要なもののみ
- 多すぎるSGPR → ルートシグネチャがローカルメモリにはみ出るだろう
- もっとも頻繁に変更されるエントリーを最初に
- デスクリプタテーブルのはみ出しを回避する!
非同期キュー(ASYNC QUEUES)
D3D12 --- 更なるパフォーマンスの解放
キュータイプ(QUEUE TYPES)
- コピーキュー
- データをコピーするのに使われる
- PCIe転送に最適化される
- シェーダリソースを盗まない!
- コンピュートキュー
- ローカル間のコピーで使う
- グラフィクスと非同期に動作できるコンピュートタスクで使う
- グラフィクスキュー
- なんでもできる
- 描画が通常では最大のワークロードである
キュータイプ(QUEUE TYPES)
- 非同期キューを使うと”タダで”追加のパフォーマンスを得られる
- DirectX 11のパフォーマンスに打ち勝つのに役立つ
- リソースは共有される
- 様々なボトルネックと共にワークロードをスケジュールする
- シャドウは通常ではジオメトリスループットで制限される
- コンピュートは通常ではフェッチに束縛される、稀にALUで制限される
- メモリ効率を最適化するためにLDSを用いる
- 非同期コンピュートはグラフィクスキューのパフォーマンスに影響を与えるだろう
- プロファイリング時にこれを気に留めておく --- 同期パスをエンジン内に留めておく
- 様々なボトルネックと共にワークロードをスケジュールする
非同期キューの使い方(ASYNC QUEUE USAGE)
- 実装アドバイス
- ジョブベースのレンダラを構築する
- これはバリアでも役立つだろう!
- どのタスクが並列に動作すべきかを手動で指定する
- ジョブベースのレンダラを構築する
- ジョブは小さくしすぎないようにすべき
- フレームあたりのフェンス数を1桁台に留めておく
- 各シグナルはフロントエンドをストールして、パイプラインをフラッシュする
灰の中の非同期コンピュート(ASYNC COMPUTE IN ASHES)
我々のレンダリングの内訳(WHERE OUR RENDERING GOES)
フレームの観察結果(FRAME OBSERVATIONS)
- ライティングとほとんどのシャドウのワークはコンピュートシェーダである
- ポストプロセスもコンピュートシェーダである
- フレームの何%がコンピュートキューに持っていけるか
我々のレンダリングの内訳(WHERE OUR RENDERING GOES)
シャドウマップ(SHADOW MAP)
- シャドウを投影されるterrain
- 単純なテクニック
- ただし、エイリアシングを抑制するための広いガウスブラーがある
- 2msかかる可能性がある --- ただし、フレームが遅れる可能性も
- フレームがレンダリング中の間にブラーできるかも
ポストプロセス(POST PROCESS)
- 3つの部分
- 単純なガウスブラー(狭い、5x5)
- 複雑なグレア効果(大きい、スクリーンサイズの非対称なレンズ効果)
- 色曲線 --- ACES
- フレームにはこれ以上の処理がないので、他にオーバーラップするものがない
- 何かあった?
フレームのオーバーラップ(FRAME OVERLAP)
- 前の1フレームを次のフレームの開始とオーバーラップさせる
それほどのレイテンシを引き起こさずに(WITHOUT INTRODUCING TOO MUCH LATENCY)
- フレームのオーバーラップはエンジンでは複雑になる可能性があるだろう
- エンジンはフレーム全体を一度にキューに入れるので、レンダリング中の前フレームという概念が存在しない
- Direct3D 12ならフレームのオーバーラップができそう
基本のアイデア(BASIC IDEA)
- キューに入れられるフレームの数をに設定する
- グラフィクスキューとは別のプレゼントキューを生成する
- レンダリングの終わりに、プレゼントを発行する代わりに、コンピュートタスクとポストプロセスのレンダリングをシグナルする
- ポストプロセスが完了したら、実際のプレゼント行うために代替グラフィクスキューをシグナルする
フレームのオーバーラップ(FRAME OVERLAP)
D3D12のスケジューラ(D3D12 SCHEDULER)
- コマンドストリームを挿入する世話をしたい
- が…
- ほとんどのグラボにはプリエンプションがない
- 故に、コマンドバッファが1〜2msの範囲を維持するように、複数のサブミットにフレームをバラバラにする
- Windowsはその境界でプレゼントを挿入できる
- 最終的には約1/2から1/3の追加のレイテンシーのみとなる
GPUViewでフレームがどう見えるか(WHAT OUR FRAME LOOKS LIKE IN GPUVIEW)
約15%のパフォーマンス上昇(PERFORMANCE INCREASE 〜15%)
リソース管理(RESOURCE MANAGEMENT)
Direct3D 11のメモリ管理(DIRECT3D 11 MEMORY MANAGEMENT)
- OSの構成要素がレジデンシを扱う
- (各コマンドバッファで)
- メモリは時間とともに満杯になる、大抵はそのままビデオメモリに
- 最終的にオーバーフローする
- システムメモリに追いやられる
Direct3D 11のメモリ管理(DIRECT3D 11 MEMORY MANAGEMENT)
- 笠の中の優先度システム
- RT、DS、UAVは移されづらい
- つまり、高帯域幅の読み書きを行うサーフェス
- 重要なものが移される可能性が依然としてある
- …それに気付いた人で、とても気に入っているように見える人は誰一人いない!
WDDM2がすること(WHAT WDDM2 DOES:)
- 限界がどこにあるかをアプリケーションに伝える
- アプリケーションがOS/ドライバよりどのリソースが重要かを知っている
- まさに間違った方向に行こうとしていることを確認できる
- 介入する!
- より低い解像度のテクスチャを使う、高次MIPを削る、フォーマットをBC1に変更する
- 要求の緩いリソースをシステムメモリに移す
- もしくは、しない
- バックアッププランとして移行するだろう
- 5〜10%やそこらの、小さなoversubscriptionsなら恐らくうまくいく
- 20%超だと、恐らくかなり酷いユーザーエクスペリエンスになるだろう
- それ以上だと、stutteringや一貫しないフレームレートが現れる可能性が高くなる
- バックアッププランとして移行するだろう
- 介入する!
予約(RESERVATION)
- “私にはコレがホント必要なんです”と言える
IDXGIAdapter3::SetVideoMemoryReservation
- OSは
QueryVideoMemoryにどれだけ予約できるかを伝えるだろう- フォアグラウンドアプリケーションなら、アイドル時のシステムの約半分のVRAMから始まる
- それより少ない場合、恐らく他の重いアプリケーションがすでに動作していることを意味する
- 他のアプリケーションを閉じる必要があると要求するダイアログをポップアップするのが賢明かも
最小スペックとユーザーの選択肢(MINIMUM SPECS AND USER OPTIONS)
- メモリの枯渇は最小スペックの問題である
- 必要なメモリ量を大まかに知っておく必要がある
- 開発中にこれを追跡する
- ドキッとさせられないようにデザイン/アートにやらせないこと!
- 1GB、2GB、3GBなどのグラボ用オプションとしてハードウェア能力を設定する
- アプリケーションは低メモリグラボで無茶なセッティングを選ばせない
- 1GBのグラボで4kを許可すれば、それだけで全部持っていってしまう
- システム内の他のアプリケーションのために、トータルソリューションはない
- 予約と相まって、十分な制御を持つべき
- 率直に言って、11でやってた以上には
- 予約と相まって、十分な制御を持つべき
MakeResident(MAKERESIDENT)
- MakeResidentは同期的である
- アロケーションが利用可能になるまでブロックする
- まとめてバッチする
- レンダスレッドから外に移さなければならない
- ページング操作は合理的に綺麗にレンダリングとインターリーブする
- 使う前にやる必要がある
- そうしないと、stutterするだろう
先行戦略(RUN-AHEAD STRATEGIES)
- 今後使われるものをかなり前もって予測する
- レンダリングスレッドの2、3フレーム前にやる
- バッファリングが多い == stutteringが少ない
- ただし、システムにレイテンシを注入する
先行戦略(RUN-AHEAD STRATEGIES)
- 実際にはレジデンシはまったく使わない!
- 使うかもしれないリソースをシステムメモリにプリロードする
- それらを即座に移動しなくてもよい
- 使うときに、ローカルへコピーして、デスクリプタを書き換えるかページをリマップする
- メモリ使用をカットする必要があるときは、逆の操作をして、ローカルコピーをevictする
先行戦略(RUN-AHEAD STRATEGIES)
- VRアプリケーションに対する大きな挑戦
- レイテンシーの大きいソリューションは明らかに実行不可能
- 賢明にシステムメモリを使い、ストリーミングでは良好な先読みがなければならないだろう
バリア(BARRIERS)
タンスの角に小指をぶつけないために(そして、時折頭を)
バリア(BARRIERS)
- バリアとは?
- 同期
- 作業の厳格で正確な順序を保証する
- 可視性
- 以前に書き込んだデータが対象のユニットから可視であることを保証する
- フォーマット変換
- データが対象のユニットと互換性のあるフォーマットにあることを保証する
- 同期
同期(SYNCHRONISATION)
- GPUパイプラインの深さのために発生する
- 例: UAVのRAW/WAWバリア
- 実行におけるシェーダwavesオーバーラッピングを回避する
同期バリア(SYNCHRONISATION BARRIER)
- ドロー3がドロー1に依存すると仮定する
- one-pieceなバリアは何をする?
同期バリア(SYNCHRONISATION BARRIER)
- one-pieceなUAVバリア曰く
- 「UAVのことが終わったら、すぐにまた使えるようにする」
- ドライバはシグナルを挿入して待機する。作業のオーバーラップがないことが保証される
同期単一バリア(SYNCHRONISATION SINGLE BARRIER)
- ドロー3とドロー2がドロー1で書き込まれる異なるリソースに依存すると仮定する
- 2つの個々のバリアは何をする?
- 「UAVのことが終わったら、すぐにまた使えるようにする」
- 2つの個々のバリアは何をする?
同期複数バリア(SYNCHRONISATION MULTIPLE BARRIER)
- 同じバリア呼び出しに複数のバリアを置くと、両方が同時に発生する。
同期分割バリア(SYNCHRONISATION SPLIT BARRIER)
- ドロー1とドロー3の間の分割バリア
- ドロー1の”処理完了”後、ドロー3の”準備完了”前
- ドロー2は影響を受けず、3のみ1の完了を待つ必要がある
同期のまとめ(SYNCHRONISATION, SUMMARY)
- 分割バリアは同期を減らす
- 最終使用の終わりと新規使用の始めの間に他の作業があるならば
- 複数同時バリアは同期を減らす可能性もある
- 一度にすべてのバリアを済ませる
可視性(VISIBILITY)
- 大量の小さなL1”キャッシュ”
- 大きなL2キャッシュ
- 主にシェーダのコアへ接続される
可視性 --- 単純なバリア(VISIBILITY - SIMPLE BARRIER)
- バッファのUAV →
SHADER_RESOURCE | CONSTANT_BUFFER- テクスチャL1をL2にフラッシュ
- シェーダL1をフラッシュ
- …で、終わり
可視性 --- コモンステートへの遷移(VISIBILITY - TRANSITION TO THE COMMON STATE)
RENDER_TARGET->COMMON- カラーL1をフラッシュ
- たぶんすべてのL1をフラッシュ
- L2をフラッシュ
- より高価
- より時間がかかる
- よりメモリトラフィックがかかる
可視性(VISIBILITY)
- 単一呼び出しの複数バリアは可視性のコストを減らす
- すべてのフラッシュを合わせたものをフラッシュする
- 示した以前の場合を検討する
- RT->COMMONの上で、RT->SRVを追加する場合のコストはかからない --- タダ!
- 分割バリアは可視性のコストも減らす
- これがバリアを監視したり打ち消したりするのにかけた努力の成果を暗示することに注意する
展開(DECOMPRESSION)
- RTとDSサーフェスは圧縮されているとかなりよく処理する
- 2倍かそれ以上になり得る
- 最新ハードウェアでの2種の異なる圧縮
- フルではRTやDS以外だと読み込むのに展開しなければならない
- パートではSRVでも動作する
- 展開が必要なら、どこかで時間を取らないといけない
- ただし、本当に必要でないときに展開するのは難しくない
- これらを回避するのは必須
バリアの最適化(BARRIERS, OPTIVISATION)
- 展開を含まないバリアは”数us”かかる
- バリアのGPUコストは(主に)タイムスタンプで計測可能
- 数%より大きくなるのは稀
- 巨大なAAサーフェスの展開を含む場合をの除いて
- 数%より大きくなるのは稀
- 書き込まれるサーフェスあたり2つ以上のバリアを必要としない方が良い
悪いパターン(BAD PATTERNS)
- RT -> SRV -> Copy_source -> SRV -> RT
- ステートフラグを一緒にORして組み合わせられることを忘れずに
- read-to-readバリアは一切しなくていい
- 初めに正しいステートに置く
- “時々これからコピーするので、常にRT -> SRV|Copyとしよう”
- RT -> SRはかなり安価になるかも、RT->SRV|Copyはかなり高価になるかも
- 最初に正しいステートに置く
より悪いパターン --- 手短な所で済ませてしまう(WORSE PATTERNS - SYMPTOMS OF WORKING TOO LOCALLY)
- これらを行っているなら、エンジンはDirect3D 12のエンジンではない
- ゆく先を考えなければならない、高レベルで考えなければならない
- “このオブジェクトが次にどのステートになるか分からないから、全部のリストの終わりにすべてCOMMONに遷移させよう”
- このコストは計り知れない
- すべてのサーフェスに展開を強制する
- ほとんどのコマンドリストは事実上始まる前にアイドル待機する
- このコストは計り知れない
- 内部ループにある、または/および、使用中のバリアのみを考慮する
- バリアの結合を防ぐ
最悪になり得る例(THE WORST POSSIBLE EXAMPLE)
void UploadTextures() {
for (auto resource : resources) {
pD3D12CmdList->Barrier(reource, Copy);
pD3D12CmdList->CopyTexture(src, dest);
pD3D12CmdList->Barrier(reource, SR);
}
}リソースのアップロードあたり2つのバリア
それぞれがGPUで恐らくシリアライズしている
この方が良い(THIS IS BETTER)
void UploadTextures() {
BarrierList list;
for (auto resource : resources)
AddBarrier(list, reource, Copy);
pD3D12CmdList->Barrier(list);
list->clean();
for (auto resource : resources)
pD3D12CmdList->CopyTexture(src, dest);
for (auto resource : resources)
AddBarrier(list, reource, SR);
pD3D12CmdList->Barrier(list);
}バリアのまとめ(BARRIER SUMMARY)
- まあ、これはちょっと難しいね
- 毎フレーム書き込まれるサーフェスが主な問題
- 書き込んだサーフェスが破損した?バリア不足
- フレームあたりのサーフェスあたりのバリア2つが対象
- (それと、それより少ないバリア呼び出し)
- ツールを使おう