プレイヤーに没入感のあるゲーム体験を提供するためには、スムーズなパフォーマンスが不可欠です。幅広いプラットフォームやデバイス向けにゲームのパフォーマンスをプロファイリングし、磨き上げることで、プレイヤーベースを拡大し、成功の可能性を高めることができます。
このページでは、ゲーム開発者向けの一般的なプロファイリングのワークフローの概要を説明します。電子書籍「Ultimate guide to profiling Unity games」から抜粋したもので、 無料でダウンロードできます。この電子書籍は、ゲーム開発、プロファイリング、最適化に関する社内外のUnity専門家によって作成されました。
プロファイリングで設定する有用な目標、CPUバウンドやGPUバウンドなどの一般的なパフォーマンスのボトルネック、そしてこれらの状況を特定し、より詳細に調査する方法について学ぶには、この先をお読みください。
ゲームのフレームレートをフレーム/秒(fps)で測定することは、プレイヤーに一貫した体験を提供する上で理想的ではありません。次のような単純化したシナリオを考えてみよう:
ランタイム中、ゲームは59フレームを0.75秒でレンダリングします。しかし、次のフレームのレンダリングには0.25秒かかる。平均60fpsというフレームレートは良さそうに聞こえるが、実際には最後のフレームのレンダリングに4分の1秒かかるため、プレイヤーはスタッター効果に気づくだろう。
これが、1枠あたりの時間予算を決めることが重要な理由のひとつである。これにより、プロファイリングやゲームの最適化を行う際に、確かな目標が得られ、最終的には、プレーヤーによりスムーズで一貫性のある体験を提供することができます。
各フレームには、目標とするfpsに基づいたタイムバジェットが設定される。30fpsをターゲットとするアプリケーションでは、1フレームにかかる時間は常に33.33ms(1000ms÷30fps)未満でなければなりません。同様に、60fpsを目標にすると、1フレームあたり16.66ms(1000ms÷60fps)となる。
例えば、UIメニューの表示やシーンのローディングなど、非インタラクティブなシーケンス中は、このバジェットを超えることができますが、ゲームプレイ中はできません。目標フレームバジェットを超えるフレームが1つでもあれば、ヒッチの原因となる。
注:VRゲームでは、プレイヤーに吐き気や不快感を与えないために、常に高いフレームレートが不可欠だ。これがないと、ゲームの認証時にプラットフォームホルダーから拒否される危険性があります。
フレーム/秒:欺瞞に満ちた指標
ゲーマーがパフォーマンスを測る一般的な方法は、フレームレート(1秒あたりのフレーム数)である。しかし、代わりにミリ秒単位のフレームタイムを使うことを推奨する。その理由を理解するには、上のフレーム時間に対するfpsのグラフを見てほしい。
この数字を考えてみよう:
1000 ms/秒 / 900 fps = 1.111 ms/フレーム
1000 ms/秒 / 450 fps = 2.222 ms/フレーム
1000 ms/秒 / 60 fps = 16.666 ms/フレーム
1000 ms/秒 / 56.25 fps = 17.777 ms/フレーム
アプリケーションが900fpsで動作している場合、1フレームあたりのフレームタイムは1.111ミリ秒となります。450fpsの場合、1フレームあたり2.222ミリ秒となる。これは、フレームレートが1/2になったように見えても、1フレームあたり1.111ミリ秒の差しかないことを意味する。
60fpsと56.25fpsの違いを見ると、それぞれ1フレームあたり16.666ミリ秒と17.777ミリ秒になる。これも1フレームあたり1.111ミリ秒の追加に相当するが、フレームレートの低下は割合的にははるかに少なく感じられる。
開発者がゲーム速度のベンチマークにfpsではなく平均フレームタイムを使うのはこのためだ。
目標フレームレートを下回らない限り、fpsを気にする必要はない。フレームタイムに着目し、ゲームの実行速度を測定し、フレーム予算内に収まるようにします。
詳しくは元記事「ロバート・ダンロップのfps対フレームタイム」をお読みください。
熱制御は、モバイル機器向けのアプリケーションを開発する際に最適化すべき最も重要な分野の一つである。非効率的な設計のためにCPUやGPUがフルスロットルで働く時間が長すぎると、それらのチップは熱くなる。チップの損傷(そしてプレーヤーの手を火傷させる可能性も!)を避けるため、オペレーティングシステムはデバイスのクロックスピードを下げて冷却させ、フレームがスタッタリングし、ユーザーエクスペリエンスが低下する。この性能低下はサーマルスロットリングとして知られている。
フレームレートが上がり、コード実行(またはDRAMアクセス操作)が増えると、バッテリーの消耗と発熱が増える。パフォーマンスが悪いと、低価格帯のモバイル・デバイスのセグメント全体が切り捨てられ、市場機会を逃すことになり、その結果、売上が減少する可能性もある。
サーマルの問題に取り組むときは、システム全体の予算として考えてください。
アーリープロファイリング技術を活用し、ゲームを最初から最適化することで、サーマルスロットリングやバッテリー消耗に対抗。熱やバッテリーの消耗の問題に対処するために、ターゲット・プラットフォームのハードウェアに合わせてプロジェクトの設定を調整します。
モバイルでのフレーム予算調整
長時間のプレイでデバイスの熱問題に対処するためには、フレームのアイドル時間を35%程度にするのが一般的です。これにより、モバイルチップが冷却される時間が得られ、バッテリーの過剰な消耗を防ぐことができる。フレームあたりの目標フレーム時間33.33ミリ秒(30fpsの場合)を使用すると、モバイル機器の一般的なフレームバジェットは、フレームあたり約22ミリ秒になります。
計算はこうだ:(1000 ms / 30) * 0.65 = 21.66 ms
同じ計算でモバイルで60fpsを達成するには、(1000ミリ秒÷60)×0.65=10.83ミリ秒の目標フレーム時間が必要になる。これは多くのモバイル機器では実現が難しく、30fpsを目標にするよりも2倍の速さでバッテリーを消耗することになる。こうした理由から、ほとんどのモバイルゲームは60fpsではなく30fpsをターゲットにしている。この設定を制御するには、Application.targetFrameRateを使用します。フレーム時間の詳細については、電子ブックの「フレーム予算を設定する」を参照してください。
モバイルチップの周波数スケーリングは、プロファイリング時にフレームのアイドル時間バジェット割り当てを特定するのを難しくします。あなたの改善や最適化は正味でプラスに働くかもしれないが、モバイル・デバイスは周波数が低下し、その結果、動作が低温になるかもしれない。FTraceや Perfettoなどのカスタム・ツールを使用して、最適化前後のモバイル・チップの周波数、アイドル時間、スケーリングを監視する。
目標fps(30fpsの場合は33.33ms)の総フレーム時間予算内に収まり、このフレームレートを維持するためにデバイスの動作が少なくなったり、温度が下がったりしていることが確認できれば、正しい方向に進んでいることになる。
モバイルデバイスのフレーム予算に余裕を持たせるもう一つの理由は、現実世界の温度変動を考慮するためだ。暑い日、モバイルデバイスは発熱し、放熱がうまくいかなくなり、サーマルスロットリングやゲームパフォーマンスの低下につながります。フレーム予算の何パーセントかを確保しておけば、こうした事態を避けることができる。
DRAMアクセスは通常、モバイル機器では電力を多く消費する操作である。モバイル機器のグラフィックス・コンテンツに対するArmの最適化アドバイスによると、LPDDR4のメモリ・アクセスには1バイトあたり約100ピコジュールのコストがかかるという。
フレームあたりのメモリアクセス操作の回数を減らす:
- フレームレートの低減
- 可能な限りディスプレイの解像度を下げる
- 頂点数と属性精度を抑えたシンプルなメッシュの使用
- テクスチャ圧縮とミップマッピングの使用
ArmまたはArm Maliハードウェアを使用するデバイスに焦点を当てる必要がある場合、Arm Mobile Studioツール(特に、Streamline Performance Analyzer)には、メモリ帯域幅の問題を特定するための優れたパフォーマンス・カウンターが含まれています。カウンターは、例えばMali-G78のように、Arm GPUの世代ごとにリストアップされ、説明されています。Mobile Studio GPUプロファイリングにはArm Maliが必要です。
ベンチマークのためのハードウェア・ティアの確立
プラットフォーム固有のプロファイリング・ツールを使用することに加え、サポートしたい各プラットフォームと品質の階層ごとに階層または最低仕様のデバイスを設定し、これらの仕様ごとにプロファイリングと性能の最適化を行う。
例として、モバイル・プラットフォームをターゲットにしている場合、ターゲットとするハードウェアに応じて機能のオン・オフを切り替える品質管理で3つの階層をサポートすることにするかもしれない。そして、各階層で最も低いデバイス仕様に最適化する。別の例として、PlayStation 4とPlayStation 5の両方でゲームを開発する場合、両方のプロファイルを確認してください。
モバイル最適化の完全ガイドについては、以下をご覧ください。 モバイルゲームのパフォーマンスを最適化する.この電子書籍には、ゲームを実行するモバイルデバイスのサーマルスロットリングを低減し、バッテリーの寿命を延ばすのに役立つヒントやトリックが多数掲載されています。
プロファイリングは、ディープ・プロファイリングを無効にして、上から下へのアプローチが効果的です。このハイレベルなアプローチを使ってデータを収集し、どのシナリオが不要なマネージド・アロケーションを引き起こすか、またはコア・ゲーム・ループ領域でCPU時間がかかりすぎるかについてメモを取る。
まずGC.Allocマーカーのコールスタックを集める必要がある。このプロセスに慣れていない場合は、次のセクションの「アプリケーションのライフタイムにわたって繰り返し発生するメモリ割り当ての場所を特定する」にヒントとトリックがあります。 Unityゲームのプロファイリング究極ガイド.
報告されたコールスタックが、割り当てやその他のスローダウンの原因を突き止めるのに十分なほど詳細でない場合、割り当ての原因を突き止めるために、ディーププロファイリングを有効にして2回目のプロファイリング・セッションを実行することができる。
フレームタイムの "違反者 "に関するメモを集める際には、他のフレームとの相対的な比較に注意すること。この相対的な影響は、ディープ・プロファイリングをオンにすることで影響を受ける。
ディープ・プロファイリングについては Unityゲームのプロファイリング究極ガイド.
プロフィール
プロジェクトの開発ライフサイクルの早い段階からプロファイリングを始めることで、プロファイリングから最高の利益を得ることができる。
早くから頻繁にプロファイリングを行い、あなたとあなたのチームがプロジェクトの「パフォーマンス・シグネチャー」を理解し、記憶する。パフォーマンスが急降下した場合、物事がうまくいかなくなるタイミングを簡単に察知し、問題を改善することができる。
最も正確なプロファイリング結果は、常にターゲット・デバイス上でビルドを実行しプロファイリングすることと、プラットフォーム固有のツールを活用して各プラットフォームのハードウェア特性を掘り下げることから得られる。この組み合わせにより、すべてのターゲット・デバイスにわたるアプリケーション・パフォーマンスの全体像を把握することができます。
この図表の印刷用PDF版はこちらからダウンロードできます。
プラットフォームによっては、アプリケーションがCPUバインドかGPUバインドかを判断するのは簡単です。例えば、XcodeからiOSゲームを実行すると、fpsパネルにCPUとGPUの合計時間が棒グラフで表示され、どちらが最も高いかがわかります。CPU時間には、VSyncの待ち時間も含まれる。VSyncはモバイル機器では常に有効になっている。
しかし、プラットフォームによっては、GPUのタイミングデータを取得するのが難しい場合があります。幸いなことに、Unity Profilerはパフォーマンスのボトルネックの場所を特定するのに十分な情報を示してくれる。上のフローチャートは、最初のプロファイリング・プロセスを示しており、それに続くセクションは、各ステップの詳細情報を提供しています。また、実際のUnityプロジェクトからのProfilerキャプチャを提示し、どのようなものを探すべきかを説明します。
GPUを待機しているときを含め、すべてのCPUアクティビティを全体的に把握するには、ProfilerのCPUモジュールにあるTimelineビューを使用します。キャプチャを正しく解釈するために、Profilerの一般的なマーカーについてよく理解してください。Profilerのマーカーは、ターゲットとするプラットフォームによって見え方が異なる場合があります。
プロジェクトの性能は、最も時間のかかるチップやスレッドによって制限される。それが、最適化に力を入れるべき分野だ。例えば、目標フレームタイムバジェットが33.33msで、VSyncが有効なゲームを想像してください:
- CPUのフレームタイム(VSyncを除く)が25ミリ秒、GPUのフレームタイムが20ミリ秒なら問題ありません!CPUに縛られているが、すべてが予算内であり、いろいろ最適化してもフレームレートは改善しない(CPUとGPUの両方を16.66ミリ秒以下にし、60fpsにジャンプアップしない限り)。
- もしCPUのフレームタイムが40ミリ秒、GPUのフレームタイムが20ミリ秒なら、CPUに縛られていることになり、CPUのパフォーマンスを最適化する必要があります。GPUの性能を最適化しても何の役にも立ちません。むしろ、例えば、C#コードの代わりにコンピュート・シェーダーを使うなどして、CPUの仕事の一部をGPUに移した方がバランスが取れるかもしれません。
- CPUのフレームタイムが20ミリ秒、GPUのフレームタイムが40ミリ秒なら、GPUに縛られていることになり、GPU作業を最適化する必要があります。
- CPUとGPUの両方が40msの場合、両方に拘束され、30fpsを達成するには両方を33.33ms以下に最適化する必要があります。
CPUまたはGPUに縛られることについて、さらに掘り下げたリソースをご覧ください:
開発期間中、早い段階から頻繁にプロジェクトのプロファイリングと最適化を行うことで、アプリケーションのすべてのCPUスレッドとGPU全体のフレーム時間がフレーム予算内に収まるようにすることができます。
上の画像は、継続的なプロファイリングと最適化を行ったチームが開発したUnityモバイルゲームのProfilerキャプチャ画像です。このゲームは、ハイスペック携帯電話では60fps、このキャプチャのような中・低スペック携帯電話では30fpsを目標としている。
選択したフレームのほぼ半分の時間が、黄色のWaitForTargetfps Profilerマーカーで占められていることに注目してください。アプリケーションはApplication.targetFrameRateを30fpsに設定し、VSyncを有効にしている。メインスレッドでの実際の処理作業は19ミリ秒あたりで終了し、残りの時間は次のフレームを開始する前に33.33ミリ秒の残りの時間が経過するのを待つのに費やされる。この時間はProfilerのマーカーで表示されるが、メインCPUスレッドはこの間基本的にアイドルであり、CPUを冷却し、バッテリーの電力を最小限に抑えることができる。
注意すべきマーカーは、他のプラットフォームやVSyncが無効になっている場合は異なるかもしれません。重要なのは、アプリケーションがVSyncを待機していることを示す何らかのマーカーと、他のスレッドにアイドル時間があるかどうかで、メインスレッドがフレームバジェット内で動いているか、フレームバジェットぴったりで動いているかをチェックすることだ。
アイドル時間は、灰色または黄色のProfilerマーカーで示される。上のスクリーンショットは、レンダースレッドがGfx.WaitForGfxCommandsFromMainThreadでアイドリングしていることを示しています。これは、あるフレームでGPUへの描画コールの送信が終了し、次のフレームでCPUからの描画コール要求を待っている時間を示しています。同様に、Job Worker 0スレッドはCanvas.GeometryJobで時間を過ごすが、ほとんどの時間はアイドルである。これらはすべて、予算内に収まるアプリケーションの兆候である。
あなたのゲームがフレーム予算内であれば
バッテリーの使用やサーマルスロットリングを考慮した予算への調整を含め、フレーム予算内に収まっていれば、次回までのパフォーマンスプロファイリングは終了です。アプリケーションがメモリバジェット内に収まっていることを確認するために、メモリプロファイラの実行を検討してください。
上の画像は、30fpsに必要なフレームバジェット(~22ミリ秒)内でゲームが快適に動作していることを示している。WaitForTargetfpsは、VSyncまでのメインスレッド時間と、レンダースレッドとワーカースレッドの灰色のアイドル時間をパディングしていることに注意してください。また、VBlankの間隔は、Gfx.Presentの終了時間をフレームごとに見ることで観察できる。
ゲームがCPUフレームバジェット内に収まっていない場合、次のステップはCPUのどの部分がボトルネックになっているか、言い換えれば、どのスレッドが最もビジーになっているかを調査することです。プロファイリングのポイントは、最適化のターゲットとなるボトルネックを特定することです。当て推量に頼ると、ボトルネックではない部分を最適化してしまい、結果的に全体的なパフォーマンスがほとんど向上しない可能性があります。最適化」の中には、ゲーム全体のパフォーマンスを悪化させるものさえあります。
CPUワークロード全体がボトルネックになることはまれだ。最近のCPUは、独立して同時に仕事をこなすことができる複数の異なるコアを備えている。各CPUコアで異なるスレッドを実行することができる。完全なUnityアプリケーションは、さまざまな目的のためにさまざまなスレッドを使用しますが、パフォーマンスの問題を見つけるために最も一般的なスレッドは次のとおりです:
- 本スレッドここは、デフォルトですべてのゲームロジック/スクリプトが作業を行う場所であり、物理、アニメーション、UI、レンダリングなどの機能やシステムに大半の時間が費やされる。
- レンダー・スレッドレンダリング処理中、メインスレッドはシーンを検査し、Cameraカリング、深度ソート、描画コールのバッチ処理を実行し、その結果、レンダリングすべきもののリストが生成される。このリストはレンダースレッドに渡され、レンダースレッドはUnityの内部的なプラットフォームに依存しない表現から、特定のプラットフォームのGPUに指示するために必要な特定のグラフィックスAPI呼び出しに変換します。
- ジョブ・ワーカーのスレッド:開発者は、C#ジョブシステムを使用して、特定の種類の作業をワーカースレッドで実行するようにスケジュールすることができます。Unityのシステムや機能の中には、物理、アニメーション、レンダリングなど、ジョブシステムを利用しているものもある。
本スレッド
上の図は、メイン・スレッドに束縛されたプロジェクトでの様子を示している。このプロジェクトは、通常13.88ミリ秒(72fps)、あるいは8.33ミリ秒(120fps)のフレームバジェットを目標とするMeta Quest 2で実行されている。しかし、このゲームが30fpsを目標にしていたとしても、このプロジェクトが問題を抱えているのは明らかだ。
レンダースレッドとワーカースレッドはフレーム予算内の例と同じように見えるが、メインスレッドはフレーム全体を通して明らかに忙しく働いている。フレーム終了時のわずかなProfilerオーバーヘッドを考慮しても、メインスレッドは45ミリ秒以上ビジー状態であり、このプロジェクトは22fps以下のフレームレートを達成していることになる。メインスレッドがのんびりとVSyncを待っていることを示すマーカーはない。
次の調査段階は、フレームの中で最も時間がかかる部分を特定し、その理由を理解することである。このフレームでは、PostLateUpdate.FinishFrameRenderingに16.23msかかり、フレーム全体の予算よりも多くかかっている。よく見ると、Inl_RenderCameraStackというマーカーのインスタンスが5つあり、これは5台のカメラがアクティブでシーンをレンダリングしていることを示している。Unityのすべてのカメラは、カリング、ソート、バッチ処理を含むレンダーパイプライン全体を呼び出すため、このプロジェクトで最も優先度の高いタスクは、アクティブなカメラの数を減らすことです。
BehaviourUpdateは、すべてのMonoBehaviour Update()メソッドを包含するマーカーで、7.27ミリ秒かかり、タイムラインのマゼンタの部分は、スクリプトが管理ヒープ・メモリを割り当てる場所を示している。Hierarchyビューに切り替え、検索バーにGC.Allocと入力してフィルタリングすると、このフレームでは、このメモリーの割り当てに約0.33ミリ秒かかっていることがわかる。しかし、これはメモリ割り当てがCPU性能に与える影響の不正確な測定である。
GC.Allocマーカーは、実際にはBeginポイントからEndポイントまでの時間を計測しているわけではない。オーバーヘッドを小さくするため、ビギンのタイムスタンプと割り当てのサイズだけが記録される。プロファイラーでは、目に見えるようにするために、最小限の時間を割り当てている。実際の割り当てには、特に新しいメモリ範囲をシステムに要求する必要がある場合、時間がかかることがある。ディープ・プロファイリングでは、タイムライン・ビューのマゼンタ色のGC.Allocサンプル間のギャップが、どれだけ時間がかかったかを示してくれる。
さらに、新しいメモリを割り当てることで、パフォーマンスへの悪影響が出ることがあるが、それを直接測定して帰結させるのは難しい:
- システムに新しいメモリを要求すると、モバイルデバイスのパワーバジェットに影響を与え、CPUやGPUの動作が遅くなる可能性がある。
- 新しいメモリは、CPUのL1キャッシュにロードされる必要があるため、既存のキャッシュラインを押し出すことになる。
- インクリメンタルまたはシンクロナス・ガベージ・コレクションは、マネージド・メモリの既存の空き容量が最終的に超過した場合に、直接または遅延を伴ってトリガーされる。
フレーム開始時、Physics.FixedUpdateの4つのインスタンスの合計は4.57msとなる。その後、LateBehaviourUpdate(MonoBehaviour.LateUpdate()の呼び出し)には4ミリ秒かかり、アニメーターには約1ミリ秒かかる。
このプロジェクトが望ましいフレーム予算とレートを達成するためには、これらのメインスレッドの問題をすべて調査し、適切な最適化を見つける必要がある。最大のパフォーマンス向上は、最も時間がかかるものを最適化することで得られる。
メインスレッドに束縛されたプロジェクトでは、次のような領域が最適化するのに有効です:
- Physics
- MonoBehaviourスクリプトの更新
- ゴミの割り当てと収集
- カメラのカリングとレンダリング
- 貧弱なドローコールのバッチ処理
- UIの更新、レイアウト、再構築
- アニメーション
調査したい問題によっては、他のツールも役に立つ:
- MonoBehaviourスクリプトに時間がかかるが、その原因がはっきりしない場合は、コードにProfiler Markersを追加するか、ディープ・プロファイリングでコール・スタックの全容を確認してください。
- マネージド・メモリを割り当てるスクリプトの場合、割り当てコール・スタックを有効にすると、割り当てがどこから来たかを正確に確認できます。また、ディープ・プロファイリングを有効にするか、Project Auditorを使用すると、メモリでフィルタリングされたコードの問題が表示され、マネージド・アロケーションが発生するすべてのコード行を特定することができます。
- フレームデバッガを使用して、描画コールのバッチ処理がうまくいかない原因を調べます。
ゲームを最適化するための包括的なヒントについては、これらのUnityエキスパートガイドを無料でダウンロードしてください:
- モバイルゲームのパフォーマンスを最適化する
- コンソールとPC用にゲーム・パフォーマンスを最適化する
上のスクリーンショットは、レンダースレッドによってバインドされたプロジェクトのものだ。これはアイソメトリック視点のコンソールゲームで、目標フレームバジェットは33.33ミリ秒。
Profilerのキャプチャは、Gfx.WaitForPresentOnGfxThreadmarkerが示すように、現在のフレームでレンダリングを開始する前に、メインスレッドがレンダリングスレッドを待機していることを示している。レンダースレッドはまだ前のフレームからの描画呼び出しコマンドを送信しており、メインスレッドからの新しい描画呼び出しを受け入れる準備ができていない。
現在のフレームに関連するマーカーと他のフレームのマーカーは、後者の方が濃く表示されるため、区別がつく。また、メインスレッドが処理を続行し、レンダースレッドが処理するための描画コールを発行し始めると、レンダースレッドは現在のフレームを処理するのに100ミリ秒以上かかり、次のフレームでもボトルネックになっていることがわかります。
さらに調査を進めると、このゲームには9つの異なるカメラと、シェーダーを置き換えることによる多くの余分なパスを含む、複雑なレンダリング設定があることがわかった。また、このゲームでは、130以上のポイントライトをフォワードレンダリングパスを使ってレンダリングしていた。これらの問題を合計すると、1フレームあたり3000回以上のドローコールが発生することになる。
以下は、レンダースレッドに拘束されるプロジェクトについて調査すべき一般的な原因である:
- 特にOpenGLやDirectX 11のような古いグラフィックスAPIでは、描画コールのバッチ処理が不十分。
- カメラが多すぎる。画面分割のマルチプレイヤーゲームを作るのでなければ、アクティブなカメラは1つだけにしておくべきです。
- 淘汰がうまくいかず、結果的に多くのものが描かれてしまった。カメラのフラストゥム寸法とレイヤーマスクのカルを調べる。オクルージョンカリングを有効にしてください。おそらく、オブジェクトがどのように世界に配置されているか知っていることに基づいて、独自の簡単なオクルージョンカリングシステムを作ることもできるだろう。シーンに影を落とすオブジェクトがどれだけあるか見てみよう。影のカリングは、"通常の "カリングとは別のパスで行われる。
レンダリングプロファイラーモジュールは、毎フレームの描画呼び出しバッチとSetPass呼び出しの数の概要を表示します。レンダースレッドがどの描画呼び出しバッチをGPUに発行しているかを調べる最良のツールは、フレームデバッガーです。
メインスレッドやレンダースレッド以外のCPUスレッドに束縛されるプロジェクトは、それほど一般的ではない。しかし、プロジェクトがデータ指向技術スタック(DOTS)を使用している場合、特にC#ジョブ・システムを使用してメイン・スレッドからワーカースレッドに作業を移動する場合、この問題が発生する可能性があります。
上のキャプチャは、エディターのプレイモードで、DOTSプロジェクトがCPU上で粒子流体シミュレーションを実行しているところです。
一見すると成功のように見える。ワーカースレッドにはBurstコンパイルされたジョブがぎっしり詰まっており、大量の仕事がメインスレッドから移動したことを示している。通常、これは正しい判断である。
しかしこの場合、メイン・スレッドのフレーム時間48.14ミリ秒と灰色のWaitForJobGroupIDマーカー35.57ミリ秒は、すべてがうまくいっていない兆候である。WaitForJobGroupIDは、メイン・スレッドがワーカースレッド上で非同期に実行するジョブをスケジューリングしているが、ワーカースレッドがジョブの実行を終える前に、それらのジョブの結果が必要であることを示している。WaitForJobGroupIDの下にある青いProfilerマーカーは、メインスレッドが待機中にジョブを実行していることを示しています。
ジョブはバーストコンパイルされているとはいえ、多くの仕事をこなしている。おそらく、どの粒子が互いに近いかを素早く見つけるためにこのプロジェクトで使用された空間クエリー構造は、より効率的な構造に最適化されるか、交換されるべきである。あるいは、空間クエリのジョブをフレームの開始時ではなく終了時にスケジュールし、次のフレームの開始時まで結果を必要としないようにすることもできる。おそらくこのプロジェクトは、あまりにも多くの粒子をシミュレートしようとしているのだろう。解決策を見つけるためには、ジョブのコードをさらに分析する必要があります。そのため、より細かいProfilerマーカーを追加することで、最も遅い部分を特定することができます。
あなたのプロジェクトのジョブは、この例ほど並列化されていないかもしれない。おそらく、1つのワーカースレッドで1つの長いジョブが実行されているだけだろう。ジョブがスケジュールされてから完了するまでの時間が、ジョブの実行に十分な長さである限り、これは問題ありません。そうでない場合は、上のスクリーンショットのように、メインスレッドがジョブの完了を待つ間にストールするのが見えるだろう。
同期ポイントやワーカースレッドのボトルネックの一般的な原因には、以下のようなものがある:
- Burstコンパイラでコンパイルされないジョブ
- 複数のワーカースレッドで並列化されるのではなく、単一のワーカースレッドで長時間実行されるジョブ
- ジョブがスケジュールされた時点と、結果が必要とされる時点の間に十分な時間がない。
- フレーム内に複数の「同期ポイント」があり、すべてのジョブが即座に完了する必要がある。
CPU Usage ProfilerモジュールのTimelineビューにあるFlow Events機能を使って、ジョブがいつスケジュールされ、その結果がいつメインスレッドに期待されるかを調査することができる。効率的なDOTSコードの書き方については DOTSベストプラクティスガイドをご覧ください。
メインスレッドがGfx.WaitForPresentOnGfxThreadなどのProfilerマーカーに多くの時間を費やし、レンダースレッドが同時にGfx.PresentFrameや<GraphicsAPIName>.WaitForLastPresentなどのマーカーを表示している場合、アプリケーションはGPUバウンドしています。
以下のキャプチャはサムスンのGalaxy S7で、VulkanグラフィックスAPIを使って撮影された。この例でGfx.PresentFrameに費やされている時間の一部は、VSyncの待ち時間に関連しているかもしれませんが、このProfilerマーカーの長さが極端であることから、この時間の大部分は、GPUが前のフレームのレンダリングを終了するのを待つのに費やされていることがわかります。
このゲームでは、特定のゲームプレイイベントがトリガーとなり、GPUがレンダリングする描画コール数が3倍になるシェーダーが使用された。GPU性能をプロファイリングする際に調査すべき一般的な問題には、以下のようなものがあります:
- アンビエントオクルージョンやブルームなどの一般的な原因を含む、高価なフルスクリーンポストプロセッシングエフェクト
- 高価なフラグメント・シェーダー
- 分岐ロジック
- 半精度ではなく完全な浮動小数点精度を使用
- GPUの波面占有率に影響するレジスタの過剰使用
- 非効率的なUI、パーティクルシステム、またはポストプロセッシングエフェクトが原因で発生する、透明レンダーキューでの過剰描画。
- 4Kディスプレイやモバイル機器のRetinaディスプレイなど、画面解像度が高すぎる。
- 高密度のメッシュ形状やLODの不足によって生じる微小トライアングル。これはモバイルGPUで特に問題となるが、PCやコンソールのGPUにも影響を与える可能性がある。
- 非圧縮テクスチャやミップマップのない高解像度テクスチャが原因で発生するキャッシュミスやGPUメモリ帯域幅の浪費
- ジオメトリまたはテセレーションシェーダ(ダイナミックシャドウが有効な場合、フレームごとに複数回実行されることがある
アプリケーションが GPU バウンドしているようであれば、GPU に送られる描画コールのバ ッチを理解する簡単な方法としてフレームデバッガを使うことができます。しかし、このツールは具体的なGPUのタイミング情報を提示することはできず、シーン全体がどのように構成されているかだけを示す。
GPUボトルネックの原因を調査する最善の方法は、適切なGPUプロファイラからGPUキャプチャを調べることです。どのツールを使うかは、ターゲット・ハードウェアと選択したグラフィックスAPIに依存する。