- はじめに
- Metal System Traceでできること
- 開発環境
- 用語
- Chapter1. 単色を出力するシェーダーの負荷を調べてみよう
- Vertex Function の負荷
- Fragment Function の負荷
- Chapter2. テクスチャサンプリングの負荷を調べてみよう
- Chapter3. Performance Stateを活用しよう
- 最後に
- リンク
はじめに
こんにちは、 サイバーエージェントのゲーム・エンターテイメント事業部(SGE)に所属する子会社Colorful Paletteで Unityエンジニアをしている永留です。
この記事では、Xcode15 の Metal System Traceを利用してシェーダーの負荷を調べる方法を紹介します。
Metal System Traceでできること
Metal System Trace を利用することで、iOS実機でのGPUの負荷を調べることができます。
開発環境
開発に使用した環境は以下のとおりです。
- Unity2022.3.21f1
- Xcode 15.4
- iPhone11 (iOS17.5)
用語
記事で出てくる用語について記載します。
- Vertex Function
- 頂点シェーダー関数のこと。
- Unityシェーダーで
#pragma vertex vert
と指定している場合は、vert
が Vertex Function
- Fragment Function
- フラグメントシェーダー関数のこと。
- Unityシェーダーで
#pragma fragment frag
と指定している時は、frag
が Fragment Function
Chapter1. 単色を出力するシェーダーの負荷を調べてみよう
カメラの正面にUnity標準のSphereを配置し、単色を出力するシェーダーをアタッチします。
このシーンをiOSデバイスで実行した時の負荷を確認してみましょう。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142003.png)
v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); return o; } half4 frag (v2f i) : SV_Target { return half4(1, 0.5, 0.3, 0.2); }
シェーダーコード (Test001_UnlitColor.shader)
Shader "MyShader/Test001_UnlitColor"
{
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
half4 frag (v2f i) : SV_Target
{
return half4(1, 0.5, 0.3, 0.2);
}
ENDCG
}
}
}
Schemeの編集
Metal System Trace を利用するためには、XcodeにてSchemeの設定を行う必要があります。
OptionsタブのGPU Frame CaptureをMetalにします。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142005.png)
Diagnosticsタブ の API Validation を有効化しておきます
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142008.png)
キャプチャの実行
Xcode上でアプリをビルドしてiOS実機に転送すると、M のような見た目のボタンが出てくるので、これをクリックします。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142011.png)
Captureボタンをクリックすることで、キャプチャが始まります。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142013.png)
キャプチャの確認
キャプチャが完了すると、Metal System Traceが表示されます。
ウィンドウ左側のDebug NavigatorにはGPUの情報が表示されています。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142015.png)
Pipeline Stateによるグルーピング
Group By API Callと書かれた部分を Group By Pipeline Stateに変更することで、パイプラインステートごとの描画内容を確認できます。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142018.png)
Pipeline Stateでグルーピングすることで、Pipeline Stateの負荷が表示されるようになります。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142022.png)
Shader editorの起動
今回のシェーダーを実行しているパイプラインステートを選択し、ウィンドウ右側からShader editorを起動します。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142020.png)
Vertex Function の負荷
Vertex Function を確認した場合は以下のような表示になっています。
Vertex Functionの処理にかかった合計時間や、シェーダーの各行の実行にかかった時間が確認できます。
円グラフの部分にマウスカーソルを合わせると、処理負荷の内訳を確認できます。
iOSのバージョンによって、表示が異なります。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142032.png)
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142105.png)
これらの数値の意味を理解するには、頂点シェーダーの実行フローを知っておいた方が良いでしょう。
Vertex Function の実行フロー
Appleのプロセッサーはタイルベース遅延レンダリング (TBDR) を採用しており、以下のようなフローでVertex Functionが実行されます。
① メインメモリから GPU へ頂点データを読み出す (Load)
② GPUで Vertex Function を実行し、頂点を計算する
③ 計算結果を Tiled Vertex Buffer へ書き出す (Vertex Write)
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142054.png)
Shader editor上で確認できる、処理負荷は以下のような意味を持ちます。
- ALU : GPU上での算術演算にかかった時間
- Wait Memory : メモリアクセスの同期までにGPUが待機した時間
- Load : メモリからGPUへ頂点データを読み出すのにかかった時間
- Vertex Write : Vertex Function で計算した頂点データをTiled Vertex Bufferへ書き込みのにかかった時間
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142032.png)
参考 : https://developer.apple.com/documentation/xcode/inspecting-shaders
Fragment Function の負荷
Fragment Function の負荷も見てみましょう。
- Fragmentの実行時間は 100.67μs
SV_Target0
の設定にかかった時間は 12.54% (約 12.6μs)- Fragmentの出力 (バッファへの書き込み)にかかった時間は 87.46% (約 88μs)
Unityのシェーダーコード(fragment)
half4 frag (v2f i) : SV_Target
{
return half4(1, 0.5, 0.3, 0.2);
}
Frament Function の実行フロー
Fragmentは以下のように実行されます。
① Tiled Vertex Bufferから頂点データを読み出す
② 読み出した頂点データをラスタライズし、フラグメントを構築
③ フラグメントをFragment Functionへ入力する
④ Fragment Functionでピクセルを計算する (ALU)
⑤ 計算したピクセルをFrame Bufferへ出力する (Store)
Frament Function の負荷の内訳
Fragmentの円グラフは以下のような意味を持ちます。
- ALU : Fragmentの算術演算にかかった時間
- Store : Fragmentの出力をFrameBufferへ書き込むのにかかった時間
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142038.png)
Chapter2. テクスチャサンプリングの負荷を調べてみよう
続いては、テクスチャをサンプリングを行った時のシェーダーの負荷を見てみましょう
テクスチャサンプリングを行うシェーダーをChapter1と同じくSphereに設定し、iOSデバイス上でのGPU負荷を計測してみます。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142025.png)
v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } half4 frag (v2f i) : SV_Target { return tex2D(_MainTex, i.uv); }
シェーダーコード (Test002_Texture.shader)
Shader "MyShader/Test002_Texture"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return tex2D(_MainTex, i.uv);
}
ENDCG
}
}
}
Vertex負荷
頂点シェーダーの負荷は以下のようになりました。
Chapter1では頂点負荷が29μsほどでしたが、こちらは頂点負荷が3μsほど増加しています。
グラフを比較してみると、Chapter2の方がメモリアクセスの比重が大きくなっていることがわかります。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142032.png)
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142041.png)
Vertexの負荷が増えた理由
Chapter1では、頂点シェーダーで頂点座標のみを出力していました。
v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); return o; }
Chapter2では頂点座標とUVの両方を入出力しているため、メモリアクセスの量が増えます。
v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; }
Fragment負荷
Fragment Functionの負荷を見てみましょう。
Chapter1 は 100.67μsでしたので、テクスチャサンプリングを追加したことで負荷が72μsほど増えています。
Fragment負荷の比較
Chapter1とChapter2のグラフを比較してみると、 Synchronization(Wait Memory) の負荷が増えていることがわかります。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142038.png)
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142048.png)
テクスチャサンプリングのメモリ同期
Fragment Function でテクスチャをサンプリングする場合、テクスチャデータをメインメモリから読み出す必要がありますが、
テクスチャはサイズが非常に大きいため、データの読み出しが完了するまでに長い時間がかかります。
テクスチャデータのGPU側への読み出しが完了するまではFragmentを実行できないので、同期までの待機が発生します。
今回の計測では、Fragment時間 172.72μs のうち、Wait Memory
の割合は 57.07% なので 約 98μs の待機時間が発生しています。
Chapter3. Performance Stateを活用しよう
最後に、iOSデバイス上での負荷計測を行う際に有用な Performance State について紹介したいと思います。
発熱による性能劣化
iOSデバイスは熱を持つと、サーマルスロットリング によって端末の性能が低下します。
例えば、iOSデバイスを充電したままゲームを長時間遊んでいると端末が熱を持ち、FPSが大きく下がることがあります。
Performance State
iOSデバイスは、端末の熱や品質設定などさまざまな要因によってパフォーマンスが変動します。
Xcodeでは、パフォーマンスの状態( Performance State ) をシミュレートし、GPU速度を計測するという機能が備わっております。 これを利用すれば、「端末が熱くなった時にどれくらいの速度が出るか」を調べるといったことができます。
Xcode15では、3つのPerformance Stateが利用可能です。
- Maximum : パフォーマンスが最も高い状態をシミュレートして、GPU負荷を計測
- Medium : パフォーマンスが中くらいの状態をシミュレートして、GPU負荷を計測
- Minimum : パフォーマンスが最も低い状態をシミュレートして、GPU負荷を計測
発熱によって速度が低下したときのGPU負荷を調べたい場合、Minimum を選ぶと良いでしょう。
Performance State の使い方
時計のような見た目のアイコンをクリックします
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142110.png)
Performance Stateの指定ができます。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/sge_coretech/20240819/20240819142112.png)
最後に
今回の記事では、XcodeのMetal System Traceの基本的な使い方について紹介しました。
XcodeのMetal System TraceのShader editorを利用すると、シェーダーの各行の処理負荷を詳細に確認することができます。
次回の記事では、シェーダーでアルファクリップを使った時のGPU負荷について触れてみたいと思います。
リンク
Inspecting shaders | Apple Developer Documentation
Optimize Metal apps and games with GPU counters - WWDC20 - Videos - Apple Developer