WWDC Quick Look 💓 By SwiftGGTeam
Discover compilation workflows in Metal

Discover compilation workflows in Metal

元の動画を見る

ハイライト

メタル拡張ダイナミック ライブラリ、関数ポインタ、バイナリ アーカイブ、プライベート リンク関数、WWDC21 での新しいシェーダ コンパイル プロセスへの関数スプライシングにより、開発者は GPU バイナリを再利用し、実行時にレンダリング パイプラインを拡張し、シェーダ コンパイル コストを削減できます。

主な内容

Metal レンダラーを作成しました。複数のフラグメント シェーダは、次のようなユーティリティ関数の同じバッチを呼び出します。foo()そしてbar()。以前のアプローチは単純で、各パイプラインに実装をコード化していました。プロジェクトが大きくなるにつれて、コンパイルを繰り返すと起動と最初のレンダリングが遅くなり始めます。

もう 1 つの問題は、実行時の構成に起因します。マテリアル、フィルター、ユーザー定義のエフェクトはすべて、アプリの実行中に変更される可能性があります。シェーダーのソースコードが変更されるたびに再アセンブルされ、その後 Metal ソースから AIR にコンパイルされると、ユーザーは待たされることになり、開発者がキャッシュの粒度を制御することが困難になります。

Apple はこのセッションでいくつかの道筋を示しています。

固定ツールの機能をダイナミック ライブラリ (Dynamic Library) に組み込むことができます。その後、パイプラインの作成時にそれらをリンクすると、同じ GPU バイナリで複数のレンダリング、タイル、コンピューティングのワークロードを処理できます。

呼び出しターゲットは実行時に置き換える必要があり、関数ポインターと可視関数テーブルを使用できます。シェーダーは関数のシグネチャのみを知っており、どの関数が実際に呼び出されるかは、CPU にバインドされたテーブルによって決まります。

コンパイル コストはスタートアップ間で再利用する必要があり、バイナリ アーカイブを使用できます。パイプラインとバイナリ関数ポインタのコンパイル結果をディスクに保存し、次回起動時にアーカイブを直接チェックします。

現在、シェーダーの生成に文字列のスプライシングに依存している場合は、関数ステッチの方がより安定したアプローチを提供します。プリコンパイルされたものを置きます[[stitchable]]関数は有向非巡回グラフを形成し、Metal は AIR レイヤーで新しい関数を生成し、Metal フロントエンドの実行時のオーバーヘッドをスキップします。

##詳細

ダイナミック ライブラリ: ツール関数をパイプラインから分離します。

(04:44) ダイナミック ライブラリのプロセスは AIR から始まります。 Xcode の Metal ツールチェーンを使用して、ビルド時に AIR を生成したり、実行時に呼び出すことができます。newLibraryWithSource。 AIR を取得したら、次を使用します。newDynamicLibraryを使用して動的ライブラリを作成します。serializeToURLディスクに書き込んで次回使用するnewDynamicLibraryWithURL再利用。

id<MTLLibrary> airLibrary = [device newLibraryWithSource:source
                                                 options:nil
                                                   error:&error];

id<MTLDynamicLibrary> dynamicLibrary = [device newDynamicLibrary:airLibrary
                                                           error:&error];

[dynamicLibrary serializeToURL:libraryURL
                         error:&error];

id<MTLDynamicLibrary> cachedLibrary = [device newDynamicLibraryWithURL:libraryURL
                                                                  error:&error];

キーポイント:

  • newLibraryWithSourceトランスクリプトに記載されている、実行時にソースから AIR にコンパイルされるエントリ ポイントに対応します -newDynamicLibraryコンパイルされた Metal ライブラリを GPU バイナリ形式のダイナミック ライブラリに変換します。 -serializeToURL次回の起動時に再利用できるように、ダイナミック ライブラリをディスクに書き込みます -newDynamicLibraryWithURL以前に保存したダイナミック ライブラリをディスクからロードします

(05:38) シェーダーでは外部関数のみが宣言され、実装は提供されません。この実装は、パイプラインの作成時にロードされる動的ライブラリから取得されます。

// Declare external functions

extern float4 foo(FragmentInput input);
extern float4 bar(FragmentInput input);

// Use functions in shader

fragment float4 main(FragmentInput input [[stage_in]])
{
    switch(condition(input))
    {
        case 0:
            return foo(input);
        case 1:
            return bar(input);
    }
}

キーポイント:

  • extern float4 foo(...)関数シグネチャを宣言します。現在のシェーダー ファイルは関数本体を提供しません。 -extern float4 bar(...)外部シンボルもあり、後でダイナミック ライブラリによって解決されます。 -mainはフラグメント シェーダです。入力は次のとおりです。[[stage_in]]
  • switch(condition(input))どの外部関数を呼び出すかを決定する -return foo(input)そしてreturn bar(input)別の動的ライブラリをロードすることで、実行時に実装を置き換えることができます

(06:04) ステージに対応するプリロードされたライブラリ配列にダイナミック ライブラリを追加した後、シンボルは配列順に解析されます。フラグメント、頂点、タイルなどのステージには対応する属性があります。

renderPipeDesc.fragmentPreloadedLibraries = @[cachedLibrary];

キーポイント:

  • fragmentPreloadedLibrariesフラグメントステージに対応
  • 動的ライブラリによって配列で提供されますfoo()bar()このタイプextern機能実装
  • トランスクリプトは、シンボルが配列に追加された順序で解析されることを明示的に示しています

関数ポインター: シェーダーが実行時に呼び出しターゲットを選択できるようにします。

(09:01) 関数ポインターの最初のステップは、呼び出し可能な関数をインスタンス化することです。パイプラインの作成を高速化するために、関数をバイナリ形式にコンパイルできます。

// Declare a descriptor and set CompileToBinary options

MTLFunctionDescriptor* functionDescriptor = [MTLFunctionDescriptor new];
functionDescriptor.options = MTLFunctionOptionCompileToBinary;

// Backend compile the function

functionDescriptor.name = @"foo";
id<MTLFunction> foo = [library newFunctionWithDescriptor:functionDescriptor
                                                     error:&error];

キーポイント:

  • MTLFunctionDescriptorライブラリから取得する関数を記述します -MTLFunctionOptionCompileToBinaryGPU バイナリを生成するにはバックエンド コンパイラが必要 -functionDescriptor.name = @"foo"関数名を指定してください -newFunctionWithDescriptor:error:記述子に基づいて作成するMTLFunction

(09:30) パイプライン記述子を作成するときは、現在のステージでどの関数を呼び出すことができるかを Metal に伝える必要があります。 AIR 関数とバイナリ関数は異なる配列で動作します。

// Provide a list of functions that the pipeline stage may call

// AIR functions

renderPipeDesc.fragmentLinkedFunctions.functions = @[foo, bar, baz];

// Binary functions

renderPipeDesc.fragmentLinkedFunctions.binaryFunctions = @[foo, bar, baz];

キーポイント:

  • fragmentLinkedFunctionsフラグメントステージにバインドされた呼び出し可能な関数のコレクション -functions関数を AIR 形式にすると、コンパイラーは AIR レイヤーで静的リンクと最適化を実行できます。 -binaryFunctionsパイプラインの作成時間を短縮するためにバックエンドでコンパイルされた関数を配置する
  • トランスクリプトでは、バイナリ関数に複雑な呼び出しチェーンがある場合、スタックのオーバーフローを避けるために最大呼び出しスタックの深さを正しく設定する必要があると述べました。

(10:47) パイプラインが作成されたら、表示される関数テーブルを作成し、関数ハンドルをテーブルに配置します。

// Create visible function table

id<MTLVisibleFunctionTable> visibleFunctionTable =
    [renderPipeline newVisibleFunctionTableWithDescriptor:tableDescriptor
                                                    stage:MTLFunctionStageFragment];

// Create function handles

id<MTLFunctionHandle> fooHandle =
    [renderPipeline functionHandleWithFunction:foo
                                         stage:MTLFunctionStageFragment];

// Insert handles into table

[visibleFunctionTable setFunction:fooHandle
                           atIndex:0];

キーポイント:

  • newVisibleFunctionTableWithDescriptor:stage:指定したステージの関数テーブルを作成します -functionHandleWithFunction:stage:パイプラインからステージ固有の関数ハンドルを取得します。 -setFunction:atIndex:ハンドルを関数テーブルスロットに書き込みます
  • テーブルとハンドルの両方が特定のパイプラインと特定のステージにバインドされています

(11:21) レンダリング時に、CPU 側は関数テーブルをバインドし、シェーダー側は関数テーブルをバッファー パラメーターとして使用します。

// Bind visible function table objects to each stage

[renderCommandEncoder setFragmentVisibleFunctionTable:visibleFunctionTable
                                        atBufferIndex:0];
// Usage in shader

fragment float4 shaderFunc(FragmentData vo[[stage_in]],
                           visible_function_table<float4(float3)>materials[[buffer(0)]])
{
    // ...

    return materials[materialSelector](coord);
}

キーポイント:

  • setFragmentVisibleFunctionTable:atBufferIndex:関数テーブルをフラグメントステージのバッファインデックスにバインドします。 -visible_function_table<float4(float3)>関数テーブル内の各関数のシグネチャを宣言します。 -materials[[buffer(0)]]CPU側でatBufferIndex:0対応する -materials[materialSelector](coord)インデックスで関数を選択して呼び出します

インクリメンタル パイプライン: 後でバイナリ関数を追加します

(12:20) パイプラインを作成した後、新しい関数を呼び出す必要があることがわかるかもしれません。bat()。完全な記述子を使用して 2 番目のパイプラインを再作成すると、パイプラインのコンパイルがトリガーされます。 Metal を使用すると、まずこのパイプラインが将来拡張できることを宣言し、その後、古いパイプラインに基づいて新しいパイプラインを迅速に作成できます。

// Enable incrementally adding binary functions per stage

renderPipeDesc.supportAddingFragmentBinaryFunctions = YES;

// Create render pipeline functions descriptor

MTLRenderPipelineFunctionsDescriptor extraDesc;
extraDesc.fragmentAdditionalBinaryFunctions = @[bat];

// Instantiate render pipeline state

id<MTLRenderPipelineState> renderPipeline2 =
  [renderPipeline1 newRenderPipelineStateWithAdditionalBinaryFunctions:extraDesc
                                                                 error:&error];

キーポイント:

  • supportAddingFragmentBinaryFunctions = YES元のパイプライン記述子のフラグメント ステージを拡張可能であると宣言します。 -MTLRenderPipelineFunctionsDescriptor新しい関数コレクションについて説明する -fragmentAdditionalBinaryFunctions = @[bat]新しいバイナリ関数のみを追加します -newRenderPipelineStateWithAdditionalBinaryFunctions:error:に基づくrenderPipeline1含むものを作成するbat新しいパイプラインの状態

バイナリ アーカイブ: コンパイル結果をディスクに保存します。

(14:15) WWDC20 のパイプラインにはバイナリ アーカイブが使用されました。 WWDC21 では、可視関数と交差関数をアーカイブに保存する機能が追加されました。関数を保存するときに、addFunctionWithDescriptor、関数記述子とソース ライブラリを渡します。

[archive addFunctionWithDescriptor:functionDescriptor
                            library:library
                              error:&error];

functionDescriptor.binaryArchives = @[archive];

id<MTLFunction> foo = [library newFunctionWithDescriptor:functionDescriptor
                                                     error:&error];

キーポイント:

  • addFunctionWithDescriptor:library:error:関数のバイナリコンパイル結果をアーカイブに追加します -functionDescriptor.binaryArchives = @[archive]後続の関数作成時に最初にアーカイブをチェックするようにします -newFunctionWithDescriptor:error:コンパイルされた関数がアーカイブ内で見つかった場合は、それが直接返されます。
  • トランスクリプトでは、アーカイブがレンダリング、タイル、計算パイプライン、およびバイナリ関数ポインターを同時に保存できると述べました

(14:55) ルックアップ ルールはエラー処理にも影響します。 Metal はまずバイナリ アーカイブ リストをチェックします。見つからない場合はもう一度探してくださいCompileToBinaryオプション;バイナリ コンパイルが必要な場合は、パイプライン オプションも確認してください。FailOnBinaryArchiveMiss

functionDescriptor.options = MTLFunctionOptionCompileToBinary;
functionDescriptor.binaryArchives = @[archive];

id<MTLFunction> foo = [library newFunctionWithDescriptor:functionDescriptor
                                                     error:&error];

キーポイント:

  • binaryArchivesバイナリ関数を検索するためのアーカイブ リストです -MTLFunctionOptionCompileToBinaryアーカイブミス後もバイナリ形式が必要であることを示します
  • トランスクリプトの説明、FailOnBinaryArchiveMiss実行時にコンパイルするか、アーカイブミスが発生したときに戻るかを決定します。nil

プライベートリンク機能: 最適化の余地を残す

17:45linkedFunctions内部functionsそしてprivateFunctionsすべては AIR 層で静的にリンクされます。違いは視認性です。privateFunctions関数ハンドルを生成することも、表示される関数テーブルに配置することもできません。これらは、コンパイラーがより完全に最適化できるパイプラインの内部実装です。

renderPipeDesc.fragmentLinkedFunctions.privateFunctions = @[foo, bar];

キーポイント:

  • privateFunctions現在のパイプラインによって内部的にのみ呼び出される外部関数に適しています
  • AIR 層で静的にリンクされます。
  • プライベート関数の関数ハンドルを作成できません
  • トランスクリプトには、この機能は AIR レイヤーで動作するため、macOS Monterey および iOS 15 のすべてのデバイスで利用できることが記載されています

関数のステッチング: グラフを使用してランタイム関数を生成する

(19:03) 関数のスプライシングにより、計算グラフから関数が生成されます。グラフは有向非巡回グラフです。入力ノードは生成された関数のパラメーターを表し、関数ノードは関数呼び出しを表します。データ エッジはデータ フローを表し、コントロール エッジは呼び出しシーケンスを表します。

[[stitchable]] int FunctionA(device int*, int) {…}
[[stitchable]] int FunctionC(int, int) {…}

[[stitchable]]
int ResultFunction(device int* Input0,
                   int Input1,
                   int Input2)
{
  int N0 = FunctionA(Input0, Input1);
  int N1 = FunctionA(Input0, Input2);
  int N2 = FunctionC(N0, N1);
  return N2;
}

キーポイント:

  • [[stitchable]]マーカー関数は関数ステッチング API で使用できます -FunctionAそしてFunctionCライブラリ内に事前に存在するスプライス可能な関数です -ResultFunctionスプライシング結果がどのメタル関数に相当するかを表示します
  • トランスクリプトでは、実際のステッチングが Metal フロントエンドをスキップして、AIR レイヤー上に直接ライブラリを生成すると説明しています。

(21:32) Objective-C 側は、まず入力ノードを作成し、次に関数ノードを作成し、最後にグラフを作成します。

// Create input nodes

inputs[0] = [[MTLFunctionStitchingInputNode alloc] initWithArgumentIndex:0];

// Create function nodes

n0 = [[MTLFunctionStitchingFunctionNode alloc] initWithName:@"FunctionA"
                                                  arguments:@[inputs[0], inputs[1]]
                                        controlDependencies:@[]];
n1 = [[MTLFunctionStitchingFunctionNode alloc] initWithName:@"FunctionA"
                                                  arguments:@[inputs[0], inputs[2]]
                                        controlDependencies:@[]];
n2 = [[MTLFunctionStitchingFunctionNode alloc] initWithName:@"FunctionC"
                                                  arguments:@[n0, n1]
                                        controlDependencies:@[]];

// Create graph

graph = [[MTLFunctionStitchingGraph alloc] initWithFunctionName:@"ResultFunction"
                                                          nodes:@[n0, n1]
                                                     outputNode:n2
                                                     attributes:@[]];

キーポイント:

  • MTLFunctionStitchingInputNode引数インデックスを使用して、生成された関数のパラメータをマップします。 -MTLFunctionStitchingFunctionNode関数名とパラメータリストを使用して関数呼び出しを表します -controlDependencies:@[]ここに追加の実行順序制約がないことを示します -MTLFunctionStitchingGraph生成関数名、ノードリスト、出力ノード、属性を指定します。

(22:18) グラフを取得したら、ステッチされたライブラリ記述子を作成し、そこにスプライシング可能な関数とグラフを入れて、生成された関数を新しいライブラリから取り出します。

// Configure stitched library descriptor

MTLStitchedLibraryDescriptor* descriptor = [MTLStitchedLibraryDescriptor new];

descriptor.functions      = @[stitchableFunctions];
descriptor.functionGraphs = @[graph];

// Create stitched function

id<MTLLibrary> lib = [device newLibraryWithDescriptor:descriptor
                                                error:&error];

id<MTLFunction> stitchedFunction = [lib newFunctionWithName:@"ResultFunction"];

キーポイント:

  • MTLStitchedLibraryDescriptorステッチされたライブラリを作成するためのエントリ ポイントです -functionsグラフ内で呼び出すことができるステッチ可能な関数を配置します。 -functionGraphs生成する関数グラフを入れる -newLibraryWithDescriptor:error:記述子に基づいてライブラリを生成する -newFunctionWithName:@"ResultFunction"ステッチされた関数を取り出します。これは、パイプライン、関数ポインター、または他のステッチ グラフで引き続き使用できます。

重要なポイント

  1. やるべきこと: 3D エディター用のランタイム マテリアル システムを作成します。 実行する価値がある理由: 関数ポインターによりシェーダーが通過できるようになります。visible_function_tableマテリアル関数の呼び出しとマテリアルの切り替えには、パイプライン全体を再構築する必要はありません。 開始方法: さまざまな材料関数をコンパイルします。MTLFunction、合格newVisibleFunctionTableWithDescriptor:stage:を使用してテーブルを作成しますsetFunction:atIndex:ハンドルを書き込み、シェーダーで使用しますmaterials[materialSelector](coord)電話。

  2. やるべきこと: フィルター アプリ用にユーザーが組み合わせ可能なエフェクト チェーンを作成します。 実行する価値がある理由: 関数ステッチングによりプリコンパイル済みの変換が可能[[stitchable]]実行時に Metal ソース文字列のスペルを避けるために、関数は図に従って結合されます。 開始方法: 基本フィルターを次のように記述します。[[stitchable]]機能、使い方MTLFunctionStitchingInputNodeそしてMTLFunctionStitchingFunctionNode写真を組み立てて再利用するMTLStitchedLibraryDescriptor関数を生成します。

  3. やるべきこと: ゲーム エンジン用のシェーダ ツール ライブラリのキャッシュを作成します。 実行する価値がある理由: 動的ライブラリは、ヘルパー関数を独立した GPU バイナリにコンパイルでき、レンダリング、タイル、およびコンピューティング パイプラインで共有できます。 開始方法: ツール関数を AIR にコンパイルし、呼び出しますnewDynamicLibrary動的ライブラリを生成するには、次を使用します。serializeToURLディスクにドロップし、起動時に渡しますnewDynamicLibraryWithURLステージに対応するプリロードされたライブラリをロードして配置します。

  4. やるべきこと: 初めてシーンに入るときのラグを軽減します。 実行する価値がある理由: Binary Archive は、パイプラインとバイナリ関数ポインターのコンパイル結果を保存し、次回起動時のシェーダーのコンパイル時間とコンパイル メモリのコストを削減できます。 開始方法: 作成するMTLBinaryArchive、使用addFunctionWithDescriptor:library:error:プレフィル機能、アーカイブをディスクに書き込みます。ロード時に設定されるfunctionDescriptor.binaryArchives次に関数を作成します。

  5. 機能: プラグイン レンダラ サポート ユーザーにシェーダ機能を提供します。 これを実行する価値がある理由: 動的ライブラリを使用すると、ユーザーはコンパイル済みの実装を提供でき、メイン シェーダーはその実装のみを保持します。extern宣言;関数ポインターを使用すると、パイプラインはコンパイル時に表示されない関数を呼び出すことができます。 開始方法: 合意された関数署名、メイン シェーダーによって使用されますexternまたはvisible_function_tableコールポイントを宣言し、ユーザーダイナミックライブラリまたはバイナリ関数をロードして、パイプラインにバインドします。

関連セッション

コメント

GitHub Issues · utterances