WWDC Quick Look 💓 By SwiftGGTeam
Bring your game to Mac, Part 3: Render with Metal

Bring your game to Mac, Part 3: Render with Metal

元の動画を見る

ハイライト

これは、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 回の往復が行われます。

最適化の手順:

  1. レンダリングが開始される前にコピー操作を移動します
  2. コマンドを種類ごとにグループ化: 描画コマンドと計算コマンドをまとめます。
  3. 同じレンダー ターゲットを共有するパスをマージします
  4. 空のエンコーダー クリアの代わりに LoadActionClear を使用します
  5. ストア アクションの最適化: 必要な場合にのみレンダー ターゲット コンテンツを保存します。

最適化後は最後のフラッシュが 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 制御をサポートしていることを確認し、空間アルゴリズムまたは時間アルゴリズムを選択します

関連セッション

コメント

GitHub Issues · utterances