
-
Metal 4ゲームの詳細
Metal 4の最新の機能強化を導入してゲームエンジンを最適化する方法を学びましょう。コマンドのエンコーディングを統一してCPUオーバーヘッドを最小限に抑える方法、大規模なシーンをサポートするためにグラフィックスリソース管理を拡張する方法、割り当てられたメモリ使用量を最大限に活用する方法、パイプライン状態の大規模なライブラリを迅速にロードする方法などを紹介します。 このセッションの内容を十分理解できるよう、まず「Discover Metal 4」を視聴することをおすすめします。
関連する章
- 0:00 - イントロダクション
- 1:33 - エンコードの効率化
- 8:42 - リソース管理のアップスケール
- 17:24 - パイプラインの迅速なローディング
- 31:25 - 次のステップ
リソース
- Human Interface Guidelines: Designing for games
- Metal binary archives
- Reading and writing to sparse textures
- Resource synchronization
- Synchronizing resource accesses between multiple passes with a fence
- Synchronizing resource accesses with earlier passes with a consumer-based queue barrier
- Synchronizing resource accesses with subsequent passes with a producer-based queue barrier
- Synchronizing resource accesses within a single pass with an intrapass barrier
- Understanding the Metal 4 core API
- Using a render pipeline to render primitives
- Using the Metal 4 compilation API
関連ビデオ
WWDC25
- イマーシブなアプリを作成するためのMetalレンダリングの新機能
- ゲームをレベルアップさせる方法
- Metal 4による機械学習とグラフィックスの統合
- Metal 4の概要
- Metal 4ゲームの知識を深める
Tech Talks
-
このビデオを検索
こんにちは Jasonです Yangです 私たちはGPUドライバエンジニアです このビデオでは Metal 4でゲームエンジンを 高速化する方法を紹介します これは4部構成のシリーズの第2部で Metal APIの次のメジャーバージョン について紹介します Metal 4ゲームの詳細の前に 「Discover Metal 4」をご覧ください Metal 4の概要を説明しています このビデオの次は 「Go Further with Metal 4 Games」で 新しいMetal FXとMetal Raytracingの APIについて説明します 「Metal 4 Machine Learning」では MLの組み込みについて説明します では Metal 4の説明に入ります
Metal 4は最新のゲームエンジン向けに 設計されています Ubisoftのゲーム 「アサシン クリード:シャドウズ」では 驚くほど精細に キャラクターや風景が描画され ファンタジーの世界に引き込まれます ギガバイト単位の詳細なジオメトリや テクスチャをストリーミングし レンダリングに膨大な数のシェーダを使い Appleシリコンの演算能力を 最大限に活用しています これからのゲームは 求められる水準が さらに高くなり タスクに応じた拡張性の高い グラフィックスAPIが必要になります それに応えるのがMetal 4です
Metal 4には ゲームに役立つ Metalの 重要な最新機能がいくつも含まれています 効率的なコマンドのエンコーディングや 拡張型のリソース管理による ホットパスの高速化 読み込み中画面なしでのプレイを実現する パイプライン読み込みの高速化などです ここでは 同僚とともにこれらの機能の 最適な使い方についてお話します ゲームのすべてのフレームで 描画呼び出しやカーネルディスパッチ ブリットおよびレイトレーシング処理が エンコーディングのホットパスになります Metal 4のエンコーディングは 効率化と並行処理により この課題に対応するよう設計されています
Metal 4では 使用頻度の高い処理を 2つのエンコーダクラスにまとめ 各エンコーダの機能を高めています コマンドアロケータでは エンコーディングに伴う メモリ割り当てを明示的に管理できます コマンドバッファにより 複数のスレッドで 処理をエンコードできます
「Discover Metal 4」では Renderと Computeの2つのエンコーダクラスによる コマンドエンコーディングの 処理について説明しました これらを効率的に使うには 演算処理間のデータの依存関係を同期し アタッチメントマップを使って フラグメントシェーダの出力の 再マッピングを行う必要があります すべての演算処理やカーネルディスパッチ ブリット、アクセラレーション構造の構築を 1つの演算エンコーダで エンコードできるようになります 特に同期の必要なく これらのコマンドを同時に実行できます これにより 依存関係のないワークロードで GPUリソースを効果的に使用できます データの依存関係により パスのコマンドを シリアルに実行する必要がある場合は パスバリアを使って明示できます このバリアにより GPUが待機状態になり このエンコーダのそれまでのブリットが すべて完了してから その後の演算ディスパッチが開始されます こちらは ブリットからディスパッチへの アクセスを同期する方法の一例です まず copyFromBufferのブリットでbuffer1を 更新して パスバリアをエンコードします これで buffer1のデータを使用する ディスパッチをエンコードできます これが統合演算エンコーディングです すべての演算処理を1つのエンコーダで実行し バリアでデータの依存関係を表現すると Metal 4によりレンダリングの エンコーディングも更新されます カラーアタッチメントマッピングにより レンダリングパイプラインの色出力と レンダリングエンコーダのアタッチメントの 対応関係を制御できるようになります 固定のレンダターゲットレイアウトに パイプラインをバインドするのではなく カラーアタッチメントマップを指定できます エンコーダを切り替えるのではなく 新しいパイプラインの設定時に マップを変更できます 3つのアタッチメントに描画する fragment関数を持つ Metalパイプラインがあるとします カラーアタッチメントマッピングがない場合 3つのカラーアタッチメントを持つ レンダリングエンコーダを作成します fragment関数が3つの色出力を返し エンコーダがそれらの出力をタイルメモリの 対応するアタッチメントに送ります 次の描画呼び出しでは 別の出力の書き込みを行う パイプラインが必要になる場合があります アタッチメントが異なるため 色出力に応じた新しいレンダリング エンコーダを作成する必要が生じます カラーアタッチメントマッピングがあれば 2つ目のエンコーダは必要ありません レンダリングエンコーダに両方のパイプラインで 必要なカラーアタッチメントがあります カラーアタッチメントマップでシェーディング 出力が特定のアタッチメントに変換されます カラーアタッチメントマップを実装するには カラーアタッチメントマップをサポートする RenderPassDescriptorを用意します 次に エンコーダで使用される アタッチメントのスーパーセットを作成します エンコーダが描画する アタッチメントを設定するには カラーアタッチメントマップを作成し 再マッピングエントリを設定します エントリごとに シェーダ出力を指定する 論理インデックスと アタッチメントのインデックスを指定する 物理インデックスを設定します エンコーディング前にマッピングオブジェクトを 構築し 各フレームで再利用します レンダリングパイプラインの設定時にカラー アタッチメントマップもバインドします パイプラインで別のアタッチメントの 描画を行う場合は 別のカラーアタッチメントマップに 切り替えます カラーアタッチメントマッピングにより ゲーム内のレンダリングエンコーダの数を 大幅に削減できます レンダリングパスが減ることで エンコーディングのオーバーヘッドが減り GPUの効率性が向上します
Metal 4ではメモリ割り当ての 制御も強化されています
コマンドアロケータにより コマンドバッファメモリを再利用でき エンコーディング時の動的割り当てが 避けられます コマンドをエンコードするほど アロケータのメモリ消費量が増えます 関連付けられたGPUコマンドの実行が 完了したら アロケータをリセットします それにより 以降のコマンドエンコーディングで コマンドのメモリを再利用できます 複数のアロケータを使うことで GPUの処理完了まで エンコーディングがブロックされるのを 回避できます 新しいコマンドアロケータでエンコードすると エンコードするコマンド用の メモリが割り当てられます このコマンドのメモリは GPUで実行される処理であるため コミットされた処理が完了するまで 待機してからリセットします GPIの処理が完了したら コマンドアロケータをリセットします これにより即座にメモリに 再利用可能のマーキングがされます GPUの処理の実行中に エンコーディングを続けるには 2つ目のコマンドアロケータを使用します GPU処理の完了までエンコーディングが ブロックされるのを回避できます GPUの処理が完了したら コマンド アロケータをリセットすることが重要です リセットしないと エンコーディング対応で アロケータのメモリ消費量が増加します コマンドアロケータでエンコードする処理が それ以上ない場合は 解放することでメモリ消費量を削減できます コマンドアロケータは スレッドセーフではないため スレッドごとに別のアロケータを使用します これは シーンのエンコーディングを 並列処理する際に重要です Metal 4のコマンドバッファでは 複数の スレッドにエンコーディングを分割できます シングルスレッドのエンコーディングでは 一連のコマンドを1つ以上の コマンドバッファに順次エンコードします Appleシリコンのパワフルな マルチコアCPUを利用すれば コマンドアロケータの異なる複数のスレッドで 複数のコマンドバッファを使用できます 柔軟性が向上したMetal 4の 演算エンコーダを使用して ブリットやディスパッチ アクセラレーション構造の処理の エンコーディングを均等に 分散させることができます コマンドバッファのエンコードが完了したら 1回のコミット呼び出しで すべてを送信できます Metal 4では 複数のレンダリング エンコーダを1つのパスとして GPUにコミットすることもできます エンコードに時間がかかる レンダリングパスがあるとします デフォルトでは エンコーディングを 別々のレンダリングエンコーダに分割すると GPUはそれらを 個別のレンダリングパスとして実行します パスごとに中間結果の保存と読み込みの オーバーヘッドが発生します
Metal 4のSuspendとResumeのオプションでは 複数のレンダリングエンコーダをマージできます 1つのコマンドバッファの レンダリングエンコーダを一時停止して 別のコマンドバッファで再開します コマンドバッファのエンコードが完了したら 1回のコミット呼び出しで順次送信します 1回のコミットで複数の レンダリングエンコーダを送信すると 複数のレンダリングパスがマージされます この実装には suspendingオプションを 使って 最初のエンコーダを作成します Metalにより このエンコーダが 後続のエンコーダとマージされます エンコーダごとに 異なるコマンドバッファを使用します 真ん中のエンコーダには resumingと suspendingの両方のオプションがあります 最後のエンコーダは resumingオプションのみで作成します エンコーディングした3つのコマンド バッファをすべて一緒にコミットします
これでレンダリングパスをマージできました Metal 4では エンコーディングの 効率を上げるために エンコーダの数を減らし コマンドメモリを再利用し 複数のスレッドで エンコーディングすることができます Metal 4のコマンドエンコーディングの 詳細については Apple Developerの記事をご覧ください 効率的なエンコーディングについて 説明したので 効率的なリソース管理に進みましょう Metal 4のいくつかの新機能は 大規模なリソース管理に役立ちます 引数テーブルと常駐セットを使うと リソースバインディングを リソース数千規模まで拡大できます Metal 4では デベロッパが ドローアブルリソースを管理する必要があり 依存関係を制御できます キューバリアにより 大規模な リソースの依存関係を表現できます テクスチャビュープールと配置スパースヒープは 大規模なリソースに必要な メモリの管理に役立ちます シェーダが複雑化すると 通常は 大量のリソースに対応するために バインドレスモデルが適します 1つの引数バッファで シェーダからバッファ テクスチャ、サンプラ パイプライン状態などを含む 数千のリソースにアクセスできます ただし ルートレベルのリソースのバインドには インデックス付きバインドポイントを使います
インデックスでリソースをバインドするには 引数テーブルを使用します エンコードでは 次の描画または ディスパッチで使用する 引数テーブルを設定します これらのリソースをインデックス付き 関数引数としてシェーダで使用できます Metalは 描画時とディスパッチ時に 引数を収集します そのため描画呼び出しと描画呼び出しの間に 安全に 新しいリソースを設定して インデックスをバインドできます 1つの引数テーブルを複数の エンコーダステージに設定できます
エンコーディング前に 引数テーブルを作成すると リソースバインディングを クリティカルパスから除外できます 1つの引数テーブルを複数の エンコーダにアタッチできます 引数テーブルを引数バッファと併用すると リソースバインディングのニーズに 応じたスケーリングができます シェーダでこれらのリソースに アクセスするための次のステップは GPUの可視化です GPUでリソースが必要な場合は そのリソースを常駐セットに追加します リソースにはパイプラインやバッファ テクスチャ、ドローアブルが含まれます 常駐セットにより 複数のリソースを グループ化して 一度に可視化できます コミットするコマンドバッファに または コマンドキューに直接 アタッチします 常駐セットに時間の経過に伴う変化が 少ない場合は コマンドキューに アタッチするのがよいでしょう 常駐セットに頻繁に変更がある場合は 適切なコマンドバッファにアタッチします GPU用に大規模なリソースを準備するには 時間がかかる場合があります Metalに セットのリソースをあらかじめ 常駐させることができます 望ましいのは 少数の常駐セットに 多くのリソースを持たせることです これにより Metalでリソースを一括処理でき パフォーマンスが向上します 常駐セットの詳細については Apple Developerの記事と 昨年の「Port advanced games to Apple Platforms」をご覧ください Metal 4では リソース常駐の制御が ゲームのドローアブルサーフェスにも 適用されます ゲームのレンダリングされたコンテンツを ディスプレイに送信するには CAMetalLayerのドローアブルサーフェスに レンダリングします Metalの各レイヤーは 動的な常駐セットを保持します これをコマンドキューに追加すると レイヤーの全テクスチャを常駐させられます 常駐セットの追加は一度だけ行います 必要があれば CAMetalLayerによって 更新されます Metal 4では レンダリングを ドローアブルと同期させる必要もあります 各フレームでは 次のドローアブルの取得後 ドローアブルへのレンダリング前の コマンドキューでの待機をエンコードします 次に レンダリング処理をコミットした後 キューのドローアブルへのシグナルを エンコードします
presentを呼び出して レンダリング完了後の フレームの内容をディスプレイに送信します トラッキングのオーバーヘッドを減らすため Metal 4では デベロッパが リソースを同期させる必要があります 先ほど エンコーダでパスバリアを 使用する方法について説明しました キューバリアでは 同じキューの エンコーダ間の データの依存関係を表現します
バリアでは Metalのステージごとに フィルタリングされます エンコーダ内の各コマンドは 1つ以上の 実行ステージに関連付けられます 例えば レンダリングエンコーダの 描画呼び出しでは 頂点とフラグメントのシェーディング ステージが生成されます AppleシリコンのGPUは頂点の処理を すべて一括で処理してから フラグメントシェーディングの処理を行います Metal 4の演算コマンドはディスパッチ、ブリット アクセラレーション構造の ステージに対応しています 過剰な同期を避けるため 適切なステージを 選択することが重要です この例では 演算パスでカーネルディスパッチの 大気シミュレーションを実行します 結果をメモリ内のテクスチャに書き込みます レンダリングパスでシーンを描画します フラグメントシェーディングでは シミュレーション結果と照明のブレンドが 必要ですが 頂点の処理は演算処理と オーバーラップできる必要があります シミュレーション結果へのアクセスを 同期させるには キューのディスパッチステージから レンダリングエンコーダの フラグメントステージへのバリアを エンコードします この例を実装するには まず 演算エンコーダのディスパッチを エンコードします 次に レンダリングコマンドエンコーダで バリアをキューステージのディスパッチの後 フラグメントステージの前に追加します バリアの後 描画呼び出しを エンコードできます Metalでは これまでのエンコーダの ディスパッチステージの処理が完了するまで 現在および今後のレンダリングエンコーダの フラグメントステージの処理は実行されません
バリアに最適な位置を見つけられるように Metalデバッガでは バリアの位置と そのバリアが適用されるエンコーダ およびステージが表示されます これを利用して データの依存関係を 維持しながら 最大限に並行処理を行います
Metalのバリアを使ってリソースを 同期させる方法の詳細については Apple Developerサイトの 記事をご覧ください
テクスチャとバッファのストリーミングでは 多数のリソースの メモリ使用量を管理できます Metal 4では バッファとテクスチャを 効率的にストリーミングできます 軽量なテクスチャビューを作成し メモリリソース使用量を管理するには 配置スパースを使用します 最新のゲームではフレームごとに 数百単位のテクスチャや テクスチャバッファビューが 作られることがあります テクスチャビュープールでは すべてのテクスチャビューを格納するのに 必要なメモリの事前割り当てができます その後 プールの任意のインデックスで 軽量なテクスチャビューを作成できます 動的な割り当ては行われないため エンコーディングの際に作成できます テクスチャビューのリソースIDを使って 引数バッファまたは引数テーブルに バインドします 実装方法を説明します エンコーディングの前に テクスチャビュープールを作成します この場合 テクスチャビュープールに500の テクスチャビュー用のメモリが割り当てられます エンコーディングでは テクスチャビュープールの対象の インデックスにテクスチャビューを設定します 返されたMTLResourceIDを使って テクスチャビューを引数テーブルにバインドします バインドが必要なリソースのメモリ使用量が 大きい場合があります 一度にすべてをメモリに収めきれないような 高精細のリソースには スパースリソースが適しています リソースの作成とメモリの バッキングが分離されます 配置スパースでは ヒープのページに対する リソースのマッピングを デベロッパが管理します リソースのメモリマッピングを更新する際 Metal 4のコマンドキューに対するAPIで その更新とほかのGPU処理を同期できます 配置ヒープ内のメモリは 一連のタイルとして扱われます スパースバッファとスパーステクスチャへの タイルの割り当てはデベロッパが管理します スパースリソースのメモリを確保するには バイト範囲またはピクセル領域を スパースタイルにマッピングします
配置ヒープを作成する際には リソースに必要な スパースページサイズを考慮します ページサイズが大きいと マッピングとマッピング解除の処理で パフォーマンス上のメリットがありますが パディングとアラインメントに 使用するメモリ量が増えます ヒープでは 指定した最大値までの スパースページサイズがサポートされます この例では 最大ページサイズとして 64 KBを指定しています 配置ヒープを作成したら スパースリソースを作成できます 配置スパースバッファとテクスチャの 作成は Metalデバイスから行います スパースリソース以外の場合と同様です バッファの場合は要求するバッファサイズを スパースタイルのサイズの倍数に合わせます デバイスには この変換を実行するための クエリが用意されています 長さを指定して新規バッファを呼び出すとき または テクスチャ記述子で placementSparsePageSizeを設定します このプロパティにより 配置ヒープが メモリバッキングを提供することが Metalデバイスに伝わります 配置スパースバッファを初めて作成する場合 メモリバッキングはありません タイルをバッファ範囲に割り当てるには マッピング更新処理を使用します 配置ヒープからバッファにタイルを 割り当てるには まず マッピングの更新操作を指定します 開始オフセットと長さおよび ヒープのタイルオフセットを指定して このバッファ範囲に割り当てます Metal 4のコマンドキューで マッピング処理を送信します
スパースリソースの操作の詳細については Apple Developer Webサイトの 記事をご覧ください 最新のゲームにおけるもう1つの課題は パイプライン状態の 大規模なライブラリの管理です その説明はYangにお願いします ありがとうございます 最新のゲームでは 複雑で動的な ビジュアルを生み出すために 多数のパイプラインを作成する 必要があります シェーダコンパイルの遅延をなくし ゲームの読み込み時間を短縮するために 多数のパイプラインを迅速に 読み込むことが重要です Metal 4でパイプラインを 迅速に読み込むには レンダリングパイプラインの コンパイルを再利用します 進化した並列処理を活かして デバイス上の パイプラインをコンパイルすることもできます さらに一歩進んで 事前にパイプラインをコンパイルすることで パイプラインの読み込み時間を ほぼゼロに短縮できます 柔軟なレンダリングパイプライン状態を持つ レンダリングパイプラインのコンパイルを 再利用する方法の説明から始めます 都市建設ゲームを制作しているとします プレイヤーはマップ上に家を配置できます プレイヤーが家をどこに置くかを決める際 家をホログラムスタイルで レンダリングする必要があり 追加のブレンド状態を持つ パイプラインが必要になります プレイヤーが家を配置すると 家の建築が始まります 建築の進捗を示すために 透明度を持たせて家をレンダリングするには 透明のブレンド状態を持つ 別のパイプラインが必要です 最後に 家の建築が完了したら 不透明のブレンド状態を持つ 3つ目の パイプラインで家をレンダリングします 作成時にすべてのパイプライン設定を指定し これら3つをすべての状態を持つ パイプラインとして コンパイルすることもできます vertex関数とfragment関数 不透明、透明、ホログラムの家用の カラーアタッチメントの設定から始めます ここでのカラーアタッチメントの設定は 記述子の一部を指し カラーアタッチメントへのフラグメント シェーダ出力の書き込みに影響します これにはアタッチメントのピクセル形式 書き込みマスク、ブレンド状態が含まれます vertex関数とfragment関数、不透明の設定を 参照するレンダリングパイプライン記述子を 作成します この記述子を使って 不透明のパイプラインを作成し 頂点のバイナリと フラグメントのバイナリボディ フラグメント出力部を含めます 記述子のカラーアタッチメントの 設定を入れ替えることで 透明とホログラムのパイプラインも 同様に作成できます これら3つのパイプラインの バイナリはほぼ同じで フラグメント出力部のみが異なります CPUのタイムラインの観点では 完全な不透明のパイプライン 透明のパイプライン、ホログラムの パイプラインの順にコンパイルします CPUは フラグメント出力部を除いて ほぼ同じパイプラインの 再コンパイルに多くの時間を費やします Metal 4では パイプラインのコンパイルの 大部分を再利用でき それには 最初に汎用型パイプラインを作成します そのうえで 異なるカラーアタッチメントの 設定を使用して 最終的に必要な 特化型パイプラインを実現します これにより レンダリングパイプラインの コンパイル時間を大幅に短縮できます これを実現するには まず 汎用型パイプラインを作成します 同じ記述子で始めますが 実際のカラーアタッチメントの 設定を指定せずに 各フィールドをunspecializedに設定します それには すべてのカラーアタッチメント 記述子をループして pixelFormat、riteMask、blendingStateに 適切なunspecialized値を 設定するだけです unspecializedのパイプラインには 頂点バイナリと フラグメントバイナリボディ デフォルトのフラグメント出力部が含まれます デフォルトのフラグメント出力で うまくいく場合もありますが 多くの場合は特化型の パイプラインに置き換える必要があります 特化型パイプラインを作成するには 汎用型パイプラインと新しいレンダリング パイプライン記述子から始めます ここでは 記述子のカラーアタッチメントの 設定に 実際に必要な値を設定します 特化型パイプラインには デフォルト フラグメント出力の代わりに 対応するフラグメント出力を含めます この新しいフラグメント出力は 非常に高速に生成でき シェーダのコンパイル処理を すべてやり直す必要はありません パイプラインを透明専用の 特化型にする例に戻ります まず 記述子の中の 汎用型のプロパティの設定から始めます ブレンディング状態を有効にし ブレンディングのサブ状態を設定します このコードでは乗算済みアルファブレンドを 行うようパイプラインを設定します 次に 特化型パイプラインを インスタンス化するために 新しい記述子を使用し unspecializedPipelineを指定します ゲーム内で膨大なステートフルレンダリング パイプラインを作成する場合もあります 読み込み時間を最大限に短縮するには すべてのレンダリングパイプラインを unspecializedで作成し 必要に応じて後で特化型にします そのうえで 小さなGPUパフォーマンスの オーバーヘッドがある場合があります オーバーヘッドの多くは共有フラグメント ボディの不要な処理によるものです 例えば フラグメントシェーダが 4つのカラーチャネルに書き込み カラーアタッチメントには チャネルが1つしかない場合 コンパイラでは 未使用チャネルを最適化できません フラグメントバイナリボディから フラグメント出力部へのジャンプによっても 小さなオーバーヘッドが生じます 通常は小さなオーバーヘッドですが フラグメントシェーダによっては大きくなります 重要なシェーダを特定し バックグラウンドで すべての状態を持たせて コンパイルすることにより 読み込み時間の短縮と 優れたフレームレートを両立できます InstrumentsのMetalシステムトレースでは 特化型フラグメントシェーダの 負荷の高さの順位がわかります まとめると 柔軟性の高い レンダリングパイプライン状態を ゲームに取り入れる最適な方法はこうです レンダリングパイプラインはすべて 汎用型でコンパイルし 必要に応じて特化型にします 著しいパフォーマンスの低下がある場合は InstrumentsのMetalシステムトレースで ゲームのプロファイリングを行い 重要なパイプラインを特定します 重要なパイプラインは バックグラウンドで ステートフルバージョンをコンパイルし 準備ができたら 特化型バージョンと 置き換えて使用します 柔軟なレンダリングパイプライン状態の 詳細については Apple Developer Webサイトの こちらの記事をご覧ください
柔軟なレンダリングパイプライン状態により パイプラインのコンパイルを再利用したら パイプラインの読み込み時間短縮のため デバイスのコンパイルを並列処理します ゲームプレイ中にシングルスレッドで パイプラインを読み込むゲームもあります 1つのコンパイル用スレッドで ゲームで使うパイプラインをビルドします こちらはレンダリングのスレッドで エンコーディングなどの反復的な フレームレンダリング処理を実行します 必要なパイプラインの準備が間に合わないと ゲームがもたつくことがあります 別のコンパイルスレッドを追加することで パイプラインの読み込みを高速化できます パイプラインのコンパイルが 短時間で完了します ただし スレッドの優先度に注意しないと 表示間隔に合わなくなるおそれがあります バックグラウンドでコンパイルされる スレッドの優先度を レンダリングスレッドよりも 低い値に設定すると 問題は解消されます ゲームをスムーズに楽しめるようになります こちらが ゲームにマルチスレッドでの コンパイルを追加する方法です Metal 4 compilation APIを使用します Metal 4ではさらに広く コンパイラを使用できます ゲームのアーキテクチャへの適合性に応じて Grand Central Dispatchを使用するか 独自のスレッドプールを作成できます どちらを選択しても 適切な優先度を設定する必要があります Metalでは コンパイルタスクの 優先度が考慮されます Grand Central Dispatchはマルチスレッドで コンパイルを実行する最も簡単な方法です コンパイルで呼び出し元スレッドの優先度を 継承するには コンパイラで用意された 非同期メソッドを指定して ディスパッチグループを使用します 非同期メソッドには完了ハンドラがあります Metalでは これらのメソッドが 自動的に並行実行されます コンパイルの優先度をカスタマイズするには 並行ディスパッチキューを作成し カスタムのQoSクラスを使用します パイプラインのプレウォーミングと ストリーミングにはデフォルト設定が推奨です ディスパッチキューに コンパイルタスクを送信するには ブロック内で同期メソッドを呼び出し dispatch_asyncでキューに送信します 同期メソッドには完了ハンドラがありません
ゲームのアーキテクチャに適している場合 独自のスレッドプールを 作成することもできます Metalデバイスの maximumConcurrentCompletionTestCount プロパティをスレッドプールの スレッド数として使用します デフォルトのスレッド数を2に設定します これがこのプロパティをサポートしていない OSの並行処理の最大数です ゲーム内の他の重要スレッドの リソースが枯渇しないように コンパイル用スレッドに適切な QoSクラスを設定することも重要です パイプラインのプレウォーミングとストリーミングでは QoSクラスをデフォルトに設定します 以上です これで スレッドプールへの コンパイルタスクの送信を開始できます パイプラインのコンパイルの並列処理と 優先順位付けの最適な方法については Apple Developer Webサイトの こちらの記事をご覧ください デバイスでのマルチスレッドコンパイルにより コンパイル時間を大幅に短縮できます さらにゼロに近づけるには 開発時にパイプラインを事前コンパイル するのが最善の方法です パイプラインを事前にコンパイルするには 通常 ゲームでエンドツーエンドの ワークフローを使用します ワークフローではまず インストゥルメンテーションを使用して ゲームを実行し ゲーム内で使用する パイプラインの設定を取得します 取得した結果は GPUバイナリをビルド するためにGPUツールチェーンに渡されます 最後に 実行時に 事前コンパイルされたGPUバイナリが検索され パイプラインが迅速にビルドされます Metal 4では これまでになく簡単に パイプライン設定をオンラインで収集し ゲーム内の事前コンパイルされた バイナリを検索できます Metal 4でパイプラインの設定を 収集する最も簡単な方法は パイプラインスクリプトのシリアル化です パイプラインスクリプトは JSON形式のファイルです デバイスで作成するパイプライン記述子の テキスト表現が含まれます パイプラインスクリプトの シリアル化は簡単で Metal 4のパイプラインデータセット シリアライザを使用します このオブジェクトを コンパイラにバインドすると 作成されたパイプラインの記述子が 自動的に記録されます 次に これらの記述子をシリアル化して パイプラインスクリプトに書き込みます パイプラインデータセットシリアライザを 作成するには 記述子から始めます configurationに CaptureDescriptorsを設定します これにより パイプライン記述子の 追跡のみを行うようシリアライザに伝わり メモリ使用量が減ります パイプラインデータセットシリアライザの 作成には シリアライザ記述子を使います 次に シリアライザを コンパイラの作成に使用する コンパイラ記述子にアタッチします コンパイラを作成したら それを使用して 通常通りパイプラインを作成できます 使用するパイプライン記述子が シリアライザによって自動的に記録されます 収集が完了したら serializeAsPipelinesScriptWithErrorを呼び出し 記録されたパイプラインをシリアル化して パイプラインスクリプトに書き込みます 戻り値はNSDataです 任意の方法で これを 開発システムに送り返すことができます この例では ディスク上の ファイルに書き込みます ファイルのサフィックスを mtl4-jsonに設定します これは GPUツールチェーンで 求められるサフィックスです パイプラインの設定を取得できたら 次のステップはバイナリのビルドです パイプライン設定スクリプトとMetal IR ライブラリの内容をmetal-ttに入力します GPUバイナリがMetalアーカイブに 格納されて出力されます 取得したパイプラインスクリプトの内容を metal-ttに入力する前に スクリプトを開いて Metal IRライブラリへのパスを編集し 開発システム上のパスと一致させます パイプライン設定スクリプトの 形式の詳細については このコマンドで マニュアルページを開いてください 画面上でmetal-ttコマンドを実行すると iOS用のアーカイブをビルドできます バイナリは事前コンパイルしているので ゲームの実行時にそれを 見つける必要があります Metal 4では これまでより簡単に アーカイブ内のGPUバイナリから パイプラインを作成できます デバイス上でのコンパイルに使用するのと 同じ記述子を使用して パイプライン状態を取得します 例えば MTL4Archiveオブジェクトを 作成するには アーカイブのURLを指定します 次に パイプライン記述子を指定して アーカイブオブジェクトから直接 パイプライン状態を照会します
アーカイブの検索は様々な理由で 失敗することがあります 一致するパイプラインがない OSに互換性がない GPUアーキテクチャに 互換性がない などです Metal 4では デベロッパが これらの失敗に対処する必要があります この例では 単にオンデバイスコンパイルに フォールバックしており ゲームで必要なパイプライン状態が 使用できるようにしています
こちらは 先ほどのサンプルゲームの CPUタイムラインで マルチスレッドのオンデバイス コンパイルを使用しています 事前コンパイルを採用することで パイプラインの読み込み時間を ほぼゼロに短縮できます 事前コンパイルの詳細については Apple Developer Webサイトの こちらの記事をご覧ください ここからは まとめです Metal 4では これまで以上に高速に パイプライン状態を読み込む方法があります パイプラインを特化型にすることで コンパイル結果を再利用できます マルチスレッド化により コンパイルをさらに高速化できます パイプラインの読み込み時間を 最短にするには 事前コンパイルとともに 効率的な収集と 検索のワークフローを採用します Metal 4 APIを使って 次世代の パフォーマンスに優れたゲームを 開発するさまざまな方法を お伝えできて嬉しく思います 新しいXcodeをダウンロードして ゲームのエンコーディング、リソース管理 パイプラインの読み込みの 最適化を始めましょう 皆さんにお役立ていただけるよう サンプルプロジェクトや 詳細な記事を用意しています Metal 4について さらに詳しく知りたい場合は このシリーズの他のビデオもご覧ください ご視聴ありがとうございました
-
-
0:01 - Synchronize access to a buffer within an encoder
// Synchronize access to a buffer within an encoder id<MTL4ComputeCommandEncoder> encoder = [commandBuffer computeCommandEncoder]; [encoder copyFromBuffer:src sourceOffset:0 toBuffer:buffer1 destinationOffset:0 size:64]; [encoder barrierAfterEncoderStages:MTLStageBlit beforeEncoderStages:MTLStageDispatch visibilityOptions:MTL4VisibilityOptionDevice]; [encoder setComputePipelineState:pso]; [argTable setAddress:buffer1.gpuAddress atIndex:0]; [encoder setArgumentTable:argTable]; [encoder dispatchThreads:threadsPerGrid threadsPerThreadgroup:threadsPerThreadgroup]; [encoder endEncoding];code snippet.
-
4:29 - Configure superset of color attachments
// Configure superset of color attachments MTL4RenderPassDescriptor *desc = [MTLRenderPassDescriptor renderPassDescriptor]; desc.supportColorAttachmentMapping = YES; desc.colorAttachments[0].texture = colortex0; desc.colorAttachments[1].texture = colortex1; desc.colorAttachments[2].texture = colortex2; desc.colorAttachments[3].texture = colortex3; desc.colorAttachments[4].texture = colortex4;
-
4:38 - Set color attachment map entries
// Set color attachment map entries MTLLogicalToPhysicalColorAttachmentMap* myAttachmentRemap = [MTLLogicalToPhysicalColorAttachmentMap new]; [myAttachmentRemap setPhysicalIndex:0 forLogicalIndex:0]; [myAttachmentRemap setPhysicalIndex:3 forLogicalIndex:1]; [myAttachmentRemap setPhysicalIndex:4 forLogicalIndex:2];
-
4:57 - Set a color attachment map per pipeline
// Set a color attachment map per pipeline [renderEncoder setRenderPipelineState:myPipeline]; [renderEncoder setColorAttachmentMap:myAttachmentRemap]; // Draw with myPipeline [renderEncoder setRenderPipelineState:myPipeline2]; [renderEncoder setColorAttachmentMap:myAttachmentRemap2]; // Draw with myPipeline2
-
8:03 - Encode a single render pass with 3 render encoders
// Encode a single render pass with 3 render encoders with suspend/resume options id<MTL4RenderCommandEncoder> enc0 = [cmdbuf0 renderCommandEncoderWithDescriptor:desc options:MTL4RenderEncoderOptionSuspending]; id<MTL4RenderCommandEncoder> enc1 = [cmdbuf1 renderCommandEncoderWithDescriptor:desc options:MTL4RenderEncoderOptionResuming | MTL4RenderEncoderOptionSuspending]; id<MTL4RenderCommandEncoder> enc2 = [cmdbuf2 renderCommandEncoderWithDescriptor:desc options:MTL4RenderEncoderOptionResuming]; id<MTL4CommandBuffer> cmdbufs[] = { cmdbuf0, cmdbuf1, cmdbuf2 }; [commandQueue commit:cmdbufs count:3]
-
11:48 - Synchronize drawable contents
// Synchronize drawable contents id<MTLDrawable> drawable = [metalLayer nextDrawable]; [queue waitForDrawable:drawable]; // ... encode render commands to commandBuffer ... [queue commit:&commandBuffer count:1]; [queue signalDrawable:drawable]; [drawable present];
-
13:25 - Encode a queue barrier to synchronize data
// Encode a queue barrier to synchronize data id<MTL4ComputeCommandEncoder> compute = [commandBuffer computeCommandEncoder]; [compute dispatchThreadgroups:threadGrid threadsPerThreadgroup:threadsPerThreadgroup]; [compute endEncoding]; id<MTL4RenderCommandEncoder> render = [commandBuffer renderCommandEncoderWithDescriptor:des]; [render barrierAfterQueueStages:MTLStageDispatch beforeStages:MTLStageFragment visibilityOptions:MTL4VisibilityOptionDevice]; [renderCommandEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:vertexStart vertexCount:vertexCount]; [render endEncoding];
-
14:57 - Create a texture view pool
// Create a texture view pool MTLResourceViewPoolDescriptor *desc = [[MTLResourceViewPoolDescriptor alloc] init]; desc.resourceCount = 500; id <MTLTextureViewPool> myTextureViewPool = [myDevice newTextureViewPoolWithDescriptor:myTextureViewPoolDescriptor error:nullptr];
-
15:07 - Set a texture view
// Set a texture view MTLResourceID myTextureView = [myTextureViewPool setTextureView:myTexture descriptor:myTextureViewDescriptor atIndex:5]; [myArgumentTable setTexture:myTextureView atIndex:0];
-
16:01 - Choose appropriate sparse page size
MTLHeapDescriptor *desc = [MTLHeapDescriptor new]; desc.type = MTLHeapTypePlacement; desc.storageMode = MTLStorageModePrivate; desc.maxCompatiblePlacementSparsePageSize = MTLSparsePageSize64; desc.size = alignedHeapSize; id<MTLHeap> heap = [device newHeapWithDescriptor:desc];
-
17:05 - Update buffer mappings
// Update buffer mappings MTL4UpdateSparseBufferMappingOperation bufferOperation; bufferOperation.mode = MTLSparseTextureMappingModeMap; bufferOperation.bufferRange.location = bufferOffsetInTiles; bufferOperation.bufferRange.length = length; bufferOperation.heapOffset = heapOffsetInTiles; [cmdQueue updateBufferMappings:myBuf heap:myHeap operations:&bufferOperation count:1];
-
20:41 - Set unspecialized configuration
// In MTL4RenderPipelineColorAttachmentDescriptor // Set unspecialized configuration pipelineDescriptor.colorAttachments[i].pixelFormat = MTLPixelFormatUnspecialized; pipelineDescriptor.colorAttachments[i].writeMask = MTLColorWriteMaskUnspecialized; pipelineDescriptor.colorAttachments[i].blendingState = MTL4BlendStateUnspecialized;
-
21:40 - Create a specialized transparent pipeline
// Create a specialized transparent pipeline // Set the previously unspecialized properties pipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm; pipelineDescriptor.colorAttachments[0].writeMask = MTLColorWriteMaskRed | MTLColorWriteMaskGreen | MTLColorWriteMaskBlue; pipelineDescriptor.colorAttachments[0].blendingState = MTL4BlendStateEnabled; pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorOne; pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd; id<MTLRenderPipelineState> transparentPipeline = [compiler newRenderPipelineStateBySpecializationWithDescriptor:pipelineDescriptor pipeline:unspecializedPipeline error:&error]; // Similarly, create the specialized opaque and hologram pipelines
-
26:22 - Determine thread count
// Determine thread count NSInteger numThreads = 2; if (@available(macOS 13.3, iOS 19, visionOS 3, tvOS 19, *)) { numThreads = [device maximumConcurrentCompilationTaskCount]; }
-
26:30 - Set a proper QoS class for your compilation threads
// Create thread pool for (NSInteger i = 0; i < numThreads; ++i) { // Creating a thread with a QoS class DEFAULT pthread_attr_set_qos_class_np(&attr, QOS_CLASS_DEFAULT, 0) ; pthread_create(&threadIds[i], &attr, entryPoint, NULL); pthread_attr_destroy(&attr); }
-
28:24 - Harvest pipeline configuration scripts
// Harvest pipeline configuration scripts with the pipeline data set serializer // Create a pipeline data set serializer that only captures descriptors MTL4PipelineDataSetSerializerDescriptor *desc = [MTL4PipelineDataSetSerializerDescriptor new]; desc.configuration = MTL4PipelineDataSetSerializerConfigurationCaptureDescriptors; id<MTL4PipelineDataSetSerializer> serializer = [device newPipelineDataSetSerializerWithDescriptor:desc]; // Set the pipeline data set serializer when creating the compiler MTL4CompilerDescriptor *compilerDesc = [MTL4CompilerDescriptor new]; [compilerDesc setPipelineDataSetSerializer:serializer]; id<MTL4Compiler> compiler = [device newCompilerWithDescriptor:compilerDesc error:nil]; // Create pipelines using the compiler as usual // Serialize the descriptors as a pipeline script NSData *data = [serializer serializeAsPipelinesScriptWithError:&err]; // Write the pipeline script data to disk NSString *path = [NSString pathWithComponents:@[folder, @"pipelines.mtl4-json"]]; BOOL success = [data writeToFile:path options:NSDataWritingAtomic error:&err];
-
30:28 - Query pipeline state from MTLArchive
// Query pipeline state from MTLArchive id<MTL4Archive> archive = [device newArchiveWithURL:archiveURL error:&error]; id<MTLRenderPipelineState> pipeline = [archive newRenderPipelineStateWithDescriptor:descriptor error:&error]; if (pipeline == nil) { // handle lookup miss pipeline = [compiler newRenderPipelineStateWithDescriptor:descriptor compilerTaskOptions:nil }
-