LoginSignup
16
15

【Unity】モバイルアプリ上で GPU 負荷を計測する

Last updated at Posted at 2023-12-17

この記事は 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.

サードパーティー製のツールとしては次のようなものが挙げられます。

これらのツールを用いれば 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 の取得以外にも cpuMainThreadFrameTimecpuRenderThreadFrameTime と言った CPU 負荷に関する内訳も取得することが可能です。

  • cpuFrameTime
    • CPU の総フレーム時間を指し、メインスレッドでのフレーム開始から次フレームまでの時間として計算される
  • cpuMainThreadFrameTime
    • メインスレッドの作業時間、つまりフレームの開始からメインスレッドが仕事を終えるまでの時間の合計を指す
  • cpuRenderThreadFrameTime
    • レンダースレッドの作業時間、つまりレンダースレッドに送信された最初の作業要求から Present() 関数が呼び出されるまでの時間の合計を指す
  • cpuMainThreadPresentWaitTime
    • フレーム中で CPU が Present() の完了を待っている時間
  • gpuFrameTime
    • GPU の作業時間、つまり GPU に作業が送信された時刻から GPU が作業を終了したことを示す信号が出る時刻までの時間の合計を指す

そのため、用途としては今回のテーマでもある「GPU 負荷計測」に限らず、アプリ全体の負荷計測やボトルネックの調査などに活用していけるかと思います。

詳細情報や使い方などについては公式ドキュメントの他、以下の公式ブログ記事が参考になります。

ブログ中でも触れられてますが、FrameTimingManager2022.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 のサンプルをビルドして、実機上で表示させた場合には次のようになります。

DisplayStats.jpeg

何れにせよ、リリースビルド相当で表示する際や Built-in RenderPipeline を用いる場合には自前で実装する必要が出てきます。

注意点 / 制限事項

公式ドキュメントやブログにも制限事項として記載されてますが、こちらでもいくつかピックアップします。

    1. 返すフレームは現在のフレームではなく、4フレーム分遅れた結果が返ってくる
    1. GPU FrameTime は全てのフレームで使用可能であることが保証されておらず、正確な値を返せない場合がある
    1. Metal のような TBDR (Tile-based Deferred Rendering) の環境下では GPU FrameTime の精度が落ちる場合がある

1 についてはフレームの終了時点では直ぐに結果を得ることが出来ないので、同期を取るための仕様になるかと思います。
3 については手元でサンプルシーンなどを動かして計測してみた分には Xcode 上での計測結果とそこまで差は見受けられなかったものの、場合によっては計測結果に差が発生すると言った現象も確認できたので、これらの仕様を踏まえた上で参考値として活用するのが良いのかと思われます。

◇ Android OpenGL ES 3.x 系統だと GPU FrameTime が正しく計測できない?

こちらは不具合になるかと思われますが、Android で GraphicsAPIOpenGL ES 3.x にすると GPU FrameTime が正確に計測できていない?と思われる現象を確認しました。

具体的に言うと今回検証したバージョンである Unity 2022.3.13f1 では次のような現象を手元で確認できています。

  • 47404688.459 ms と言った明らかな不正値が返ってくる
  • どのような状況でも一定の数値から変動しない
    • 例えば Screen.SetResolution を用いて解像度を極端に上げようが下げようが、常に一定した数値が返り続ける

こちらは Metal 及び Vulkan だと再現できなかった不具合となるので、恐らくは OpenGL ES 3.x 起因の不具合になるかと思われます。

この件についてのバグレポは提出済みであり、 Issue Tracker にも起票されています。
現時点でのステータスとしては In Progress になっているので、もしステータスに更新があったら記事の方も更新していければと思います。

Metal Performance HUD ( iOS のみ )

次は Metal Performance HUD について解説します。
(以降、こちらの機能を HUD と省略)

こちらは OS 標準で提供されている開発機能であり、手軽に導入できる上で今回のテーマである「GPU 負荷」についても計測することが可能です。

iPhone12ProMax.jpeg

具体的な概要や表示方法については公式ドキュメント及び次の公式動画が参考になります。
その上で後者の動画に関しては長さが 6:44 と短い上に右下の︙から日本語字幕も表示することが可能なので、気になる方は一度こちらを御覧ください。

ここでは幾つかのポイントについて解説していきます。

HUD の表示内容

表示内容は次のようになってます。
詳細についてはドキュメントや動画を御覧ください。

◇ 上部

上部には GPU 名や解像度、リフレッシュレートなどが表示されます。

  • 1行目: GPU名 / 解像度
  • 2行目: スケーリングファクター、モード (Direct / Composited) 4、リフレッシュレート

Hud1.png

◇ 中央部

中央部には直近の 1.5 秒間における各要素の「平均値、最小値、最大値」が表示されます。
(フォーマットとしては (要素): 平均 [最小 最大] と言う並び順です)

表示される要素としては次の物があります。

  • 1行目: FPS
  • 2行目: Present Time delta
  • 3行目: GPU Time

ちなみに「最小値」と「最大値」が平均値から 15% 以上異なる場合には、識別しやすいように赤文字で表示されます。

Hud2.png

◇ 下部

下部にはメモリ使用量の情報のほか、中央部にある情報がグラフとして表示されます。

  • メモリ使用量については Mem: アプリ全体 [GPU] の並び順で表示されます
  • グラフについては直近 200 フレーム分の Present Time delta 及び GPU Time が表示されます
    • それぞれ中央部の文字色と対応してます

Hud3.png

HUD の表示方法について

表示方法については幾つかありますが、ここでは次の方法について解説していきます。

  • 実機上から有効にする
  • Xcode 上から有効にする

◇ 実機上から有効にする

恐らくは一番手軽な方法です。

設定アプリから「デベロッパ」メニューを開き、下にスクロールしていくと「 グラフィックスの HUD」と言う項目が存在するので、こちらを有効にしてアプリを動かすことで HUD が表示されるようになります。

このようにノーコーディングで HUD を表示させることが可能となるので、計測機能が未実装のアプリでも簡単に計測を行えるようになるかと思います。

Settings.jpg

一点注意点として、こちらから HUD 表示を有効化にした場合には、アプリ内から HUD を非表示にすることが不可能になります。
アプリ内から表示 / 非表示を切り替える方法ついては別途後述しますが、設定アプリから有効にしている場合にはこちらも効かなくなるみたいです。

そのため、非表示にする際には一度アプリキルした上で再度設定アプリ上から無効にする必要があります。

◇ Xcode 上から有効にする

他にも Xcode のスキーム設定から有効にすることも可能です。

手順としては Xcode 上から Edit Scheme... を開き、Run > Diagnostics にある「 Show Graphics Overview 」にチェックを付けることで有効にできます。

EditScheme1.png

EditScheme2.png

◆ Unity から設定を自動化する

PostProcessBuildAttributeIPostprocessBuildWithReport などを用いて、ビルド後にスキーム設定を自動で書き換えるようにすることで実現可能です。
(この手順についての詳細はこちらの記事を参照)

ただ、Unity が標準で提供している XcScheme にはこちらの設定変更を行う API が生えていないので、今回は Reflection を用いて自前で設定を書き込むことで有効にするサンプルコードを載せます。

XcodePostProcess.cs
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 は上述の方法で表示する以外にも、ネイティブプラグインを実装することでアプリ上から表示 / 非表示の切り替えを行えるようにすることも可能です。

これを実現するには次のステップで実装していきます。

  1. Info.plist5 に設定を加える
  2. HUD の表示切替を行うためのネイティブプラグインを実装する

Info.plist に設定を加える

コードから切り替えるためには Info.plistMetalHudEnabled と言うキーを加え、値には YES を設定する必要があります。

こちらの手順は先程と同じく PostProcessBuildAttributeIPostprocessBuildWithReport を用いることで次のように設定を自動化することが可能です。

XcodePostProcess.cs
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

MetalPerformanceHUD.mm
#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 で呼び出すことでコード上から表示を切り替えられるようになります。

MetalPerformanceHUD.cs
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
    }
}

注意点 / 制限事項

最後に幾つか注意点や制限事項について触れていきます。

これから紹介する内容については自分が解決方法を把握していないだけの可能性もあります。
もし解決方法についてご存じの方が居たらコメントや編集リクエストなどで教えて頂けると幸いです... :bow:

◇ ノッチとかぶる

縦画面 (Portrait) のアプリの場合、ノッチがあるデバイスで HUD を表示するとかぶってしまう言う問題を確認してます。
(恐らくは SafeArea を考慮せずに配置されている?)

その上で HUD の表示位置を調整する方法についても不明だったので、現状はかぶった状態で計測することになるかと思われます。
ただ、手元のiPhone12 ProMax にてデバイス解像度で動かして見る分には幸いにも中央部までは隠れていないので、計測には使えるかな...?と言う印象です。

◇ スクリーン解像度によってサイズが変わる

HUD の表示サイズが解像度に依存しているせいか、例えば Screen.SetResolution から解像度を変更すると HUB の表示サイズが変わってしまう現象を確認してます。

以下の画像は「左: 540x1168」「右: 1284x2778」に設定して比較したものです。
このように負荷対策などで解像度を意図的に下げている場合には HUD の表示サイズに影響が出る可能性があります。

Compare.png

Profiling.Recorder

最後に Profiling.Recorder と言う API について簡単に紹介します。

こちらは 2020 系統から GPU の時間計測に対応しており、詳細については黒河さんの記事が参考になります。

他にも次の講演では URP で GPU 負荷を画面上に表示する方法について解説されています。

手元にある幾つかの iOS デバイス7で実行してみた所、全てのデバイスで SystemInfo.supportsGpuRecorder から false が返ってきました。

たまたま動かないバージョンを引いただけなのか、それもと Unity や iOS / Xcode のバージョンアップで挙動が変わってしまったのかは不明ですが、iOS だと動作が怪しい可能性はあります...。

こちらの API については別途検証を進めているところでもあるので、機会があれば別途記事として纏めるかもしれません。
(アドカレに間に合わなかった...)

おわりに

アプリ上から GPU 負荷計測を行う方法について幾つか紹介しました。

特に FrameTimingManagerMetal Performance HUD はアプリ全体の負荷を手軽に計測することが可能となるので、こちらを表示してパフォーマンスボトルネックを特定し、他のツールと組み合わせることでパフォーマンスチューニングを行っていけるかなと思います。

サンプルプロジェクトについて

今回解説した機能を幾つか組み込んだサンプルプロジェクトを用意しました。
自身のプロジェクトに組み込む際に参考にして頂けると幸いです。

主な機能としては以下。

  • 解像度変更
    • 解像度を拡縮させることで GPU 負荷に影響が出ているかを確認するために入れてます
  • Metal Performance HUD の表示 / 非表示切り替え
  • FrameTimingManager で計測した GPU FrameTime の表示

Sample.jpeg

参考/関連リンク

FrameTimingManager

Metal Performance HUD

Profiling.Recorder

  1. ちなみに Xcode は GPU 負荷を計測することが可能です。Android のツールについてはそこまで知見が無いため、各種ベンダーが提供するツールでどの程度計測できるかについては未調査...

  2. 他にも OpenGL ES 2 も挙げられますが、現時点では Deprecated となっているため未検証です

  3. 正確に言うと SRP の Core RP Library に含まれている

  4. 「モード ( Direct / Composited )」についてはまだ理解しきれていない点があるので、理解でき次第に補足出来ればな〜と考えています... (もしご存じの方が居たらコメントや編集リクエストなどで教えていただけると助かります...:bow:)

  5. Info.plist とは大雑把に説明するとアプリに関する設定ファイルであり、Unity の場合には iOS ビルドで生成される Xcode プロジェクトに自動で含まれます

  6. 頑張れば Swift オンリーでも実装可能かと思われますが、今回は UnityGetGLView を参照する都合 + コード的にもそこまで複雑では無いので、手っ取り早く ObjC++ で実装してます

  7. iPhone12 ProMax (iOS 17.1)、iPhone15 ProMax (iOS 17.1)、iPhoneX (iOS 16.7.3)、iPod touch 7th (iOS 15.8) で確認

16
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
15