Pocolです。
喪中につき、新年のご挨拶は控えさせていただきます。
寒い日が続きますが,どうぞ皆様ご自愛くださいませ。
本年もよろしくお願い致します。
パフォーマンス反省点メモ
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できれば重い処理を回避し,最適化できるケースはちゃんと最適化する。メッシュ描画などは何度も呼ばれるため,意外と大したことがない変更でもめちゃくちゃ速くなることがある。
* デバッグツールは用意する
プログラマー的なグラフィックスデバッガーを使いこなせるアーティストは少ないので,ゲーム上にデバッグツール・可視化ツールは用意しておく。大量生産前に用意しないと,そのあとは忙しさで作られることないと思ったほうが良い。特にオーバードローやシェーダの複雑度あたりは必須。
Direct3D 12 ゲームグラフィックス実践ガイド 増刷決定!!
こんにちわ。Pocolです。
技術評論社様より販売させていただいております『Direct3D 12 ゲームグラフィックス実践ガイド』ですが,おかげさまで増刷が決定しました!
Uneral EngineやUnity等が台頭する時代に逆行する内容の書籍なので,すぐに絶版するのではないか?という不安。
また,DirectX12の魔導書なども既に発売されていて,私の書籍など見向きもされないのではないか?
発売しても売れるのだろうか,そもそも買おうとすら思ってもらえないのではないか?
…など色々な不安を抱えながら,なんとか発売までたどり着いた書籍です。
この本は本当にいろいろな方にご迷惑をお掛けしながら,何とか出せた書籍ですので,増刷が決定したと連絡を受けて嬉しかったです。
本当に皆様のおかげです。本当にありがとうございます。
ただ正直,もっと批判とか苦情に近い意見が多いんだろうなと覚悟はしていたのですが,想像していたよりも少なく,また良かったという意見も全くない状態でして,著者としては書いてよかったのか,書いて悪かったのかが何にも分からない。受け入れられているのか,そうでもないのか判断に困るという状態です。
分かりづらいところがあれば,SNS等で遠慮なく書いていただきたいですし,逆にダメなところは今後の執筆に活かせるチャンスとなるので,忌憚のないご意見を書いていただけると幸いです。また,もっと書いて欲しいものとかあるのであれば,応援の意味を込めて「良かったよ!」など肯定的な意見を頂けると,励みになります。肯定的な意見が無ければ,「これはもう書かないほうがいいな」という執筆を辞める決断にもなってしまいますので,応援していただけるのであれば,応援の声も頂けるとありがたいです。
書籍のほうですが,12月ころから増刷版が市場流通する見込みと伺っておりますので,現在手に入れられていない方はもうしばらくお待ちいただければと思います。
今後ともDirect3D 12ゲームグラフィックス実践ガイドをよろしくお願いいたします。
タイル分類化による最適化(1)
こんちゃわ。Pocolです。
相も変わらず最適化でヒーヒーいっています。
ライティングシェーダって複数のマテリアルをサポートするとために,大体Uber Shaderになると思うのですが…
それだとやっぱりswtich-caseなどの分岐で重くなりがちです。
分岐を除くと,占有率の改善がみられ,速くなったりすることがあります。
そのため「分岐を無くそう!」というのが今回のネタで,それを実現するための資料について紹介します。
Deferred Lighting in Uncharted 4
まず,1つ目は「Deferred Lighting in Uncharted 4」です。
これはSIGGRAPH 2016のAdvances in Real-Time Rendering Courseで発表されています。
資料は下記からダウンロードできます。
https://advances.realtimerendering.com/s2016/index.html
もう8年前の資料なんですね。びっくり!
ディファードシェーディングすぐに肥大化します。
スキン,布,植物,メタル,髪など…をサポートする必要があります。すべてにライトタイプについて言及はしません。
マテリアル”ID”テクスチャを保存します。
– 実際のマテリアルIDではありません。単にシェーダの使用されるシェーダ機能のビットマスクです。
– 12bitを8bitへ圧縮(機能の相互排他性を考慮)
・各16×16タイルについて、タイル全体のマテリアルマスクを使用してルックアップテーブルにインデックスを付けます。
・ルックアップテーブルは事前に計算されています。タイル内のすべての機能をサポートする、可能な限りシンプルなシェーダーを保持します。
・アトミックにタイル座標を、そのシェーダーがライティングするタイルのリストにプッシュします。
・アトミック整数は dispatchIndirect 引数バッファのディスパッチカウントにもなります。
・既に大きな改善です。
・類似したテクニックは[1]で使用されています。
[1] SPU-Based Deferred Shading in Battlefield 3, http://www.dice.se/news/spu-based-deferred-shading-battlefield-3-playstation-3/
・タイル内のすべてのピクセルが同じマテリアルマスクを持つ場合に使用される、事前に計算されたもう1つのテーブル、「ブランチレス」のpermutationテーブルを作成します。
・クラス分けの際にその条件をチェックし、適切なテーブルを使用します。
・分岐をなくすだけでなく、グローバルなコンパイラ最適化の機会を開きます。
・最も悪い場合である高価なカット―シーンにおけるパフォーマンス改善
ー 4.0ms 最適化無し(“uber shader”)
ー 3.4ms (-15%) 最も良いシェーダを選択することによる
ー 2.7ms (-20%, -30% 全体的に) ブランチレスシェーダを使用することによる
・平均して、ブランチレス・シェーダーは、わずかなコストで、さらに10~20%の改善をもたらします。一方、最適なシェーダーを選ぶと、平均して20~30%の改善が得られます。
・基本性能に影響を与えることなく、マテリアルの複雑さやバリエーションを持たせることができます。
ー1つのシェーダー(例えばシルクシェーダー)に複雑さを加えても、ゲームの他の部分には影響しません。
・インターフェイスはクリーンかつ透過的に実装されています。
ー何度か繰り返した後
・ボーナス:分類コンピュートシェーダーは非同期コンピュートで実行され、ランタイムにはほとんど影響しません。
・システムをさらに進化させることができる。
ーライトタイプに基づいて、異なるコンピュートシェーダーをディスパッチすることもできる。少数派のライトタイプは、複雑さとコストの大部分を追加します。
・イテレーションは難しい
ー本当に1ビットの価値を学ぶ。
ー最終的には良いシステムに到達した。
・よりシンプルなものは常に良いです。わずかな性能向上のために、ある機能の犠牲を避けられたと思います。
該当スライドは以上です。
上記で述べられているように,タイルごとに必要なシェーダを分類分けを行います。
非同期コンピュートで実行し,処理時間を隠蔽します。
Uncharted 4では16×16ピクセルのタイルにして,分類分けを実行し,groupId.xを下位16ビット,group.yを上位16ビットとして32bitにパッキングし,バッファに格納します。
同時に,Shader Permutationごとにカウンタをアトミックにインクリメントしますし,dispatchのカウントバッファとして利用します。
こうすることで,必要な数だけコンピュートシェーダを起動することができます。
Grappling With Performance: Rendering Optimization Strategies In Rumbleverse
つづいて,”Grappling With Performance: Rendering Optimization Strategies In Rumbleverse”という資料で,GDC 2023で発表された資料です。
こちらは昨年なので,比較的に最近の資料ですね。
下記に資料がアップされています。
https://gdcvault.com/play/1028790/Grappling-with-Performance-Rendering-Optimization
こちらはライティングではなくReflectionとSubsurfaceが重いという話に焦点が当てられています。
・オリジナルアイデア:タイル分類を使用することで、リフレクションを適用する際の占有率を向上させます。
・Ramy EI Garawanyのプレゼンテーション:Deferred Lighting in Uncharted 4にインスパイアされています。
・アルゴリズム:
1. G-Buffer解析します。
2. マテリアルプロパティに基づいてタイルのリストを構築する。
3. 異なるshader permutations + DispatchIndirect を用いてそれぞれ描画します。
リフレクションに費やされる重い時間は、ここでも同じように最適化できると思いました。8×8のピクセルグループのGバッファプロパティを見て、存在するマテリアルに基づいてリストを構築するタイル分類シェーダを書きます。そして、各リストを DispatchIndirect を使って、各ディスパッチに異なるshader permutationsをバウンドしてレンダリングします。
例えば、このフレームでは、すべてデフォルトでライティングしているピクセルや、両面フォリッジでライティングしているピクセルをはっきりと見ることができます。
そしてここで、実際のタイル分類の視覚化を見ることができます。緑のタイルはデフォルトのライティング、青はすべてのフォリッジ、そして赤は「複雑」でフルシェーダーを実行するシェーディングパスを含んでいます。
しかし、この処理で最も重要だったのは、このタイルまでで、タイルは完全にカリングされ、実行時間に最も大きな影響を与えました。このことから、タイルの分類をSSR+SSSからのカリングワークロードに対しても使用し、分類を実行するコストをレンダリングの複数のステップで共有する方法について考えるようになりました。
最速のウェイブフロントは、決して起動しないウェイブフロントであることを忘れないでください!
すべてのパスからライティングのないタイルをカリングし、すべてのピクセルがSSRトレースをトリガーするには粗すぎるかどうかの分類を追加し、スキンマテリアルがないタイルのSSSをスキップし、クリアする必要がありますが,完全なSSSセットアップを必要としないタイルの簡略化クリアを実行します。
Tile Classifyシェーダーのコードで何が起こっているかを少し見てみましょう。分類は8×8タイルで行われますが、サブサーフェススキャッタリングは半分の解像度で行われるため、各グループは16×16のエリアをカバーします。UE4 の GetScreenSpaceDataUnit 関数で gbuffer プロパティをサンプリングした後、wave ops を使用して各 8×8 タイルのビットマスクをマージします。
コードでは、UE4シェーダーAPIコマンドの WaveAllBitOr と WaveAllBitAnd で起こっていることがわかります。これらのウェーブ操作の後、ウェーブフロントの各スレッドは MergedResult に同じマスク値を保持します。
ウェーブ操作を使用する利点の1つは、コンパイラがMergedResultがwave全体で均一であることを知っているため、waveコマンドに続くロジックがすべてスカラーALUになることです。
次に、ウェーブ全体にわたってMergedResultに保持されているビットに基づいてタイルのshader permutationが選択され、結果が最初のスレッドによって書き込まれますinterlocekdされた加算がカウントで発生し、タイル位置バッファにマップされるタイルの一意のインデックスを取得します。タイル位置バッファは、画面上の特定のタイルのピクセル位置を保持し、適用シェーダーで各タイルのピクセル位置を再構築するために使用されます。
なお、8×8タイルを選んだのは、GCNで1ウェーブフロント(64スレッド)のサイズだからです。アンチャーテッド4では16×16タイルを使用し、タイルリストに必要なメモリを25%削減した。タイルロケーションリストは、最大タイル数*permutation数に等しいメモリを必要とします。8×8タイルは、高価なマテリアルパスの境界をより厳しくすることができます。私が8×8を選んだのは、permutation数がより限られているからでもあります。たとえば、フォリッジを含むタイルのパスを追加してみたり、default litしているタイルのパスを追加してみたり。
現在、10個のシェーダーパーミュテーションがあり、その結果、1080の8×8タイルで1.296MBのタイルロケーションバッファになります。ハーフ解像度タイルリストに過剰に割り当てなければ48kbを取り戻すことができるはずですが、メモリのほとんどはSSR+Reflection Applyに使用される8つのpermuationから来ています。
uint bAnySSSProfile = 0; // loop over each 8x8 tile within the 16x16 pixel area uint2 PixelOffsets[4] = { uint2(0, 0), uint2(1, 0), uint2(0, 1), uint2(1, 1) }; UNROLL for(int i=0; i<4; ++i) { uint2 PixelPos = (DispatchThreadId.xy * 2 + ViewDimensions.xy); FScreenSpaceData ScreenSpaceData = GetScreenSpaceDataUint(PixelPos + (PixelOffsets[i] * 8)); FGBufferData InGBufferData = ScreenSpaceData.GBuffer; uint bIsDefaultLit = (InGBufferData.ShadingModelID == SHADINGMODELID_DEFAULT_LIT) ? 1 : 0; uint bIsFoliageLit = (InGBufferData.ShadingModelID == SHADINGMODELID_TWOSIDED_FOLIAGE) ? 1 : 0; uint bIsComplexLit = (InGBufferData.ShadingModelID > SHADINGMODELID_DEFAULT_LIT) ? 1 : 0; uint bIsSSSProfile = UseSubsurfaceProfile(InGBufferData.ShadingModelID) ? 1 : 0; float Roughness = InGBufferData.Roughness; float RoughnessFade = GetRoughnessFade(Roughness); uint bSkipSSR = (RoughnessFade <= 0.0 || InGBufferData.ShadingModelID == SHADIGNMODELID_UNLIT) && InGBufferData.ShadingModelID != SHADINGMODELID_CLEAR_COAT; // OR results uint MergedResult = (bIsSSSProfile << 2) | (bIsComplexList << 1) | bIsDefaultLit; MergedResult = WaveAllBitOr(MergedResult); uint bAnyDefaultLit = MergedResult & (1 << 0); uint bAnyComplexLit = MergedResult & (1 << 1); bAnySSSProfile = bAnySSSProfile | (MergedResult & (1 << 2)); // AND result. MergedResult = (bSkipSSR << 2) | (bIsFoliageList << 1) | bIsDefaultLit; MergedResult = WaveAllBitAnd(MergedResult); uint bAllDefaultLit = MergedResult & (1 << 0); uint bAllFoliageLit = MergedResult & (1 << 1); uint bAllSkipSSR = MergedResult & (1 << 2); // select which permutation uint PermutationIndex = NUM_PREMUTATIONS; if (bAllFliageList) { PermutationIndex = 4; } else if (bAllDefaultLit) { PermutationIndex = 6; } else if (bAllComplexLit) { PermutationIndex = 0; } else if (bAnyDefaultLit) { PermutationIndex = 2; } // odd half of permutations lacks SSR completely if (bAllSkipSSR) { PermutationIndex += 1; } // write out the 8x8 data // first thread does atomic increment and write, fully unlit tiles are skipped entirely if (GroupIndex == 0 && PermutationIndex < NUM_PERMUTATIONS) { uint TileIndex; InterlockedAdd(RWTileDispatchCounts[PermutationIndex * 3], 1, TileIndex); uint TileLocationID = TileIndex + (PermutationIndex * NumTiles); uint2 TileLocation = (GroupId * 2) + PixelOffsets[i]; RWTileLocationsBuffer[TileLocationID] = TileLocation.x | (TileLocation.y << 16); } // SSS permutations go beyond the end of the normal reflection tile permutations uint SSSPermutationIndex = NUM_PERMUATIONS + 1; if (bAnySSSProfile)
それは注目する価値があります – このシェーダの本当に素晴らしい点の1つは、GBufferプロパティをサンプリングするこれらの呼び出しがすべて1つのテクスチャ読み取りにマップされることです。Epicは便利なことに、すべての情報を1つのGBufferターゲットに既にパックしています。このターゲットには、ラフネスとマテリアルIDの両方が保持されています。
Razorに戻って、この分類シェーダーの実行コストを見てみましょう。ベースPS4で1080pの場合、0.18ミリ秒と控えめで、デカールがGBufferを変更し終わるとすぐに実行できます。
この分類処理は、非同期コンピュートを使用したシャドウ深度レンダリングと非常にうまく重なり、ここでは、マスクされたマテリアルのピクセルシェーダーウェイブが実行される前に、いくつかの頂点シェーディング処理と重なっているのがわかります。これはフレームに依存しますが、一般的に非同期で実行することでフレーム時間が約0.1ミリ秒短縮され、PS4では約0.08ミリ秒のコストになります。
適用ステップのパフォーマンスを確認する前に、環境ライティングの適用に適用するコンピュートシェーダだけを見てみましょう。これは元の実装のフルスクリーンピクセルシェーディングパスにすぎず、このコンピュートシェーダーパスは、さまざまなshader permutationを持つDispatchIndirectの繰り返し呼び出しを使用して実行されます。
シェーダは、GroupIdを使用してタイルロケーションバッファを検索し、GroupThreadIdに基づいて個々のピクセル位置にアンパックすることによって始まります。GBuffer が読み込まれた後、ShadingModelID が上書きされることで、オプティマイザがshader permuationに基づいて定義されたプリプロセッサマクロに基づいてデッドコードの除去を実行することができます。
// compute version of reflection and skylighting for dispatching tiles classified by shader featuress needed [numthreads(8, 8, 1)] void ReflectionEnvironmentSkyLightingCS( uint3 GroupId : SV_GroupId, uint3 DispatchThreadId : SV_DispatchThreadID, // DispatchThreadId = GroupId * int2(dimx, dimy) + GroupThreadId uint3 GroupThreadId : SV_GroupThreadID, // 0 ... THREADGROUP_SIZEX 0... THREADGROUP_SIZEY uint GroupIndex : SV_GroupIndex) // SV_GroupIndex = SV_GroupThreadID.z * dimx * dimy + SV_GroupThreadID.y * dimx + SV_GroupThreadId.x { // lookup into tile data with gorup ID uint TileLocationData = TileLocationBuffer[GroupId.x + TILE_PERMUTATION * NumTiles]; // unpack tile location uint2 PixelPos = 0; PixelPos.x = (TileLocationData & 0xFFFF) * 8 + GroupThreadId.x; PixelPos.y = (TileLocationData >> 16) * 8 + GroupThreadId.y; PixelPos += ViewDimensions.xy; float3 UVAndScreenPos; UVAndScreenPos.xy = (float2(PixelPos.xy + .5f) / (ViewDimensions.zw - ViewDimensions.xy); UVAndScreenPos.zw = float2(2.0f, -2.0f) * UVANdScreenPos.xy + float2(-1.0f, 1.0f); float4 SvPosition = float2(PixelPos.x, PixelPos.y, 0.f, 1.f); float2 BufferUV = UVAndScreenPos.xy; float2 ScreenPosition = UVAndScreenPos.zw; // Sample scene textures. FGBufferData GBuffer = GetGBufferDataFromSceneTextures(BufferUV); // Sample the ambient occlusion that is dynamically generated every frame. float AmbientOcclusion = AmbientOcclusionTexture.SampleLevel(AmbientOcclusionSampler, BufferUV, 0).r; // override GBuffer Data if all pixels have same type #if ALL_DEFAULT_LIGHTING GBuffer.ShadingModelID = SHADINGMODELID_DEFAULT_LIT; #elif ALL_FOLIAGE_LIGHTING GBuffer.ShadingModelID = SHADINGMODELID_TWOSIDED_FOLIAGE+ #elif !HAS_COMPLEX_LIGHTING // if no complex lighting pixels we can do this clamp as a hint that everything is either unlit or default lit GBuffer.ShadingModelID = clamp(SHADINGMODELID_UNLIT, SHADINGMODELID_DEFAULT_LIT, GBuffer.ShadingModelID); #endif
では、この0.08ミリ秒が、SSR、反射環境、SSSの適用において何を意味するのかを見ていく必要があります。これが、以前お見せしたシェーディングのオリジナルシーケンスです。
ここでは、Tiled Reflection適用シェーダーが1.08msで、0.13ms向上しています。このメリットの約半分は、このフレームでスカイピクセルをカリングしたことによるものなので、スカイピクセルのないフレームではあまり意味がありません。ここで私が指摘したい1つのマイクロ最適化は、最初のバリアの後に、占有率の低い遅いウェーブを最初に並べ、最後に最も速いウェーブを並べるということです。これは、占有率の低いウェーブが、より多くのレジスタが使用可能になるのを待っているためだと思います。また、稼働率の低いウェーブほど稼働時間が長くなる傾向があるため、最速のバッチを最後に置くことで、次のバリアまでに仕事がすぐになくなるようにしています。
TiledReflectionの適用がわずかな利益を得ているのに対し、Screen Space Reflectionsは逆に実に大きな利益を得ています。0.3ms向上していますが、これはDFAOの履歴更新でウェーブがうまく重なっているためで、実際には控えめな改善です。これは、ウェーブがDFAOの履歴更新とうまく重なるようになったためです。これらは別々のバッファに書き込まれ、両方とも反射の適用に送られるため、バリアは必要ありません。SSRを使ったこれらの結果は、これが価値ある最適化になるという確信を最初に与えてくれました。
そして反射を適用した後のサブサーフェスも、0.58msと大幅に改善されています。
ここでは、セットアップとタイルクリアが実にきれいに重なり、タイルクリアはフルセットアップシェーダーよりもはるかに短いウェーブを持っているのがわかる。ブラーステップは純正のタイル分類化と同様で、スキンのあるタイルだけが実行されるため、再結合は非常に高速です。
さて、ここまで説明したところで……このパスが以前はどうだったのか、もう一度思い出しましょう。
そして結果に戻りましょう。これらのパスにより、合計で~1msの節約になりますが、これらの利点はシーンの構図によって異なるため、分類コストを差し引くと、このショットでは合計0.92msになります。
おわりに
今回は,最適化ネタの一つしてタイル分類化の資料を紹介してみました。
ライティング・SSR・Subsurfaceあたりにも適用できるので,かなり最適化に効きそうです。実際にPS4で1ms程度の改善があるという実績があるのも良いですね。
最近だとUE5のNaniteによる描画とかでも使われていますよね。
実装自体は,Wave64モードにしてタイルサイズを8×8にしてWave組み込み命令を駆使するのが個人的には妥当な気がします。
次回は,実装方法について紹介できるといいなと思っています。
他にもいい資料をご存じの方は,是非コメント等でご紹介ください。
アルファテストの改良
こんばんみん。
Pocolです。
ネットで記事を漁っていたら,アルファテストの品質向上の手法についての記事を見つけました。
https://asawicki.info/articles/alpha_test.php5
以前ゲーム開発をしていた際に,キャラクタの髪の毛や動物の毛周りで困ることがあったので,ハッシュ化アルファテストなどを試してみたのですが,結構ちらつきがやっぱりきになっちゃうなーと思っていましたし,意外と計算量多いんですよね。もっと手軽でそれっぽい方法無いかなーって常々思っていたのですが,記事で紹介している手法はかなりシンプルなので,個人的には「これでよくね?」って感じています。
アルファ値を次のように変えるのと,事前準備としてPhotoShopなどでSolidifyフィルタを使用してテクスチャエッジ部分の色を引き延ばしてテクスチャを作成しておけばよいみたいです。
float alphaNew = max(alpha, (1.0/3/0) * alpha + (2.0/3.0) * threshold); if (alphaNew < threshold) discard;
Wave組み込み命令トリック
こんばんわ。
Pocolです。
Angry Tomato!さんという方が,“Compute shader wave intinsics tricks”
という記事を書いているので紹介です。
この記事では,以下のテクニックを紹介しています。
- Branch optimization
- Calculate on one lane, read on all
- Serialization of Writing Data
- Scalarization
- Multiple wave parallelization
- Indirect dispatch thread group count calculation
- Dividing the work between lanes
非常に面白い内容だと思うので,見ていない方は是非見るとよいでしょう。
WaveActiveLerp()について
こんちゃわ。Pocolです。
Wave組み込み命令の記事を漁っていたら,GithubにWaveActiveLerp()の実装を書いている人がいたので紹介しようと思います。
下記に説明の記事があります。
https://github.com/AlexSabourinDev/cranberry_blog/blob/master/WaveActiveLerp.md
実装は,https://github.com/AlexSabourinDev/cranberry_blog/blob/master/WaveActiveLerp_Shaders/WaveActiveLerp.hlslにあって,次のような感じみたいです。
uint WaveGetLastLaneIndex() { uint4 ballot = WaveActiveBallot(true); uint4 bits = firstbithigh(ballot); // Returns -1 (0xFFFFFFFF) if no bits set. // For reasons unclear to me, firstbithigh causes us to consider `bits` as a vector when compiling for RDNA // This then causes us to generate a waterfall loop later on in WaveReadLaneAt :( // Force scalarization here. See: https://godbolt.org/z/barT3rM3W bits = WaveReadLaneFirst(bits); bits = select(bits == 0xFFFFFFFF, 0, bits + uint4(0, 32, 64, 96)); return max(max(max(bits.x, bits.y), bits.z), bits.w); } float WaveReadLaneLast(float t) { uint lastLane = WaveGetLastLaneIndex(); return WaveReadLaneAt(t, lastLane); } // Interpolates as lerp(lerp(Lane2, Lane1, t1), Lane0, t0), etc // // NOTE: Values need to be sorted in order of last interpolant to first interpolant. // // As an example, say we have the loop: // for(int i = 0; i < 4; i++) // result = lerp(result, values[i], interpolations[i]); // // Lane0 should hold the last value, i.e. values[3]. NOT values[0]. // // WaveActiveLerp instead implements the loop as a reverse loop: // for(int i = 3; i >= 0; i--) // result = lerp(result, values[i], interpolations[i]); // // return.x == result of the wave's interpolation // return.y == product of all the wave's (1-t) for continued interpolation. float2 WaveActiveLerp(float value, float t) { // lerp(v1, v0, t0) = v1 * (1 - t0) + v0 * t0 // lerp(lerp(v2, v1, t1), v0, t0) // = (v2 * (1 - t1) + v1 * t1) * (1 - t0) + v0 * t0 // = v2 * (1 - t1) * (1 - t0) + v1 * t1 * (1 - t0) + v0 * t0 // We can then split the elements of our sum for each thread. // Lane0 = v0 * t0 // Lane1 = v1 * t1 * (1 - t0) // Lane2 = v2 * (1 - t1) * (1 - t0) // As you can see, each thread's (1 - tn) term is simply the product of the previous thread's terms. // We can achieve this result by using WavePrefixProduct float prefixProduct = WavePrefixProduct(1.0f - t); float laneValue = value * t * prefixProduct; float interpolation = WaveActiveSum(laneValue); // If you don't need this for a continued interpolation, you can simply remove this part. float postfixProduct = prefixProduct * (1.0f - t); float oneMinusT = WaveReadLaneLast(postfixProduct); return float2(interpolation, oneMinusT); }
いまのところで,使いどころがパッと浮かばないのですが,知っていればどこかで使えそうな気がしています。
…というわけで,WaveActiveLerp()の実装紹介でした。
WaveCompactValue()について
最近、最適化で忙しいPocolです。
皆さん、お元気でしょうか?
今日は,WaveCompactValue()を勉強しようかなと思いましたので,そのメモを残しておこうと思います。
この関数は,[Drobot 2017]で紹介された手法です。
スライドに掲載されている実装は下記のよう感じです。
uint WaveCompactValue( uint checkValue ) { ulong mask; // lane unique compaction mask for ( ; ; ) // Loop until all active lanes removed { uint firstValue = WaveReadFirstLane( checkValue ); mask = WaveBallot( firstValue == checkValue ); // mask is only updated for remaining active lanes if ( firstValue == checkValue ) break; // exclude all lanes with firstValue from next iteration } // At this point, each lane of mask should contain a bit mask of all other lanes with the same value. uint index = WavePrefixSum( mask ); // Note this is performed independently on a different mask for each lane. return index; }
これをHLSLに書き直すと次のような感じになるかとおもいます。
uint WaveCompactValue(uint checkValue) { // レーンのユニークなコンパクションマスク. uint4 mask; // すべてのアクティブレーンが取り除かれるまでループ. for (;;) { // アクティブレーンの最初の値を読み取る. uint firstValue = WaveReadLaneFirst(checkValue); // mask は残っているアクティブレーンに対してのみ更新される. mask = WaveActiveBallot(firstValue == checkValue); // firstValue を持つすべてのレーンを次のイテレーションから除外する。 if (firstValue == checkValue) break; } // この時点で、マスクの各レーンは、同じ値を持つ他のレーンのすべてのビットマスクを含んでいなければならない。 uint index = WavePrefixSum(mask); // これはレーンごとに異なるマスクで独立して行われる。 return index; }
さて,このWaveCompactValue()ですが,どういった使い道があるかというと,分類分けに使用することができます。
元々の[Drobot 2017]では色々なスレッドからAtomic操作をすると重くなるため,Atomic操作を減らす目的のために使われていました。
詳細な説明は,[Drobot 2017]のスライド51にアニメーション付きで載っていますので,そちらを参照してください。
軽く図の説明だけ載せておきます。
ちなみにグループ分けのよう番号を別途作りたい場合は,
uint2 WaveCompactValue(uint checkValue) { // レーンのユニークなコンパクションマスク. uint4 mask; // グループ分け番号. uint groupIndex = 0; // すべてのアクティブレーンが取り除かれるまでループ. for (uint i=0; ; ++i) { // アクティブレーンの最初の値を読み取る. uint firstValue = WaveReadLaneFirst(checkValue); // mask は残っているアクティブレーンに対してのみ更新される. mask = WaveActiveBallot(firstValue == checkValue); // グループ分け番号を更新. groupIndex = i; // firstValue を持つすべてのレーンを次のイテレーションから除外する。 if (firstValue == checkValue) break; } // この時点で、マスクの各レーンは、同じ値を持つ他のレーンのすべてのビットマスクを含んでいなければならない。 uint index = WavePrefixSum(mask); // これはレーンごとに異なるマスクで独立して行われる。 return uint2(index, groupIndex); }
のように実装すると良いみたいです。
WaveCompactValue()はタイルの分類分けやマテリアルの分類分けなんかの場面で有効活用できそうな気がしています。
…というわけで,WaveCompactValue()を使って分類分けすれば,無駄なAtomic操作を減らせるので,高速化できるよ!という話でした。
参考文献
・[Drobot 2017] Michal Drobot, “Improved Culling for Tiled and Clustered Rendering Call of Duty Infinite Warfare”, SIGGRAPH 2017 Advances in Real-time Rendering and Games course, https://advances.realtimerendering.com/s2017/index.html
CEDEC 2024 -Day3-
こんにちわ、Pocolです。
CEDEC最終日は以下の講演を聴講しました。
- 知る・創る・繋ぐ『ゼルダの伝説 ティアーズオブザキングダム』で再構築した開発環境とサウンド制作事例
- アーティストのためのカメラの仕組み講座
- 『ゼルダの伝説 ティアーズオブザキングダム』におけるフィールド制作とQA~トーレルーフの裏側で~
- VFXを物理的な数値を基準に作ろう「FINAL FANTASY XVI」開発環境
- ペルソナ3 リロードでの自動プレイの実装と運用
知る・創る・繋ぐ『ゼルダの伝説 ティアーズオブザキングダム』で再構築した開発環境とサウンド制作事例
非常に良い講演でした。NintendoWareとかだとツールが全部別れていて,しかも設計者・実装者がツールごとに違う。でも,ゲームを作るうえで連携したい!じゃ、どうやって解決したのか?…という感じの講演内容です。データベースを用意して,データベース経由で各ツールからメッシュやボーン・アニメーションといった感じの情報にアクセスして解決しているようです。
ここで問題になるのが,どこにデータベースを置くのか?ということですが,これについてもきちんと説明されていました。ローカル用とグローバル用の二つのデータベースを用意し解決しているようです。統合型環境ではない場合,非常に参考になる部分があると思うので,是非CEDIL等の資料をみることをお勧めします。
アーティストのためのカメラの仕組み講座
実演等があり,非常に分かりやすい内容でした。スマホのレンズの話から,焦点距離・画角の話,Aapature Value,ISO感度,自動露出,ホワイトバランス,一通りの説明がありました。
資料後半にカメラと同じ順番でポストプロセスを入れた方が良いという話があったので,エンジンがこれに準じた順番になっていない場合は,処理を見直した方がよさそうです。
『ゼルダの伝説 ティアーズオブザキングダム』におけるフィールド制作とQA~トーレルーフの裏側で~
トーレルーフが実はデバッグ用途の機能な施策を元に作られているという話でした。まずは,ボクセルを用いた周辺情報の収集の仕組みがあり,レイキャストを用いる場合と比べて10倍近く高速化したそうです。つづいて,QAエンジニア目線での話に移り,デバッグ機能の充実やテスターと開発者の隔たりを無くす施策などが語られました。次にアーティスト側でコリジョン抜けチェックや洞窟システムの話があり,これらのものを作ってトーレルーフが実装されているそうです。一見するとトーレルーフ要に特殊な仕組みで実装したんじゃないかと思いますが,実はありものの機能を使って実装しました。…という内容でした。でも,そもそもそこまでちゃんとした機能って普通のゲーム会社作れるところって少ないよな,自分たちの場合だったらどうすればいいんだろうか?そういう課題点も聞いていて感じました。任天堂は物凄く真っ当なことをされているなと感じました。
VFXを物理的な数値を基準に作ろう「FINAL FANTASY XVI」開発環境
特に参考になる部分はあんまりなかったですし,物理ベースレンダリングの話は結構出ていて,過去の文献をまったく調べていないじゃないか?というような知り合いとの話もありました。
逆に言うとそういう知識が無い人はある程度聞きやすいのかもしれません。
ペルソナ3 リロードでの自動プレイの実装と運用
SEGA テックブログで関連する記事があるそうです。非常に参考になる内容でした。ルールベースで移動させるとか,出来るだけゲーム側に手を入れないようにプラグインの形で実装するとか,実装する上で非常に参考になります。また対応にどれぐらいの工数がかかったかなども触れられており,QA作業工数の大幅現に成功したそうです。デバッグチェック用の工数削減に興味がある方は是非見てみると良いと思います。