25/09/21 up
ちょっと前に、ある人とプロファイルの話をしていて、やはりプロファイラは誰でも気軽に使ってみるのがいいよねというような話になりまして。
で、そういう場合には今ならPIXがいいという話をさせていただきまして。
というわけで今回はPIXの基本的な使い方、及びPIXでキャプチャする機能をアプリに組み込む話をしようと思います。
今回はPIXを利用しますが、GPUプロファイラーは他にもいくつか存在します。
コンソールゲーム機の場合、各プラットフォームベンダーが提供してくれているプロファイラーがあるので、そちらを利用することになります。
PCではPIX以外にも、RenderDoc、各GPUハードメーカーが提供するプロファイラーがあります。
RenderDocは個人で作成しているもので、GPUデバッグには都合の良いツールです。
ただ、パフォーマンスを調べるという点についてはあまり参考にならないため、描画がおかしい場合のデバッグに利用することがほとんどです。
複数OSに対応し、APIとしてもDirectXからOpenGL/Vulkanまで幅広く対応しているのが大きな特徴です。
ハードメーカーが提供しているものとしては、NVIDIAのNsight Graphicsや、AMDのRadeon GPU Analyzerなどがあります。
Nsight Graphicsは大体の機能が1つのツールに集約されていますが、AMDは複数のツールに分かれていて、GPU AnalyzerにGPU Profilerに…という感じになっています。
これらは基本的に特定ハード用ですが、APIについては複数対応している場合がほとんどです。
で、今回紹介するPIXですが、こちらはMicrosoftが提供するプロファイラーです。
もともとはXbox用で、これをWindows用にしたものがPIX on Windowsです。
対応OSはWindowsのみ、APIについてもD3D12のみという感じで、対応幅は残念ながら狭いです。
しかし、使いやすさや安定性という点ではかなりよく、ハードを問わず詳細なプロファイルが可能です。
数年前はだいぶ使いづらかったんですが、今はだいぶ使いやすいので、対応OS、APIが合うならオススメできます。
PIXを利用するための準備をしましょう。
まずはPIXをインストールします。以下のサイトからダウンロードし、インストールしてください。
https://devblogs.microsoft.com/pix/download/
次に自分のプログラムでWinPixEventRuntimeを使えるようにします。
最も簡単なのはNuGetを使う方法です。Visual StudioやRiderを使って開発しているならこれが一番手っ取り早いですね。
PIXを利用するだけならこれでもOKですが、アプリからキャプチャを行う場合はAPIを利用します。
NuGetでWinPixEventRuntimeを使えるようにしていると、pix3.hを利用することが出来ます。まずはこちらをインクルードします。
次に以下の命令を呼び出してWinPixEventRuntimeのロードを行います。
あとはボタンを押すなどしたときにキャプチャを行えばOKです。
キャプチャには2つの方法があります。
マテリアル単位でGBufferへの変換を行うとすると、同一マテリアルのピクセルは連続したバッファ領域で提供される必要があります。
この時、マテリアル数×ピクセル数のバッファを取得していしまうと、マテリアル数が多くなった場合や画面のピクセル数が高解像度になった場合にアホみたいなサイズのバッファを取得してしまいます。
Prefix Sumを計算できると、バッファはピクセル数分だけ確保すれば良く、Prefix Sumからそのバッファの先頭アドレスを、各マテリアルの総数からバッファサイズを求めることができるというわけです。
そんなPrefix Sumを求めるプログラムは簡単です。
C++で実装するなら以下のようなコードで実現できるでしょう。
大体の場合、PIXGpuCaptureNextFrames() 関数が簡単です。
名前の通り、呼び出しを行った次のフレームをキャプチャします。
PIXBeginCapture2() ~ PIXEndCapture() は指定区間をキャプチャします。
この命令を使った場合、この区間で実行されたGPU命令をキャプチャします。
実行されたというのは本当にそのままGPUが実行した命令なので、この部分だけキャプチャしたい!という場合、開始前のGPUコマンドの終了を待ってからキャプチャを開始し、キャプチャしたい命令が完了するのを待ってからキャプチャ終了を行う必要があります。
適当な場所で実行してしまうと1フレーム分のキャプチャも正常に確保できない場合がほとんどです。
ですので、特定の位置をキャプチャすることを求めるわけでないのであれば前述の PIXGPUCaptureNextFrames() を使っておくのが無難です。
これらの命令でキャプチャを行うと、結果がファイルとして指定のパスに保存されます。
これをダブルクリックすればPIXが立ち上がり、キャプチャ結果を見ることが出来ます。
UE5も-attachpixを起動時引数に追加することでPIXキャプチャができるようになります。
このように、アプリにキャプチャ機能がついている場合はそれを利用するのが簡単です。
アプリがAPIを使ったキャプチャに対応していない場合は、PIXからアプリを実行してキャプチャします。
キャプチャ結果を開いた直後はこのような状態になっているはずです。
この状態では命令や一部リソースを確認することしか出来ません。
例えば途中の描画結果などは取得できません。
そのためにAnalysisを実行する必要があります。
実行するにはタイミングやリソースビューを行おうとした際に表示される文字のクリック可能な部分(例えば、"Click here to start analysis"と書かれた文字列の"Click here"部分)をクリックするか、もしくはF5キーを押します。
タイミングキャプチャを行うにはOverviewタブの下半分にあるタイミングペインでクリックする必要があります。(上図の下半分)
Analysisを実行し、タイミングキャプチャを行うと以下のような画面になります。
タイミングの上部には各イベントの名前が表示され、マウスオーバーで各イベントの時間を見ることも出来ます。
また、キュー間での矢印はコマンドリストの依存を示しています。
このプログラムではSSAO PassがShadowDepthPassと並列で実行されているのですが、PIXでは並列実行されているように見えません。
Nsight Graphicsでは並列動作しているようになっているので、この辺はPIXの弱いところかもしれませんね。
とはいえ、各イベントのパフォーマンスを調べるのにはあまり不都合はないです。
棒グラフ状に表示されているのはWave Occupancyで、PS/CS/VGTの占有率を見ることが出来ます。
マウスオーバーすればその時間の占有率が細かく見れます。
その下には各GPUメーカーごとの情報を見ることが出来ます。
今回はGeforceを使っているので、NVIDIA関連の情報を取得しています。
各ユニットのThroughputや、L1TEX/L2のキャッシュヒット率などを調べることが出来ます。
これらはほとんどが折れ線グラフで表示されていて、正直見づらいとは思っていますが、マウスオーバーでハイライトできたり、不要な項目を非表示することもできるので、まあなんとかといった感じでしょうか。
Pipelineタブでは各Draw/Dispatch命令時のリソースや描画結果をチェックできます。
テクスチャは左にあるヒストグラムを調整することも出来ます。
深度バッファなどはヒストグラムを調整しないと何も描画されていないように見えたりもします。
バッファリソースはBuffer Formatを変更することで内部のデータをわかりやすくチェックできます。
メッシュについてはIAのOutputやVS/MSのOutputを利用します。
IAのOutputは行列変換する前のメッシュで、VS/MSのOutputはスクリーン変換された結果を表示します。
注意点として、MSの場合はIAが存在せず、行列変換前のメッシュを表示することは出来ません。
これについては他のプロファイラーでも同様です。
そして、大量のメッシュを一度に描画していると、VS/MSのOutputをチェックしようとしてPIXがクラッシュすることがあります。
特にこのプログラムでは、MSによりメッシュ描画が1DrawCallで大量に行われており、非常にクラッシュしやすくなっています。
テクスチャやバッファがおかしな結果を出している場合、どこでおかしくなっているか調べるにはヒストリーを調査する必要があります。
この場合、各リソースの情報からヒストリーをたどることが出来ます。
リソースビューの上部に情報を表示するボタン(赤枠内)があります。
多分デフォルトでは無効になっているので、このボタンを押してビューの右側に情報を表示します。
ここでIDをクリックすると右上のResourcesペインに移動し、選択された状態になります。
Resourcesペインでリソースが選択されていると、その右側にヒストリーが表示されます。
どのイベントで、どのステージで、どのビューにバインドされているかを確認することが出来ます。
このヒストリーから書き込みされているイベントを探し、そこからさらに別のバッファをたどるなどが可能です。
RTV/DSV/UAVの画像に対しては各ピクセルへの書き込みイベントを調べることも出来ます。
リソースビューの画像からピクセルを選択し、Pixel Histroryボタン(赤枠)を押すと右側にヒストリーが表示されます。
現在選択中のイベントだけでなく、その前後でのピクセル書き込みイベントが表示されます。
よくあるピクセルにNaNが入って困る場合にはこのピクセルヒストリーを利用してデバッグできます。
デバッグ用にシェーダのPDBファイルが出力されている場合、シェーダのデバッグも可能です。
PIXの設定で”PDB Search Paths”が存在するので、PDBを保存しているパスを追加します。
.wpixファイルで使用されているシェーダとPDBが一致するのであれば、各Draw/Dispatch命令でシェーダのコードをチェックすることが出来ます。
デバッグしたいシェーダをイベントのShaderから選択、再生ボタンを押すとDebugタブに移動してそのシェーダをデバッグすることが出来ます。
パラメータとして頂点IDやピクセル位置、スレッドIDなどを指定してデバッグできます。
どの程度まで正確なデバッグができるのかはあまり使ったことがないので不明ですが、最低限の調査は行えるはずです。
また、シェーダを書き換えてF6キーを押すと、変更したシェーダでイベントを実行し直すことも可能です。
ちょっとしたデバッグで使用できるだけでなく、タイムラインにも影響を与えます。
どのコードがどの程度パフォーマンスに影響を与えているかを確認することが出来て便利です。
UE5の場合はデフォルトでPDBは出力されないので、デバッグが必要な場合はPDB出力を行う必要があります。
これは結構時間が掛かるしファイルサイズも大きくなるので、どうしてもデバッグしなければという人以外はまずやらないとは思います。
今回紹介したPIXの機能についてはPIXのみの機能というわけではなく、各プロファイラーでもそれぞれ実装されている機能が多いです。
名前や操作方法はもちろんツールによって違いはありますが、だいたいこれらの機能は含まれていると思います。
PIXだけが素晴らしいというわけでもないので、やりたいことによってはNsightやRenderDocを使うのも良いでしょう。
いろんなツールを使ってみて、自分の使い方に合ったツールを選択することが重要です。
ここから最適化を行っていこうと思うと更に多くの知識が必要になります。
そこまで行くとグラフィクスエンジニアに頼んだほうが良いのですが、パフォーマンスが明らかにおかしい部分があるかどうかを調べることはグラフィクスエンジニア以外でも可能です。
作っているゲームを試遊するときなどにちょっとキャプチャしてみるのも面白いと思いますので、是非試してみてください。