本記事は、CyberAgent Advent Calendar 2022 7日目の記事です。
0.はじめに
こんにちは。ゲーム・エンターテイメント事業部の清原(@nezumimusume)です。
ご存じの方もいらっしゃるかもですが、私は今年の8月末までゲーム系の専門学校で教鞭をとっており、教員時代に「HLSLシェーダーの魔導書」という書籍を出版いたしました。
現在はサイバーエージェントのゲーム・エンターテイメント事業部のコア技術本部(コアテク)でUnityのグラフィックエンジニアとして働いています。
この書籍を執筆している時に、RenderDocの内容も書きたいなぁ、と考えていたのですが力尽きてしまい、書籍に載せることはかないませんでした。
そこで、今回はこの場を借りて、RenderDocの使い方をステップバイステップで解説していきたいと思います。
なお、この記事は以下の環境での解説となりますが、Render Docの機能に関しては、どのプラットフォームでも共通となりますので、適時置き換えてご参照ください。
- Unity 2020.3.23f1
- URP 10.7.0
1.RenderDocとは?
RenderDocとはCGプログラミングのデバッグを強烈にサポートしてくれるオープンソースソフトウェアで、多くのプラットフォームで利用できます。
UnityやUnrealEngineも下記のように公式にRenderDocに対応しており、信頼性の高いツールであることが分かります。
RenderDocのインストール( Unreal Engine )
ちなみに、この手のツールはRenderDoc以外にもいくつか存在しており、下記のようなものがあります。
各種ツール、微妙に機能が違ったり、できること、できないことがあったりするので、用途に合わせて使い分ける必要があります。
例えば、DirectX Raytracingのデバッグをするのであれば、NVIDIA Nsight Graphicsを利用するとレイトレワールドの確認をする機能があり、とても便利です。
このように、いくつかのツールがあるのですが、今回はRenderDocに絞って解説します。
何ができるの?
RenderDocを利用することで、GPUに対する1フレーム分の描画コマンドをキャプチャできます。
GPUというのは、CPUから送られてきた描画コマンドを元に1フレームの絵を描画しています。
その描画コマンドをキャプチャすることで、1フレームの処理の流れを追いかけられるようになり、テクスチャ、モデルデータなどのリソースの不具合や、シェーダーの不具合、パイプラインステートの設定の間違いなどを見つけられます。
Frame Debuggerとの違いは?
さて、UnityにはビルトインのGPUデバッガとして「Frame Debugger」があります。
Frame DebuggerもRenderDocと同様に1フレームの描画コマンドキャプチャして、処理の流れを追いかけられます。
そのため、Frame Debuggerでいいんじゃないの?と思われた方もいるかと思いますが、RenderDocはFrame Debuggerよりも多くの機能を持っている多機能デバッガです。
簡単な処理の流れを追いかける程度のデバッグであれば、Frame Debuggerで十分ですが、もっと突っ込んだことを調べるには機能不足です。
Render Docを利用すると、下記のようなデバッグが行えます。
- 頂点シェーダーに流し込まれている頂点バッファ、インデックスバッファの確認
- 頂点シェーダーで座標変換後( クリップ空間 )の頂点データの確認
- 頂点シェーダーのステップ実行
- 各種パイプラインステートのさらに詳細な設定の確認
- ピクセルシェーダーのステップ実行
- 各種シェーダーのアセンブラコードの確認
- 定数バッファ、ストラクチャードバッファなどのリソースの確認
これらの要素は、すべて描画不具合やパフォーマンスの劣化を起こす可能性がある要素です。
しかし、これらの詳細な情報をFrame Debuggerから読み取ることはできません。
そこで、RenderDocを使用することで、より詳細なグラフィックデバッグ、チューニングを行えます。
2.Render Docのインストール~キャプチャまで
では、RenderDocをインストールしていきましょう。
RenderDocのインストーラーは下記のサイトからダウンロードできます。
インストールができたら、次はUnityエディタ上でRenderDocをロードします。
RenderDocのロードは、Game View か Scene Viewのタブを右クリックし、「Load RenderDoc」をクリックすることでロードできます。
ロードができると次の図のようにGameViewにカメラのアイコンが追加されます。
このアイコンをクリックするとRenderDocが立ち上がって、描画コマンドのキャプチャが行われます。
( ※ 2回目のキャプチャからはアイコンをクリックしても、RenderDocが最前面に来てくれませんが、ちゃんとキャプチャはされているので、タスクトレイなどからRenderDocを選んで最前面に移動させて確認してください )
RenderDocのlocalhostタブにキャプチャしたフレームの画像が表示されていたらキャプチャ成功です。
3.Event Browser
キャプチャをとれたので、ここから本格的にRenderDocを使って、デバッグしていきましょう。
デバッグするためには、デバッグしたいキャプチャを選ぶ必要があります。
次の図のように、デバッグしたいキャプチャを選択してダブルクリックしてください。
そうすると、左側のEventBrowserビューにズラズラと何かが表示されると思います。
Event Browserに記憶されているのは、描画コマンドをまとめたGPUイベントです。
このイベントが時系列順に並んでいます。
つまり、このフレームの最初のイベントはCapture Startというイベントで、以降は「WaitFoRender Jobs」⇒「CustomRenderTexture.Update」⇒「MeshSkinning.SkinOnGPU」と続いていきます。
さて、デバッグしたいのはGame Viewに描画されているゲームシーンのGPUイベントなのですが、このキャプチャはGame ViewとScene Viewをキャプチャしており、かつGame ViewやScene Viewを構成している各種UIを描画するためのGPUイベントも記憶しています。
そのため目的となるUnityのゲームシーンを描画しているGPUイベントを探す必要があります。
このイベントはUIR.DrawChainというイベントの下に入っています。
このイベントの子供を調べていくと、「UniversalRenderPipeline.RenderSingleCamera: Main Camera」というイベントが見つかります。さらに、このイベントの子供を調べていくと、「ScriptableRender.Execute: UniversalRender・・・・」というイベントが見つかります。このイベントがゲームシーンを描画しているイベントです。
Frame Debuggerを使ったことがある方は見覚えがあるイベントかもしれません。
実は、FrameDebuggerはこのイベントの情報を引っ張ってきていたわけです。
余談ですが、MeshSkinning.SkinOnGPUというイベントは、スキニングしているイベントで、ボーン情報を使って、スキンウェイト付きの頂点データを動かしているコンピュートシェーダーです。
一昔前は、モデルの頂点シェーダーでスキニングする実装が定番でしたが( Play Station 3くらいの時代 )、現在のモダンなレンダリングパイプラインでは、シャドウマップの描画、ZPrepass、G-Bufferへのレンダリングなど、同一モデルで頂点シェーダーが複数回呼ばれることが当たり前になったため、毎回の頂点シェーダーでアニメーション済み頂点座標を計算するコストが馬鹿にならなくなりました。
そこで、現在はコンピュートシェーダーやストリームアウトなどを使って、アニメーションによる座標変換を一回で終わるような工夫をしています。
4.Texture Viewer
さて、ゲームシーンを描画しているGPUイベントも見つかったので、さっそくゲームシーンの描画処理を追いかけていきましょう。
ゲームシーンの描画処理を追いかけていくときに、とても助けとなるのがTexture Viewerです。
Texture Viewerに表示されている出力テクスチャには、Event Browserで選択している、イベントが実行されたタイミングでの描画結果が表示されています。
次の動画はシャドウマップ生成処理を追いかけて調べている様子です。
このように、GPUイベントを選択することで、そのイベントが起きた時点での描画結果を確認できます。
また、この動画で選択しているGPUイベントはドローコールと呼ばれる非常にプリミティブで重要なイベントです。
描画結果を詳細にデバッグしたいときは、ドローコールのイベントを追いかけていくと良いでしょう。
使用されているテクスチャの確認
また、Texture Viewerは描画結果の確認だけではなく、そのGPUイベントで使用されたテクスチャも確認できます。
Texture ViewerのInputビューで使用されたテクスチャを確認できます。
Current Output以外の描画結果の確認
描画結果の出力先は一つだけではなく、複数指定もできます。
Texture ViewerのOutputビューを確認することで、他の出力先の確認もできます。
次の図から出力先として、カラーバッファと深度ステンシルバッファが指定されていることが分かります。
ピクセルの描画結果がおかしくなっているGPUイベントの特定
Texture Viewerを使えば、ピクセルの描画結果が意図しないものになっているGPUイベントを特定できます。このイベントを特定したら、該当するピクセルを選択してデバッグボタンを押すことで、このイベントで使用されたピクセルシェーダーをデバッグできます。
この機能の詳細については、後述する「ピクセルシェーダーデバッグ」の項目で解説します。
5.Mesh Viewer
Mesh Viewerでは、選択しているGPUイベントで使用された3Dモデルの情報を確認できます。
頂点シェーダーへの入力データの確認
Mesh ViewerのVS Inputビューでは頂点シェーダに入力されるデータを確認できます。
このデータを確認することで、モデルデータに不具合がないかを調査できます。
例えば、ライティングの結果が真っ黒になってしまうなどの不具合がある場合は、法線が0ベクトルになっている可能性があります。
このような不具合も、Mesh Viewerを活用すると、比較的容易に見つけられます。
頂点シェーダーからの出力データの確認
Mesh Viewerでは頂点シェーダーへの入力データだけではなく、頂点シェーダーからの出力データも確認できます。
入力データは正しいが、出力データが意図しないものになっている場合、頂点シェーダーに不具合が存在している可能性があります。
このように、入力データと出力データを確認することで、不具合が起きている可能性がある場所を絞り込めます。
入力データと出力データを視覚的に確認する
PreviewビューのVSInタブとVSOutタブを使えば、頂点シェーダーの入出力データを視覚的に確認もできます。
モデルの頂点座標しか確認できませんが、この機能を使えば、モデルの形状に破綻が起きているような描画不具合がアセットの問題なのか、頂点シェーダーの問題なのかをすぐに切り分けられます。
6.Pipeline State
Pipeline Stateでは各種パイプラインステージの設定、リソースなどを確認できます。
今回はその中でも使用頻度の高いパイプラインステージの情報の確認の仕方について解説していきます。
Input Assembler
Input Assemblerは処理するプリミティブの頂点データを頂点レイアウト、頂点バッファ・インデックスバッファ、プリミティブトポロジーなどの情報から準備して頂点シェーダーに渡すためのステージです。
このステージでは、これらのデータが正しく設定されているかを確認できます。
頂点レイアウトビューと頂点バッファ・インデックスバッファビューのGoボタンを押すと、各種リソースの確認ビューにジャンプできます。
Vertex Shader
Vertex Shaderでは頂点シェーダーで利用しているシェーダーリソース(テクスチャなど)、サンプラ、定数バッファなどの情報を確認できます。
Input Assemblerと同様にGoボタンを押すことで、各種リソースのビューにジャンプできます。
この中でも定数バッファのビューがデバッグ時に非常に役立ちます。
定数バッファはUnityで設定されているマテリアルパラメーターやTransformなどの情報をGPUに渡すために使われるものです。
このビューを確認すると、UnityでありがちなマテリアルパラメーターがGPUに反映されていないのでは?といった不具合を見つけられます。個人的にかなりお勧めの使える機能です。
Rasterizer
Rasterizerではカリングステートやフィルステートなどのラスタライザ設定や、ビューポート、シザリングの設定などの描画する範囲を表すパラメータを確認できます。
DirectX12のAPIを直叩きしてポストエフェクトなどを作っている時に、ビューポートやシザリングの設定が正しくできておらず、期待した範囲に絵が描画されていないなどの不具合をそこそこ起こしたことがあって、その時に大変お世話になった機能です。
UnityでもCommandBufferなどを用いて、ビューポートやシザリングを設定している場合はお世話になると思います。
Pixel Shader
Pixel Shaderの内容はVertex Shaderと全く同じで、シェーダーで利用しているシェーダーリソース(テクスチャなど)、サンプラ、定数バッファなどの情報を確認できます。
Goボタンを押すことで、各種リソースを確認できることも同じです。
Output Merger
最後にOutput Mergerです。Output Mergerはピクセルシェーダーからの出力結果を、出力先にマージするためのブレンドステート、Zテスト、ステンシルテストなどの設定、出力先になっているレンダリングターゲットの確認ができます。
アルファブレンドステートやZテストのステートで描画不具合が起きるのは、CGプログラミングあるあるなので、非常に使える機能です。
7.頂点シェーダーデバッグ
Render DocにはVisual StudioやRiderのようなIDEのデバッガが持っている、ステップ実行、ブレイクポイント、ウォッチのような機能を持っているシェーダーデバッガーが用意されています。
頂点シェーダーのデバッグは、先ほど紹介したMesh Viewerを使って、デバッグしたい頂点を選択して、右クリックメニューから「Debug this vertex」を選択することで行えます。
デバッガが起動すると次の図のように、頂点シェーダーをデバッグできます。
デバッガの操作はVisualStudioライクなものになっているので、VisualStudioの操作に慣れている方であれば、容易に扱えると思います。
注:ソースコードレベルでデバッグする場合は、デバッグしたいソースコードに下記の#pragmaを追加する必要があります。
#pragma enable_d3d11_debug_symbols
これが定義されていないと、アセンブラでしかデバッグができないので注意してください。
8.ピクセルシェーダーデバッグ
Texture Viewerを使えば、ピクセルシェーダーもデバッグできます。
デバッグしたいピクセルを右クリックで選択して、Pixel ContextからDebugボタンを押すだけです。
なお、選択しているGPUイベントがそのピクセルを描画していない場合は、そのピクセルを描画したGPUイベントの履歴が表示されます。
その場合は、履歴からデバッグしたGPUイベントを選択してデバッグを開始してください。
ピクセルシェーダーのデバッガが起動すると次の図のような画面になります。こちらの操作方法も頂点シェーダーのデバッガーと同様に、VisualStudioライクな操作になっています。
9.シェーダー最適化
最後にシェーダー最適化のために、私が行っているRenderDocの活用方法を紹介します。
コンパイル後のシェーダープログラムの確認
シェーダー開発あるあるとして、#ifdefによる条件コンパイルがどんどん増えていくというものがあります。
条件コンパイルが増えていくのは、動的条件分岐によるコストが高いというシェーダープログラムの事情があるため、どうしても仕方ない部分があるのですが、条件コンパイルがどんどん増えていくにしたがって、最終的に動いているシェーダーコードの処理が分かりにくくなってしまいます。
そこで、RenderDocのシェーダーデバッガを活用することで、最終的に動いているコードを確認できます。
Unityのシェーダーコードはあくまで中間コードです。この中間コードを元にUnityが各プラットフォームで動くシェーダーコードを生成しています。
RenderDocのシェーダーデバッガで表示されているコードは最終的なものになっているため、無駄なコードは削除されています。
このコードを確認することによって、意図していない処理が残っていないかなどを確認できます。
これは処理だけではなく、例えば次のような頂点シェーダーからの出力データなども該当します。
頂点シェーダーからの出力に無駄なデータが含まれていると、ラスタライザの処理負荷が増加してしまうため、こちらもRenderDocを活用して、きちんと確認するようにしています。
アセンブラコードの確認
最後にアセンブラコードの確認です。
これは先ほどの最終コードの確認から、さらに先に進んだ話ですが、RenderDocでは最終的に動いているシェーダーのアセンブラコードまで確認できます。
このアセンブラコードを確認することで、最終的にどのようなコードが実行されているのかをさらに深く確認できます。
最適化は非常に複雑な要素が絡んでくるため、一概にこれだ!というものはないのですが、シンプルに処理ステップ数が増大になってくると注意が必要です。
最後に
専門学校での教員時代に「GPUデバッガの使い方を学生に教えてほしい」、「GPUデバッガが使える人材が欲しい」といったお声をそこそこ頂いていたので、今回このような記事を書かせていただきました。
今回の記事が少しでも皆様のお役に立てば幸いです。