ハイライト
これは、Mac へのゲーム移植の 3 部作の最終章です。レンダラーを Metal に移植する方法について説明しており、リソース バインディングと永続性、コマンド送信の最適化、間接レンダリング、および MetalFX 超解像度という 4 つの主要トピックをカバーしています。
主な内容
ルート署名から引数バッファまで
Direct3D は、ルート署名を使用してリソース バインディングを記述します。一般的なルート シグネチャには、テクスチャ記述子テーブル、バッファ パラメータ、32 ビット定数、およびサンプラー記述子テーブルの 4 つのエントリがあります。
Metal の引数バッファはより柔軟で、混合型要素をサポートします。ただし、エンジンが同種の配列を想定している場合は、コーディングも簡単です。
エンコードされたテクスチャ記述子テーブル:
// 在渲染循环之外创建纹理表
id<MTLBuffer> textureTable = [device newBufferWithLength:sizeof(MTLResourceID) * texturesCount
options:MTLResourceStorageModeShared];
MTLResourceID* textureTableCPUPtr = (MTLResourceID*)textureTable.contents;
for (uint32_t i = 0; i < texturesCount; ++i) {
// 创建纹理
id<MTLTexture> texture = [device newTextureWithDescriptor:textureDesc[i]];
// 把纹理的 GPU 资源 ID 存入表
textureTableCPUPtr[i] = texture.gpuResourceID;
}
重要なポイント:
MTLResourceIDMetal 3で導入されたリソース識別子です- テクスチャ テーブルは、レンダリング ループではなく、初期化フェーズ中に作成されます。
gpuResourceIDテクスチャのGPU側識別子を直接取得します。
(03:55)
エンコーディング サンプラー記述子テーブル:
id<MTLBuffer> samplerTable = [device newBufferWithLength:sizeof(MTLResourceID) * samplersCount
options:MTLResourceStorageModeShared];
MTLResourceID* samplerTableCPUPtr = (MTLResourceID*)samplerTable.contents;
for (uint32_t i = 0; i < samplersCount; ++i) {
MTLSamplerDescriptor* desc = [MTLSamplerDescriptor new];
desc.supportArgumentBuffers = YES; // 必须设置
// ... 配置其他采样器属性
id<MTLSamplerState> sampler = [device newSamplerStateWithDescriptor:desc];
samplerTableCPUPtr[i] = sampler.gpuResourceID;
}
重要なポイント:
supportArgumentBuffers = YESサンプラーを引数バッファーに入れることができることが前提条件です。- テクスチャ テーブルと同様に、サンプラー テーブルも初期化フェーズ中に作成されます。
(04:33)
最上位の引数バッファのエンコード:
struct TopLevelAB {
MTLResourceID* textureTable;
float* myBuffer;
uint32_t myConstant;
MTLResourceID* samplerTable;
};
id<MTLBuffer> topAB = [device newBufferWithLength:sizeof(TopLevelAB)
options:MTLResourceStorageModeShared];
TopLevelAB* topABCPUPtr = (TopLevelAB*)topAB.contents;
topABCPUPtr->textureTable = (MTLResourceID*)textureTable.gpuAddress;
topABCPUPtr->myBuffer = (float*)myBuffer.gpuAddress;
topABCPUPtr->myConstant = 128;
topABCPUPtr->samplerTable = (MTLResourceID*)samplerTable.gpuAddress;
重要なポイント:
- 最上位構造はルート署名の 4 つのエントリに対応します
gpuAddressサブテーブルのGPUアドレスを取得します。- レンダリング ループ内でこの最上位バッファをバインドするだけです。
[encoder setVertexBuffer:topAB offset:0 atIndex:0]
(05:05)
リソース常駐管理
バインドレス リソースでは、常駐を明示的に管理する必要があります。主な推奨事項:
読み取り専用リソース: 大きなヒープに分類され、エンコーダーごとに 1 回呼び出されますuseHeap。
// 创建 Heap
MTLHeapDescriptor* heapDesc = [MTLHeapDescriptor new];
heapDesc.size = requiredSize;
heapDesc.type = MTLHeapTypeAutomatic;
id<MTLHeap> heap = [device newHeapWithDescriptor:heapDesc];
// 从 Heap 分配纹理和缓冲区
id<MTLTexture> texture = [heap newTextureWithDescriptor:desc];
id<MTLBuffer> buffer = [heap newBufferWithLength:length options:options];
// 渲染时一次性标记整个 Heap 驻留
[encoder useHeap:heap];
重要なポイント:
- ヒープのハザード追跡モードが Untracked に設定されている
- すべての読み取り専用リソースは 1 つのヒープから割り当てられます
- エンコーダーごとに 1 回のみ
useHeap電話
(06:49)
書き込み可能なリソース: 個別に割り当てられ、使用しますuseResourceMetal に同期を処理させるには、使用フラグを指定します。
// 单独分配可写资源
id<MTLTexture> textureRW = [device newTextureWithDescriptor:desc];
id<MTLBuffer> bufferRW = [device newBufferWithLength:length options:options];
// 标记资源驻留,指定读写标志
[encoder useResource:textureRW usage:MTLResourceUsageWrite stages:stage];
[encoder useResource:bufferRW usage:MTLResourceUsageRead stages:stage];
重要なポイント:
- 書き込み可能なリソースをヒープに置かないでください
- 使用
useResource読み取りおよび書き込みの意図を明示的に指定する - Metal は、エンコーダー間のハザード追跡と同期を自動的に処理します。
(07:34)
コマンド送信の最適化
Apple GPU は、ユニファイド メモリとオンチップ タイル メモリを備えた TBDR (Tile-Based Deferred Renderer) アーキテクチャです。Metal はパスの概念を使用しており、コマンドをタイプごとにグループ化する必要があります。
最適化前の問題シーケンス: クリア → 描画 → コピー → 計算 → 描画。このシーケンスでは、タイル メモリとシステム メモリの間で 5 回の往復が行われます。
最適化の手順:
- レンダリングが開始される前にコピー操作を移動します
- コマンドを種類ごとにグループ化: 描画コマンドと計算コマンドをまとめます。
- 同じレンダー ターゲットを共有するパスをマージします
- 空のエンコーダー クリアの代わりに LoadActionClear を使用します
- ストア アクションの最適化: 必要な場合にのみレンダー ターゲット コンテンツを保存します。
最適化後は最後のフラッシュが 1 つだけ残り、メモリ帯域幅が大幅に減少します。
(10:53)
Metal Debugger は最適化の機会を自動的に発見します
Xcode の Metal Debugger は、概要ビューアの Insights セクションに、メモリ、帯域幅、パフォーマンス、API 使用量の 4 つのカテゴリに分けて最適化の提案をリストします。
例: GBuffer Pass には、必要以上の添付ファイルが保存されます。アルベド テクスチャは後続のフレームでは使用されず、ストア操作は冗長です。修正するには、ストア アクションを DontCare に設定します。
別の例: 2 つのパス GBuffer と Forward は、同じ添付ファイルの読み取りと書き込みを行うため、マージできます。マージ後に帯域幅を節約します。
依存関係ビューアは、パス間のデータ フローを表示し、ロード/ストア アクションとマージの機会を確認できます。
(15:06)
##詳細
間接レンダリング
ExecuteIndirectは複数の描画コマンドのパラメータをバッファに格納し、GPUがバッファからパラメータを読み出して描画を実行します。これにより、GPU は何をレンダリングするかを決定できるようになり、GPU 駆動のレンダリング ループを実装するための鍵となります。
Metal には 2 つの変換方法があります。
方法 1: 間接的に描画
// 把 ExecuteIndirect 翻译成一系列 drawIndexedPrimitives 调用
uint32_t drawArgumentsBufferOffset = 0;
for (uint32_t i = 0; i < maxDrawCount; ++i) {
[renderEncoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle
indexType:MTLIndexTypeUInt16
indexBuffer:indexBuffer
indexBufferOffset:indexBufferOffset
indirectBuffer:drawArgumentsBuffer
indirectBufferOffset:drawArgumentsBufferOffset];
drawArgumentsBufferOffset += sizeof(MTLDrawIndexedPrimitivesIndirectArguments);
}
重要なポイント: -各描画コマンドは個別にコーディングされています
- オフセットによる異なる間接パラメータの読み取り
- 実装が簡単で、ほとんどのシナリオに適しています
(19:31)
方法 2: 間接コマンド バッファ (ICB)
シーンに何千もの描画コマンドがあり、CPU エンコード時間がボトルネックになっている場合は、ICB を使用します。ICB は、パイプライン状態やバッファ バインディングの設定などのコマンドを GPU 上でエンコードします。
// 计算内核:把间接绘制参数翻译成 ICB 命令
kernel void translateToICB(device const Command* indirectCommands [[buffer(0)]],
device const ICBContainerAB* icb [[buffer(1)]],
...)
{
device const Command* indirectCommand = &indirectCommands[commandIndex];
device const MTLDrawIndexedPrimitivesIndirectArguments* args =
&command->mdiBuffer[mdiIndex];
render_command drawCall(icb->buffer, indirectCommand->mdiCmdStart + mdiIndex);
if (args->indexCount > 0 && args->instanceCount > 0) {
encodeCommand(indirectCommand, args, drawCall);
} else {
cmd.reset();
}
}
void encodeCommand(device const Command* indirectCommand,
device const MTLDrawIndexedPrimitivesIndirectArguments* args,
thread render_command& drawCall)
{
drawCall.set_render_pipeline_state(indirectCommand->pso);
for (ushort i = 0; i < indirectCommand->vertexBuffersCount; ++i) {
drawCall.set_vertex_buffer(indirectCommand->vertexBuffer[i].buffer,
indirectCommand->vertexBuffer[i].slot);
}
drawCall.draw_indexed_primitives(primitive_type::triangle,
args->indexCount,
indirectCommand->indexBuffer + args->indexStart,
args->instanceCount,
args->baseVertex,
args->baseInstance);
}
重要なポイント:
- ICB コマンドは GPU 上でエンコードされ、CPU オーバーヘッドを削減します。
- 状態変化ごとに間接実行コマンドを分割する必要がありません
- 既存の間接パラメータ生成シェーダを再利用可能
(21:48)
MetalFX 超解像度
MetalFX を使用すると、ゲームを低解像度でレンダリングしてから、ターゲット解像度にオーバーサンプリングして、フレームあたりの GPU 時間を節約できます。
MetalFX は 2 つのアルゴリズムをサポートしています。
- 空間: 最高のパフォーマンス
- 時間: ネイティブ解像度のレンダリングに近い品質
今年の新機能: iOS サポート、最大 3 倍のスーパーサンプリング、Metal-cpp サポート。
MetalFX を統合するには以下が必要です。 1.超解像エンジンに対応 2. レンダラは、テクスチャ サンプリングの詳細レベルを手動で制御します。 3. 時間的スーパーサンプリングにはディザ シーケンスと動きベクトルが必要です (TAA がすでに存在する場合はすでに利用可能です)
(22:54)
重要ポイント
-
何をすべきか: 引数バッファを使用して既存のルート署名バインディング モデルを移植します。
-
実行する価値がある理由: Metal 3 の Argument Buffer はパフォーマンスが高く、Direct3D の記述子テーブルの概念を完全にマッピングできます。
-
開始方法: 初期化フェーズ中にテクスチャ テーブルとサンプラー テーブルをエンコードし、レンダリング ループはトップレベルの引数バッファのみをバインドします。
-
何をすべきか: Metal Debugger の Insights 機能を使用して、帯域幅最適化の機会を自動的に発見します。
-
実行する価値がある理由: どのパスをマージできるか、どのストア操作が冗長であるかを直感的に確認できます。
-
開始方法: Xcode で Metal ワークロードをキャプチャし、概要ビューアの Insights セクションを表示します。
-
対処法: CPU エンコーディングのボトルネック シナリオで間接コマンド バッファーを使用する
-
実行する価値がある理由: コマンド エンコーディングを CPU から GPU に移動し、CPU オーバーヘッドを大幅に削減します。
-
開始方法: 計算カーネルを追加して、間接的な描画パラメータを ICB コマンドに変換し、使用します。
executeCommandsInBuffer埋め込む -
内容: フレーム レートを向上させる統合された MetalFX アップスケーリング
-
価値がある理由: 低解像度でレンダリングし、オーバーサンプリングして Apple Silicon のパフォーマンスを大幅に向上させます。
-
開始方法: エンジンが超解像度と手動 LOD 制御をサポートしていることを確認し、空間アルゴリズムまたは時間アルゴリズムを選択します
関連セッション
- ゲームを Mac に持ち込む、パート 1: ゲーム計画を立てる — Game Porting Toolkit を使用して Windows ゲームの Mac 上での実現可能性を評価する
- ゲームを Mac に持ち込む、パート 2: シェーダーをコンパイルする — Metal Shader Converter を使用して HLSL シェーダーを Metal に変換する
- Metal を使用して GPU レンダラーを最適化 — Metal の最新機能を使用して GPU レンダラーを最適化します。
コメント
GitHub Issues · utterances