120FPS対応を仰せつかった,その時のパフォーマンスについての反省点のメモです。
* ComputeSkinningはEarlyDepthと並列で動作させるべき。
非スキニングメッシュをグラフィックスパイプで描画し,その裏でコンピュートパイプでスキニング処理を実行。完了後にグラフィックスパイプに投入できるように変更すべき。つまり,SkinningメッシュとStaticメッシュはコマンドを分けておかなければならない。
* コンテキストロール回避をあらかじめ仕組みとして用意しておく
シェーダのハッシュ値をもとにソートして,コンテキストロールが発生しづらいように設計しておいたほうが良い。PC版DXCでもハッシュ値が公開されていたはずだと思うので,それを調べてソートする感じ。
* プラットフォームによってコンピュートシェーダが速い
ピクセルシェーダを使うとWaveを使いきれないプラットフォームがあるので,フルスクリーン描画系はすべてコンピュートにしておいたほうが良い。これはとあるカンファレンスでも説明されているので,そちらの資料を参照されたし。どの資料かはお教えできません。
* 深度バッファのダウンサンプルを使いまわせる設計にすべき
何度もダウンサンプルすると遅いので,SSAO, RayMarchShadow, SSR等すべてのスクリーンスペースエフェクトで深度バッファを使いまわせるようにあらかじめ設計すべき。さらにいうとAMD SPDとかみたいな実装でダウンサンプル自体も1パスで実行するべき。
* 非同期コンピュートをフル活用できるようにする
できるだけ非同期コンピュートで実行できるようにレンダリングパスを考えるべき。特にフレームの中盤以降にRTVやUAVの依存関係で,動かせるものがなくなる傾向がある。でも,非同期コンピュートは空いているので,この時間を有効活用できるようにフレーム全体を設計すべき。結局使いどころとしては,コンピュートスキニング,VFX,タイルライトの事前処理や,カリング処理関連になるかと…。
* 半透明重い
おそらく次世代機でも半透明が重いのは変わらないと思われる。縮小バッファやTranslucencyLightingVolumeは先に対応しておき,ON/OFFできるように用意しておくのがよろし。Publisherさんによっては,Hardware-VRSは使わないでとか言われるので,Software-Based Variable Rate Shadingを実装しておくのが良いかもしれない。
* FSR重い
開発終盤にFSRパス全体がボトルネックになりやすい。特に深度を作るパスなどは先行して処理することができるので,FSRのパスの一部を切り分けできるようにしておいたほうが良い。また,とある界隈では最適化されたものが配布されているので,そちらをベースにして各プラットフォーム用に動作するようにカスタマイズするのがおススメ。自分で組み込んだ感じだと0.1ms程度通常版よりも高速化した。
* 16bit専用命令を駆使する
レジスタプレッシャー下げや高速化のために,16bitのhalf floatにすることはよくあると思うのだが,普段そこまでカリカリにチューニングしなくても済んでしまっているので,ノウハウがなかったが,できるだけfloatに戻さずに専用命令で演算し続けたほうが良い。単にhalfにするだけで四則演算するだけじゃだめ。ハードウェア命令を駆使するように。先ほど述べたFSRの最適化もこのあたりを使いまくっている。
* wave32のほうが速いケースがある
PS4なんかはWave64モードしかないので,スレッド数が64になるようにコンピュートシェーダとかで組んでいたと思いますが,最近のハードはwave32ベースで作られていて,wave64で実行する場合はエミュレーションされる命令などもあるっぽい。実際にとあるプラットフォームではシェーダコンパイラがちゃんと警告を出してくれて,wave32で実行したほうが良いことを促してくれる。また,Divergenceの多い複雑なシェーダなどではwave32にするだけで,高速化する場合もある。特にフォーワードレンダリングのシェーダなんかは複雑になりやすい傾向があるので,そういうものはwave32にしたほうが良い。またコンピュートシェーダで起動スレッド数が少ないものもwave32モードにしたほうが良い。
* バリア処理はまとめる
いうまでもないがバリア処理はまとめてバッチングするように。過去にカプコンさんだったりの資料で,そうしたほうが高速化するという実例が出ているので,面倒だけどもちゃんとまとめるように。…というかレンダーグラフとかパスグラフみたいなシステムがちゃんと作ってあるなら,そこで吸収するように作ってあるはず。もしつくっていないなら,今すぐにそうなるようにプログラムを組んだほうが良い。
* 何でもかんでもバインドレスにしない!
レンダーターゲットにもテクスチャにも使用されていない的な警告がグラフィックスデバッガで出ることがあるが,バインドレステクスチャとして使っている場合に,使用していることがスルーされることがあるっぽい。実際にあった事例だと,使っていないから消してOK的な指示があるので,遠慮なく削除したところ,見た目がバグることがあった。で、コードを追って調べたところバインドレステクスチャとして使っている箇所だった。なので,基本的にはバインドレステクスチャにしなくてもいいところは無理にしないほうが,ツールのアシストが効くので最適化に役立つことがある。何でもかんでもバインドレスにしないこと!
* 出力シェーダアトリビュートは4つ以下にする
出力パラメータが4つまでなら,処理負荷がかかることはないが,4つを超えると処理負荷がかかって遅くなる。そのため,頂点シェーダやメッシュシェーダからの出力パラメータは可能な限り4つ(float4が4つ)以内に収まるようにパッキングを行う。とあるプラットフォームだけが特定キーワードを付け足すと自動でパッキングしてくれるが,そのような機能が用意されていないプラットフォームもあるのでfloat16_tを使ってできる限り詰め込むと良い。
* 帯域を下げる
R9G9B9E5フォーマットなどを使い,R16G16B16A16_FLOATなどのフォーマットは避ける。R11G11B10_FLOATがあるが,こちらは過去タイトルで絵的なバグが出た事例を聞いたことがあるのと,あんまり精度がよくないため,お勧めできない。前述のR9G9B9E5フォーマットを使うようにする。この際に,アルファブレンドしているものがあると置き換えに苦労するので,デバッグ系描画などはシェーダ上で手動ブレンドして3チャンネルしか使わないようにするなど,あらかじめ最適化を見越した作りにしておいたほうが良い。
* 頂点シェーダは可能な限り使わない
頂点シェーダを使うだけで重いので,できる限り使わない。メッシュシェーダやコンピュートシェーダで処理できるようにしておく。
* コマンドジャンプを使う
奥の手としてコマンドジャンプを使って色々なことを実装することが可能なので,PipelineStateをGPU上で設定したりなど,PCでは出来ない,コンソールならではのことをやって最適化を行う。こうすることによってCPUのドローコールは減らせるので,CPU処理がネックだった場合は高速化ができる。
* バッファのアロケータはちゃんと作る
CommittedResourceはアライメントが64KiBになるので,例えば16Byteの定数バッファをアロケートしても64KiBでメモリが取られてしまうので,小さいバッファ用に自前でちゃんとアロケータは作っておいたほうが良い。作っていないとメモリ周りで嫌な思いをすることになる。
自分が作ったやつだと,VB or IB or (!UAV && ByteAddressBuffer) or (!UAV && StructuredBuffer)の条件を満たして,64KiBなら同一リソースから切り出して使うという実装にした。!UAVにしているのは1個のResourceの場合,リソースステートが部分的にWriteになったりさせることができないので,基本的にはReadしかしない対象に限定するため。これで数百MB減ったケースがある。
* Decompress処理をしないようにする
Decompress処理が走ると重いので,最近のやつだとDecompressしなくていいようにするためのフラグがあったりするので,そいつを付けておく。
* 最初からWave Instrinsicsを使って最適化しておく
終盤に命令最適化すると,バグで困ることが多々あるので,あらかじめ最適はできるところは最初からやっておく。終盤にしかできない場合は,ちゃんとデグレを検知するための単体テスト環境など,機械的なチェック環境を用意しておくこと。人力によるチェックはすり抜けることが当たり前にある。
* GPUネックになりやすいものは一部処理をCPUに逃がす。
今世代でもGPUネックになりやすい。そのため,CPU側に逃がせるものは逃がすようにする。例えば,オクルージョンカリングはソフトウェアラスタライズのもの使うとか,VFXの計算の一部をCPUでやるとか,メッシュのLOD判定をGPUからCPUに移すとか…。
* 非同期解放処理システムを作っておく
同期させるとやっぱりスパイクなどが発生しやすいので,1フレームでの処理対象を制限するか,そもそも大丈夫なように非同期解放の仕組みをつくっておくかしないと,ロード時・ロード直後に痛い目に会うことになる。
* その場しのぎの適当なコードを書かない
w成分空いているからここに高さデータいれちゃえーとか適当な実装をしていると,前述したパッキングによる最適化を行う際に,都合が悪いケースが出てきて,困ることがある。どこにどのデータが入っているかは,誰から見ても分かるようにしておくこと・そしてきちんと管理しておくこと。そうしないとエンバグが発生しまくって,自分の首を絞めることになる。
* ループ処理を見直す
早い段階で,continueできれば重い処理を回避し,最適化できるケースはちゃんと最適化する。メッシュ描画などは何度も呼ばれるため,意外と大したことがない変更でもめちゃくちゃ速くなることがある。
* デバッグツールは用意する
プログラマー的なグラフィックスデバッガーを使いこなせるアーティストは少ないので,ゲーム上にデバッグツール・可視化ツールは用意しておく。大量生産前に用意しないと,そのあとは忙しさで作られることないと思ったほうが良い。特にオーバードローやシェーダの複雑度あたりは必須。