この記事は Unity Advent Calendar 2023 の17日目の記事です。
はじめに
モバイルプラットフォーム (iOS / Android) では Unity 標準の GPU Usage Profiler module のサポートには次のような制限があり、GPU 負荷計測を行うにはサードパーティー製のプロファイリングツールを使う必要が出てくるかと思います。
Platform | GraphicsAPI | Status |
---|---|---|
Android | OpenGL | Supported on devices running NVIDIA or Intel GPUs. |
Vulkan | Not supported | |
iOS | Metal | Not supported. Use XCode’s GPU Frame Debugger UI instead. |
サードパーティー製のツールとしては次のようなものが挙げられます。
- iOS
- Xcode
- Android
- Android GPU Inspector (AGI)
- その他、GPU ベンダーが提供するツールなど
- 詳細は Profiling tools を参照
これらのツールを用いれば GPU 負荷計測を行うことが出来るようになるかと思われますが 1、エンジニア以外 (更に言うとツールの有識者以外) が扱うには敷居が高い上に、動作させる際にもツールに接続する必要が生じるなどと言った手間が発生します。
そこで今回はこれらの手間を無くすためにアプリ上から手軽に GPU 負荷計測を行う方法について幾つか取り上げていきたいと思います。
あくまで調査の補助ツールとしての活用を想定
これから話す内容については、「アプリ上から手軽に負荷計測を行えるようにすることで、ボトルネックとなる部分の調査を行いやすくする」ことを目的としてます。
というのも、今回紹介する機能では得られるデータに限りがあるために、「CPU or GPU のどちらかがボトルネック」と言った大雑把な情報までは得られるものの、「実際にその中の何が重いのか? (CPU ならどのコードが重いのか? GPU ならどの描画が重いのか?)」と言った詳細情報まで得るのは難しいかもしれません。
もし詳細まで調べるとなると、それこそ他のツール郡 ( UnityProfiler や 上記のサードパーティー製ツール郡など) も合わせて活用する必要が出てくるかと思います。
そのため、今回紹介する機能は「ボトルネックを特定するための補助ツール」的な立ち位置で活用し、そこから更に深く調べていく際には他のツール郡と合わせて活用していく必要があるかと思います。
環境
- 環境
- Unity 2022.3.13f1
- Xcode 15.0.1 ~ 15.1.0
- プラットフォーム (カッコ内は対応する GraphicsAPI)
- iOS (Metal)
- Android (Vulkan, OpenGL ES 3.x) 2
FrameTimingManager
FrameTimingManager とはフレーム全体の CPU / GPU 時間の合計など、フレームレベルでの時間計測を行うことができる機能です。
具体的に言うと 1フレーム中における以下の要素を計測することが可能な API であり、GPU 負荷に該当する gpuFrameTime
の取得以外にも cpuMainThreadFrameTime
や cpuRenderThreadFrameTime
と言った CPU 負荷に関する内訳も取得することが可能です。
-
cpuFrameTime
- CPU の総フレーム時間を指し、メインスレッドでのフレーム開始から次フレームまでの時間として計算される
-
cpuMainThreadFrameTime
- メインスレッドの作業時間、つまりフレームの開始からメインスレッドが仕事を終えるまでの時間の合計を指す
-
cpuRenderThreadFrameTime
- レンダースレッドの作業時間、つまりレンダースレッドに送信された最初の作業要求から
Present()
関数が呼び出されるまでの時間の合計を指す
- レンダースレッドの作業時間、つまりレンダースレッドに送信された最初の作業要求から
-
cpuMainThreadPresentWaitTime
- フレーム中で CPU が
Present()
の完了を待っている時間
- フレーム中で CPU が
-
gpuFrameTime
- GPU の作業時間、つまり GPU に作業が送信された時刻から GPU が作業を終了したことを示す信号が出る時刻までの時間の合計を指す
そのため、用途としては今回のテーマでもある「GPU 負荷計測」に限らず、アプリ全体の負荷計測やボトルネックの調査などに活用していけるかと思います。
詳細情報や使い方などについては公式ドキュメントの他、以下の公式ブログ記事が参考になります。
ブログ中でも触れられてますが、FrameTimingManager
は 2022.1 から大幅にアップデートされ、対応プラットフォームについてもサポートが強化されました。
そのため、このバージョン以前に書かれた記事などでは対応プラットフォームが限られている旨について触れられている可能性がありますが、今回紹介するバージョンでは iOS / Android ともに一通り動作する物となっています。
(ただし、記載時点だと Android の OpenGL ES 3.x 系統で不具合あり...詳細は後述します)
リリースビルドでも有効にすることが可能
特徴の 1つとして、FrameTimingManager
はリリースビルド設定でも動作可能な点が挙げられます。
ブログなどを参照するにこちらの機能は特殊なタスクとして設計されており、実行時のオーバーヘッドが非常に低いと言った特徴があるので、例えばリリースビルドでテストすることで製品版相当の環境下での性能分析を行うことが出来そうです。
他にも計測値自体はコード上から取得することが可能なので、ブログにもある通りリリースモードでのパフォーマンスレポートの作成と言った用途などにも使えるかと思います。
URP / HDRP では標準で表示機能が組み込まれている
FrameTimingManager
自体は API なので、計測値をアプリ上から確認するには表示するための UI を実装する必要が出てきます。
ただ、URP / HDRP には Rendering Debugger > Display Stats と言う機能が標準で入っており 3、こちらを表示することでアプリ / Editor 上から計測値を表示することが可能です。
(ただし、こちらを使うには Development Build
を有効にする必要があります)
試しに URP のサンプルをビルドして、実機上で表示させた場合には次のようになります。
何れにせよ、リリースビルド相当で表示する際や Built-in RenderPipeline
を用いる場合には自前で実装する必要が出てきます。
注意点 / 制限事項
公式ドキュメントやブログにも制限事項として記載されてますが、こちらでもいくつかピックアップします。
-
- 返すフレームは現在のフレームではなく、4フレーム分遅れた結果が返ってくる
-
-
GPU FrameTime
は全てのフレームで使用可能であることが保証されておらず、正確な値を返せない場合がある
-
-
- Metal のような TBDR (Tile-based Deferred Rendering) の環境下では
GPU FrameTime
の精度が落ちる場合がある
- Metal のような TBDR (Tile-based Deferred Rendering) の環境下では
1 についてはフレームの終了時点では直ぐに結果を得ることが出来ないので、同期を取るための仕様になるかと思います。
3 については手元でサンプルシーンなどを動かして計測してみた分には Xcode 上での計測結果とそこまで差は見受けられなかったものの、場合によっては計測結果に差が発生すると言った現象も確認できたので、これらの仕様を踏まえた上で参考値として活用するのが良いのかと思われます。
◇ Android OpenGL ES 3.x 系統だと GPU FrameTime
が正しく計測できない?
こちらは不具合になるかと思われますが、Android で GraphicsAPI
を OpenGL ES 3.x
にすると GPU FrameTime
が正確に計測できていない?と思われる現象を確認しました。
具体的に言うと今回検証したバージョンである Unity 2022.3.13f1 では次のような現象を手元で確認できています。
-
47404688.459 ms
と言った明らかな不正値が返ってくる - どのような状況でも一定の数値から変動しない
- 例えば Screen.SetResolution を用いて解像度を極端に上げようが下げようが、常に一定した数値が返り続ける
こちらは Metal 及び Vulkan だと再現できなかった不具合となるので、恐らくは OpenGL ES 3.x
起因の不具合になるかと思われます。
この件についてのバグレポは提出済みであり、 Issue Tracker にも起票されています。
現時点でのステータスとしては In Progress
になっているので、もしステータスに更新があったら記事の方も更新していければと思います。
- [Android] FrameTiming.GPUFrameTime is showing a clock in milliseconds that increases by 1000 every second when built on Android
-
[Android] GPU FrameTime is obtained incorrectly with too high values in the Player when Graphics API is set to OpenGLES3 (Duplicate))
- ※自分のバグレポベースに起票された Issue はこちら。既知の報告があったために現在のステータスは
Duplicate
- ※自分のバグレポベースに起票された Issue はこちら。既知の報告があったために現在のステータスは
Metal Performance HUD ( iOS のみ )
次は Metal Performance HUD について解説します。
(以降、こちらの機能を HUD
と省略)
こちらは OS 標準で提供されている開発機能であり、手軽に導入できる上で今回のテーマである「GPU 負荷」についても計測することが可能です。
具体的な概要や表示方法については公式ドキュメント及び次の公式動画が参考になります。
その上で後者の動画に関しては長さが 6:44
と短い上に右下の︙から日本語字幕も表示することが可能なので、気になる方は一度こちらを御覧ください。
ここでは幾つかのポイントについて解説していきます。
HUD の表示内容
表示内容は次のようになってます。
詳細についてはドキュメントや動画を御覧ください。
◇ 上部
上部には GPU 名や解像度、リフレッシュレートなどが表示されます。
- 1行目: GPU名 / 解像度
- 2行目: スケーリングファクター、モード (Direct / Composited) 4、リフレッシュレート
◇ 中央部
中央部には直近の 1.5 秒間における各要素の「平均値、最小値、最大値」が表示されます。
(フォーマットとしては (要素): 平均 [最小 最大]
と言う並び順です)
表示される要素としては次の物があります。
- 1行目: FPS
- 2行目: Present Time delta
- 3行目: GPU Time
ちなみに「最小値」と「最大値」が平均値から 15% 以上異なる場合には、識別しやすいように赤文字で表示されます。
◇ 下部
下部にはメモリ使用量の情報のほか、中央部にある情報がグラフとして表示されます。
- メモリ使用量については
Mem: アプリ全体 [GPU]
の並び順で表示されます - グラフについては直近 200 フレーム分の
Present Time delta
及びGPU Time
が表示されます- それぞれ中央部の文字色と対応してます
HUD の表示方法について
表示方法については幾つかありますが、ここでは次の方法について解説していきます。
- 実機上から有効にする
- Xcode 上から有効にする
◇ 実機上から有効にする
恐らくは一番手軽な方法です。
設定アプリから「デベロッパ」メニューを開き、下にスクロールしていくと「 グラフィックスの HUD」と言う項目が存在するので、こちらを有効にしてアプリを動かすことで HUD が表示されるようになります。
このようにノーコーディングで HUD を表示させることが可能となるので、計測機能が未実装のアプリでも簡単に計測を行えるようになるかと思います。
一点注意点として、こちらから HUD 表示を有効化にした場合には、アプリ内から HUD を非表示にすることが不可能になります。
アプリ内から表示 / 非表示を切り替える方法ついては別途後述しますが、設定アプリから有効にしている場合にはこちらも効かなくなるみたいです。
そのため、非表示にする際には一度アプリキルした上で再度設定アプリ上から無効にする必要があります。
◇ Xcode 上から有効にする
他にも Xcode のスキーム設定から有効にすることも可能です。
手順としては Xcode 上から Edit Scheme...
を開き、Run > Diagnostics
にある「 Show Graphics Overview 」にチェックを付けることで有効にできます。
◆ Unity から設定を自動化する
PostProcessBuildAttribute や IPostprocessBuildWithReport などを用いて、ビルド後にスキーム設定を自動で書き換えるようにすることで実現可能です。
(この手順についての詳細はこちらの記事を参照)
ただ、Unity が標準で提供している XcScheme にはこちらの設定変更を行う API が生えていないので、今回は Reflection を用いて自前で設定を書き込むことで有効にするサンプルコードを載せます。
sealed class XcodePostProcess : IPostprocessBuildWithReport
{
public int callbackOrder => 0;
public void OnPostprocessBuild(BuildReport report)
{
if (report.summary.platform != BuildTarget.iOS) return;
var outputPath = report.summary.outputPath;
var schemePath = Path.Combine(
outputPath,
"Unity-iPhone.xcodeproj/xcshareddata/xcschemes/Unity-iPhone.xcscheme");
ShowPerformanceHUD(schemePath);
}
// 起動時に表示するか
private void ShowPerformanceHUD(string schemePath)
{
var xcScheme = new XcScheme();
xcScheme.ReadFromFile(schemePath);
// Performance HUD を有効化するための API が存在しないので、`XcScheme` が持つ `m_Doc` を取得して直接編集する
var fieldInfo = typeof(XcScheme).GetField("m_Doc",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (fieldInfo == null || fieldInfo.GetValue(xcScheme) is not XDocument xDocument || xDocument.Root == null)
{
Debug.LogError("Failed to get XDocument from XcScheme");
return;
}
// `LaunchAction` に `showGraphicsOverview` と `logGraphicsOverview` を追加
// ※ HUD を表示するだけであれば `logGraphicsOverview` は不要
var xElement = xDocument.Root.XPathSelectElement("./LaunchAction");
Assert.IsNotNull(xElement, "The XcScheme document does not contain build configuration setting");
xElement.SetAttributeValue((XName)"showGraphicsOverview", "Yes");
xElement.SetAttributeValue((XName)"logGraphicsOverview", "Yes");
xcScheme.WriteToFile(schemePath);
}
}
アプリ上から表示 / 非表示を切り替えられるようにする
HUD は上述の方法で表示する以外にも、ネイティブプラグインを実装することでアプリ上から表示 / 非表示の切り替えを行えるようにすることも可能です。
これを実現するには次のステップで実装していきます。
-
Info.plist
5 に設定を加える - HUD の表示切替を行うためのネイティブプラグインを実装する
◇ Info.plist
に設定を加える
コードから切り替えるためには Info.plist
に MetalHudEnabled
と言うキーを加え、値には YES
を設定する必要があります。
こちらの手順は先程と同じく PostProcessBuildAttribute や IPostprocessBuildWithReport を用いることで次のように設定を自動化することが可能です。
sealed class XcodePostProcess : IPostprocessBuildWithReport
{
public int callbackOrder => 0;
public void OnPostprocessBuild(BuildReport report)
{
if (report.summary.platform != BuildTarget.iOS) return;
var outputPath = report.summary.outputPath;
var schemePath = Path.Combine(
outputPath,
"Unity-iPhone.xcodeproj/xcshareddata/xcschemes/Unity-iPhone.xcscheme");
EnablePerformanceHUD(outputPath);
}
// コード上から切り替える場合にはこちら
private void EnablePerformanceHUD(string outputPath)
{
// plist.info に `MetalHudEnabled : true` を追加することでコード上から ON/OFF することが可能になる
var plistPath = Path.Combine(outputPath, "Info.plist");
var plist = new PlistDocument();
plist.ReadFromFile(plistPath);
var root = plist.root;
// NOTE: `false` にするとコード上から ON/OFF 出来なくなる
root.SetBoolean("MetalHudEnabled", true);
plist.WriteToFile(plistPath);
}
}
◇ HUD の表示切替を行うためのネイティブプラグインを実装する
次に表示切替を行うためのプラグインを実装していきます。
切り替えを行うには CAMetalLayer が持つ developerHUDProperties と言うプロパティに値を設定する必要があります。
Unity の場合には UnityGetGLView()
と言う関数から得られる UIView
から CAMetalLayer
を取得することが可能なため、次のようなプラグインを実装することで実現可能です。6
#import <UIKit/UIKit.h>
// NOTE: Swift からこちらを取得するのが面倒なので、手っ取り早く ObjC で実装する
extern UIView* UnityGetGLView();
#ifdef __cplusplus
extern "C" {
#endif
void showPerformanceHUD() {
if (@available(iOS 16.0, *)) {
auto view = UnityGetGLView();
((CAMetalLayer*)(view.layer)).developerHUDProperties =
@{
// HUD を有効する場合はこちらを設定 ( value には "default" を渡す )
@"mode" : @"default",
// Logging を有効にする場合はこちらを設定
@"logging" : @"default",
};
}
}
void hidePerformanceHUD() {
if (@available(iOS 16.0, *)) {
auto view = UnityGetGLView();
// 無効にするなら Dictionary から項目を消すだけ
((CAMetalLayer*)(view.layer)).developerHUDProperties = @{};
}
}
#ifdef __cplusplus
}
#endif
あとは C# から P/Invoke で呼び出すことでコード上から表示を切り替えられるようになります。
public static class MetalPerformanceHUD
{
/// <summary>
/// Performance HUD を表示する
/// </summary>
public static void ShowPerformanceHUD()
{
#if !UNITY_EDITOR && UNITY_IOS
NativeMethod();
[DllImport("__Internal", EntryPoint = "showPerformanceHUD")]
static extern void NativeMethod();
#endif
}
/// <summary>
/// Performance HUD を非表示にする
/// </summary>
public static void HidePerformanceHUD()
{
#if !UNITY_EDITOR && UNITY_IOS
NativeMethod();
[DllImport("__Internal", EntryPoint = "hidePerformanceHUD")]
static extern void NativeMethod();
#endif
}
}
注意点 / 制限事項
最後に幾つか注意点や制限事項について触れていきます。
これから紹介する内容については自分が解決方法を把握していないだけの可能性もあります。
もし解決方法についてご存じの方が居たらコメントや編集リクエストなどで教えて頂けると幸いです...
◇ ノッチとかぶる
縦画面 (Portrait
) のアプリの場合、ノッチがあるデバイスで HUD を表示するとかぶってしまう言う問題を確認してます。
(恐らくは SafeArea を考慮せずに配置されている?)
その上で HUD の表示位置を調整する方法についても不明だったので、現状はかぶった状態で計測することになるかと思われます。
ただ、手元のiPhone12 ProMax にてデバイス解像度で動かして見る分には幸いにも中央部までは隠れていないので、計測には使えるかな...?と言う印象です。
◇ スクリーン解像度によってサイズが変わる
HUD の表示サイズが解像度に依存しているせいか、例えば Screen.SetResolution から解像度を変更すると HUB の表示サイズが変わってしまう現象を確認してます。
以下の画像は「左: 540x1168
」「右: 1284x2778
」に設定して比較したものです。
このように負荷対策などで解像度を意図的に下げている場合には HUD の表示サイズに影響が出る可能性があります。
Profiling.Recorder
最後に Profiling.Recorder と言う API について簡単に紹介します。
こちらは 2020 系統から GPU の時間計測に対応しており、詳細については黒河さんの記事が参考になります。
他にも次の講演では URP で GPU 負荷を画面上に表示する方法について解説されています。
手元にある幾つかの iOS デバイス7で実行してみた所、全てのデバイスで SystemInfo.supportsGpuRecorder から false
が返ってきました。
たまたま動かないバージョンを引いただけなのか、それもと Unity や iOS / Xcode のバージョンアップで挙動が変わってしまったのかは不明ですが、iOS だと動作が怪しい可能性はあります...。
こちらの API については別途検証を進めているところでもあるので、機会があれば別途記事として纏めるかもしれません。
(アドカレに間に合わなかった...)
おわりに
アプリ上から GPU 負荷計測を行う方法について幾つか紹介しました。
特に FrameTimingManager
や Metal Performance HUD
はアプリ全体の負荷を手軽に計測することが可能となるので、こちらを表示してパフォーマンスボトルネックを特定し、他のツールと組み合わせることでパフォーマンスチューニングを行っていけるかなと思います。
サンプルプロジェクトについて
今回解説した機能を幾つか組み込んだサンプルプロジェクトを用意しました。
自身のプロジェクトに組み込む際に参考にして頂けると幸いです。
主な機能としては以下。
- 解像度変更
- 解像度を拡縮させることで GPU 負荷に影響が出ているかを確認するために入れてます
-
Metal Performance HUD
の表示 / 非表示切り替え -
FrameTimingManager
で計測したGPU FrameTime
の表示- 実装としては Rendering Debugger > Display Stats を参考にほぼそのままの形で切り出してます
参考/関連リンク
FrameTimingManager
- 公式Blog
- Documents
- API
- 参考記事
Metal Performance HUD
Profiling.Recorder
- API
- 参考記事
-
ちなみに Xcode は GPU 負荷を計測することが可能です。Android のツールについてはそこまで知見が無いため、各種ベンダーが提供するツールでどの程度計測できるかについては未調査... ↩
-
他にも
OpenGL ES 2
も挙げられますが、現時点ではDeprecated
となっているため未検証です ↩ -
正確に言うと SRP の
Core RP Library
に含まれている ↩ -
「モード ( Direct / Composited )」についてはまだ理解しきれていない点があるので、理解でき次第に補足出来ればな〜と考えています... (もしご存じの方が居たらコメントや編集リクエストなどで教えていただけると助かります...) ↩
-
Info.plist
とは大雑把に説明するとアプリに関する設定ファイルであり、Unity の場合には iOS ビルドで生成される Xcode プロジェクトに自動で含まれます ↩ -
頑張れば Swift オンリーでも実装可能かと思われますが、今回は
UnityGetGLView
を参照する都合 + コード的にもそこまで複雑では無いので、手っ取り早くObjC++
で実装してます ↩ -
iPhone12 ProMax (iOS 17.1)、iPhone15 ProMax (iOS 17.1)、iPhoneX (iOS 16.7.3)、iPod touch 7th (iOS 15.8) で確認 ↩