ハイライト
Metal 3 では、バインドレス レンダリングで引数バッファと無制限の配列に直接書き込み、ヒープからレイ トレーシング アクセラレーション構造を割り当て、シェーダ検証と Xcode 14 ツールを通じて欠落した永続性と誤ったリソース アクセスを特定できるようになります。
主要内容
従来のバインド モデルは、リソースをパイプラインの固定スロットに 1 つずつバインドします。シーンが複雑になると、シェーダーはメッシュ、マテリアル、テクスチャ、スカイボックス、レイ トレーシング データにアクセスする必要があり、CPU 側は多数のバインディング呼び出しを維持する必要があります。
バインドレス レンダリングでは、その構成方法が変更されます。まずリソース参照をメモリに集約し、次に 1 つのバッファのみをバインドします。シェーダは、引数バッファ内のポインタとリソース ID に沿って必要なデータを見つけることができます。 (00:59)
これはレイ トレーシングにとって重要です。反射シェーダーは、床、トラック、マテリアル、空などの多くのアセットにアクセスする必要があります。これらのリソースを引数バッファに配置すると、レイ トレーシング シェーダは実行時にオンデマンドでデータをフェッチできるようになります。 (01:32)
Metal 3 は、バインドレスの使用に伴うエンジニアリング コストを解決します。これにより、引数バッファを C 構造体のように記述することができます。これにより、境界のない配列を通常の構造体配列と同様に埋めることができます。それは許可しますMTLHeapレイ トレーシング アクセラレーション構造を割り当てます。また、リソースが GPU メモリに存在しない場合、シェーダ検証レイヤーがエラーを直接報告できるようにします。 (02:02)
詳細
引数バッファを直接書き込む
(04:00) Metal 2 時代では、引数バッファを書き込むには、通常、最初に引数エンコーダを作成する必要がありました。このエンコーダは、シェーダ関数のリフレクションによって作成することも、構造体メンバを手書きで記述することもできます。マルチスレッドを使用する場合は特に注意してください。
Metal 3 では、このステップが C 構造体の作成に簡素化されています。 CPU 側はバッファーの内容を取得して構造体ポインターにキャストし、リソースの GPU アドレスまたはリソース ID を書き込みます。 Metal はこれらの参照を理解し、その効果は、過去に Argument Encoder を使用して参照をエンコードしたのと同等です。 (04:09)
// Write argument buffers in Metal 3
struct Mesh
{
uint64_t normals; // 64-bit uint for constant packed_float3*
};
NSUInteger meshArgumentSize = sizeof(struct Mesh);
id<MTLBuffer> meshArgumentBuffer = [device newBufferWithLength:meshArgumentSize
options:storageMode];
struct Mesh* meshes = (struct Mesh *)(meshArgumentBuffer.contents);
meshes->normals = normalBuffer.gpuAddress + normalBufferOffset;
キーポイント:
uint64_t normalsシェーダに対応constant packed_float3* normals、CPU 側は 64 ビット GPU アドレスを保存します。 -sizeof(struct Mesh)単一の引数バッファ構造のサイズを直接与えるため、レイアウトを計算するために引数エンコーダが必要なくなりました。 -newBufferWithLength:options:引数バッファの実際の内容を作成するMTLBuffer。meshArgumentBuffer.contents存在するManagedまたはSharedCPU 書き込み可能なポインタはストレージ モードで提供されます。 -normalBuffer.gpuAddress + normalBufferOffsetターゲット バッファの GPU アドレスを構造体メンバーに書き込みます。オフセットは GPU メモリ要件に従って調整する必要があります。
この機能には、デバイスが引数バッファ層 2 をサポートする必要があります。セッションによって指定されるスコープは、2016 以降の Mac、および A13 Bionic 以降の iOS デバイスです。 (04:37)
共有ヘッダー ファイルを使用して CPU/GPU 構造の一貫性を保つ
(06:32) 引数バッファの構造は、シェーダ側とホスト側でまったく同じようには見えません。シェーダ側はポインタ型を記述し、ホスト側は次のように記述する必要があります。uint64_t。 Metal 3 では、構造体のサイズとアライメントが Clang および Metal シェーダ コンパイラで一致していることが保証されますが、宣言自体は依然として 2 回書かれる傾向があります。
公式の例では、最初に 2 つのステートメントが示されています。
// Shader struct:
struct Mesh
{
constant packed_float3* normals;
};
// Host-side struct:
struct Mesh
{
uint64_t normals;
};
キーポイント:
- シェーダ側
constant packed_float3*通常の配列にアクセスするときに GPU によって使用されるポインターのタイプです。 - ホスト側
uint64_t引数バッファに書き込むときに CPU によって使用されるアドレス表現です。 - 両側のフィールドの順序は一貫している必要があります。一貫していないと、シェーダーによって読み取られるアドレスがずれてしまいます。
より安全なアプローチは、次のように宣言を共有ヘッダー ファイルに置くことです。__METAL_VERSION__コンパイル環境を区別します。 (06:53)
// Shared struct:
#if __METAL_VERSION__
#define CONSTANT_PTR(x) constant x*
#else
#define CONSTANT_PTR(x) uint64_t
#endif
struct Mesh
{
CONSTANT_PTR(packed_float3) normals;
};
キーポイント:
__METAL_VERSION__Metal シェーダ コードのコンパイル時にのみ定義されます。- シェーダーがコンパイルされると、
CONSTANT_PTR(packed_float3)に拡張するconstant packed_float3*。 - ホストをコンパイルすると、マクロは次のように展開されます。
uint64_t。 MeshCPU/GPU の両側での構造ドリフトの可能性を減らすために、フィールド リストは 1 つだけ維持されます。
無制限の配列は構造体配列です
(07:18) 通常、バインドレス シーンは 1 つのメッシュだけを書き込みません。シーン全体のメッシュ配列、マテリアル配列、テクスチャ配列をシェーダーに渡す必要があります。 Metal 3 では、無制限の配列の書き込みも簡素化されます。十分な大きさのバッファを割り当ててから、ループして構造体配列を埋めます。
// Write unbounded arrays of resources in Metal 3
struct Mesh
{
uint64_t normals; // 64-bit uint for constant packed_float3*
};
NSUInteger meshArgumentSize = sizeof(struct Mesh) * meshes.count;
id<MTLBuffer> meshArgumentBuffer = [device newBufferWithLength:meshArgumentSize
options:storageMode];
struct Mesh* meshes = (struct Mesh *)(meshArgumentBuffer.contents);
for ( NSUInteger i = 0; i < meshes.count; ++i )
{
meshes[i].normals = normalBuffers[i].gpuAddress + normalBufferOffsets[i];
}
キーポイント:
sizeof(struct Mesh) * meshes.count無制限の配列が占める実際のスペースを決定します。- シェーダーは宣言で配列サイズをハードコーディングする必要はありません。
-
struct Mesh* meshesバッファの内容に構造体の配列としてアクセスします。 -for各メッシュの通常バッファアドレスを 1 つずつ書き込むループ。 - 各要素は、シーン レベルのリソース テーブルに適した、異なるバッファーとオフセットを指すことができます。
シェーダ側は、この無制限の配列をバッファ パラメータとして渡すことができます。 (09:03)
// Metal shading language:
struct Mesh
{
constant packed_float3* normals;
};
fragment half4 fragmentShader(ColorInOut v [[stage_in]],
constant Mesh* meshes [[buffer(0)]] )
{
/* determine mesh to read, e.g. geometry_id */
packed_float3 n0 = meshes[ geometry_id ].normals[0];
packed_float3 n1 = meshes[ geometry_id ].normals[1];
packed_float3 n2 = meshes[ geometry_id ].normals[2];
/* interpolate normals and calculate shading */
}
キーポイント:
constant Mesh* meshes [[buffer(0)]]引数バッファを次のように考えてください。Mesh配列がシェーダーに渡されました。 -geometry_id現在のフラグメントがどのメッシュにアクセスするかを決定します。 -meshes[geometry_id].normals[0]まずメッシュ配列にインデックスを付けてから、その中の GPU ポインタに沿って法線を読み取ります。- シェーダは、C 配列と同じ方法でこの配列にアクセスします。
別の記述方法は、複数の無制限の配列を 1 つにまとめることです。Sceneシェーダーが 1 つのシーン バッファーのみをバインドできるようにする構造。 (09:25)
// Metal shading language:
struct Mesh
{
constant packed_float3* normals;
};
struct Scene
{
constant Mesh* meshes; // mesh array
constant Material* materials; // material array
};
fragment half4 fragmentShader(ColorInOut v [[stage_in]],
constant Scene& scene [[buffer(0)]] )
{
/* determine mesh to read, e.g. geometry_id */
packed_float3 n0 = scene.meshes[ geometry_id ].normals[0];
packed_float3 n1 = scene.meshes[ geometry_id ].normals[1];
packed_float3 n2 = scene.meshes[ geometry_id ].normals[2];
/* interpolate normals and calculate shading */
}
キーポイント:
Sceneメッシュ配列とマテリアル配列を集約します。- シェーダエントリはのみ受け入れます
constant Scene& scene。 ・アクセスパスは、scene.meshes[geometry_id]、リソースの入り口はより集中しています。 - この編成方法は、引数バッファ内のシーン リソース テーブルの長期保存に適しています。
レイトレーシングアクセラレーション構造をヒープから割り当てます
(10:12) Metal 3 により、レイ トレーシング アクセラレーション構造が可能になります。MTLHeap配布する。このようにして、アクセラレーション構造をバッファやテクスチャとともに管理できます。
メリットは居住者管理においてより直接的です。加速構造のグループをヒープに配置した後、ヒープ全体を一度調整できますuseHeap:、これらのリソースが一緒に存在できるようにします。セッションは、これがリソースごとに呼び出すよりも優れていると明示的に示していますuseResourceレンダリング スレッドの高速化と CPU コストの削減。 (10:26)
heapAccelerationStructureSizeAndAlignWithDescriptor:
キーポイント:
- ヒープからアクセラレーション構造を割り当てる前に、そのサイズとヒープ内での位置合わせをクエリします。
- 使用
MTLDeviceの上heapAccelerationStructureSizeAndAlignWithDescriptor:。 - このクエリは通常とは異なります
accelerationStructureSizesWithDescriptor:。 - ヒープ上でハザード追跡が有効になっていない場合、Metal はヒープ内のリソースの競合状態を自動的に防止しません。アプリケーションは、アクセラレーション ストラクチャの構築とレイ トレーシングの作業をそれ自体で同期する必要があります。
シェーダー検証レイヤーで常駐の欠落をチェックする
(12:06) バインドレスでよくあるバグは、間接アクセス リソースを常駐させるのを忘れることです。リソースが常駐していない場合、レンダリング時に基礎となるメモリ ページが利用できない可能性があり、その結果、コマンド バッファの障害、GPU の再起動、または画像の破損が発生する可能性があります。
Metal 3 のシェーダー検証レイヤーは、コマンド バッファーの実行中にこのようなエラーを報告します。エラー メッセージには、問題を引き起こしたシェーダ関数、パス名、メタル ファイルと行番号、バッファ ラベル、バッファ サイズ、およびリソースが常駐していないという事実が含まれます。 (14:53)
セッションの例は、ハイブリッド レンダリング アプリからのものです。リフレクションでは時々エラーが表示され、最終的にインデックス バッファーは引数バッファーに書き込まれているが、常駐リソース コレクションには追加されていないことがわかります。 (13:01)
// Argument buffer loading
for (NSUInteger i = 0; i < mesh.submeshes.count; ++i) {
Submesh* submesh = mesh.submeshes[i];
id<MTLBuffer> indexBuffer = submesh.indexBuffer;
NSArray* textures = submesh.textures;
// Copy index buffer into argument buffer
submeshAB[i].indices = indexBuffer.gpuAddress;
// Copy material textures into argument buffer
for (NSUInteger m = 0; m < textures.count; ++m) {
submeshAB[i].textures[m] = textures[m].gpuResourceID;
}
// Remember indirect resources
[sceneResources addObject:indexBuffer];
[sceneResources addObjectsFromArray:textures];
}
キーポイント:
indexBuffer.gpuAddress書き込まれるsubmeshAB[i].indicesの場合、シェーダーは引数バッファーを介して間接的にアクセスします。 -textures[m].gpuResourceIDはマテリアル テクスチャ配列に書き込まれ、シェーダもこれらのテクスチャに間接的にアクセスします。 -sceneResourcesヒープによってホストされていないすべての間接リソースを記録します。 -[sceneResources addObject:indexBuffer]これはこのバグの修正です。レンダリング中にインデックス バッファーを渡す前に、インデックス バッファーも記録する必要があります。useResourceマーカーが常駐しています。 -[sceneResources addObjectsFromArray:textures]レイ トレーシングをディスパッチする前にマテリアル テクスチャを常駐させます。
このコードは、デバッグの習慣も提供します。つまり、Metal オブジェクトのラベルを設定します。検証エラーにはラベルが表示されます。Xcode では、これを使用して特定のリソースを見つけることもできます。 (15:20)
ライフサイクルが長いリソースでは自動保持がオフになる場合があります
(16:44) デフォルトでは、Metal コマンド バッファーは使用するリソースへの強い参照を持つため、GPU が使い果たされる前に CPU がオブジェクトを解放できなくなります。このセキュリティ保証には CPU コストがかかります。
バインドレス アプリケーションは多くの場合、リソースをヒープに集約し、ライフ サイクルはレベルまたはシーンと一致することがよくあります。アプリケーションがすでにリソースのライフサイクルを保証できる場合は、非保持参照を使用してコマンド バッファーを作成し、Metal がこれらの参照に対して追加の強力な参照を作成しないようにすることができます。 (18:41)
セッションによって得られた結論は非常に具体的です。マイクロベンチマークでは、非保持参照コマンド バッファーに切り替えるだけで、コマンド バッファーのライフ サイクルにおける CPU 使用率が 2% 削減されます。不要な強い参照を作成したり破棄したりすることがなくなるため、時間が節約されます。 (19:19)
これは強い前提を持った最適化です。これは、GPU の使用中にリソースが解放されないことをアプリケーション自体が保証できる場合にのみ適しています。この設定の粒度はコマンド バッファー全体です。参照されたリソースがすべて保持されるか、まったく保持されません。 (19:09)
追跡されていないリソースにより、ヒープの誤った共有が削減されます
(19:46) Metal は、デフォルトでリソースの危険を追跡し、GPU タイムラインに同期を自動的に挿入して、書き込み後読み取りまたは書き込み後書き込みの問題を回避できます。
ヒープは複数のサブリソースを集約します。 Metal は同じヒープ リソースを認識し、たとえ実際にヒープ内の異なるサブリソースにアクセスしていたとしても、2 つのパス間に依存関係があると保守的に信じている可能性があります。この状況はフォールス シェアリングと呼ばれ、GPU の動作の実時間を増加させます。 (21:38)
解決策はリソース記述子を変更することですhazardTrackingに設定Untrackedその後、アプリケーションは詳細な依存関係を明示的に表現します。 Session 氏は、GPU に作業を並行して実行する機会を増やすために、デフォルトではヒープが追跡されていないと述べました。 (22:10)
利用可能な同期プリミティブはシナリオによって選択されます。
MTLFence: プロデューサがコンシューマより先に送信またはエンキューする、単一のコマンド キュー内の一般的なシナリオに最適で、オーバーヘッドが最小限に抑えられます。 (25:51) -MTLEvent: 送信順序が保証できない場合、または複数のコマンド キュー間の同期が必要なシナリオに適しています。 (26:04) -MTLSharedEvent: Metal デバイス間、または GPU と CPU 間の同期に適しています。 (26:11)- メモリバリア: パス内の同期に適しています。フラグメントステージの後にバリアを使用しないでください。Apple GPU で検証エラーがトリガーされるため、Fence を使用することをお勧めします。 (26:20)
Xcode 14 の依存ビューとリソース リスト
(27:50) バインドレス コードを作成するだけでは、戦いはまだ半分にすぎません。残りの半分は、GPU が実際にどのリソースを認識しているのか、どのパスが同期されスタックしているのかを確認します。
Xcode 14 の Metal Debugger には依存関係ビューアが追加されています。これはワークロードをグラフとして表します。ノードはパスと出力リソースを表し、エッジはパス間のリソースの依存関係を表します。実線はデータ フローを表し、破線は同期を表します。 (28:08)
このツールは、追跡されたヒープによって引き起こされる誤った共有を直接特定できます。セッション デモでは、依存関係ビューアは 2 つのパス間の同期を追加する追跡ヒープを表示しますが、コンピューティング エンコーダーは実際には前のエンコーダーのリソースを使用しません。アプリを追跡されていないヒープに変更し、実際に必要な場所に Fence を挿入すると、2 つのパスを並行して実行できます。 (29:37)
Metal Debugger は、新しいリソース リストも追加します。ドローコールを受け付けますAccessedモードの後は、この描画呼び出しによって実際にアクセスされたリソースとアクセス タイプのみが表示されます。バインドレス シナリオでは、GPU は同時に数百または数千のリソースを参照できます。このフィルタリングにより、シェーダーが実際に影響した少数の問題に問題を絞り込むことができます。 (30:55)
予期しないリソースにアクセスした場合は、シェーダー デバッガーを続行できます。シェーダの実行プロセスとリソースへのアクセスを行ごとに表示するため、シェーダが間違った Argument Buffer 要素にアクセスする問題を特定するのに適しています。 (32:01)
重要ポイント
-
対処方法: マテリアル テクスチャ テーブルをバインドレスな引数バッファに変更します。
実行する価値がある理由: シェーダーはマテリアル インデックスを通じてテクスチャ リソースにアクセスできるため、CPU は描画ごとにマテリアル テクスチャを 1 つずつバインドする必要がありません。
開始方法: 共有マテリアル構造体を定義し、CPU 側に書き込みますgpuResourceID、シェーダ側が通過しますconstant Material*読む。 -
内容: レイ トレーシングの反射パスのシーン レベルを作成します。
Scene構造。
実行する価値がある理由: 反射シェーダーは、メッシュ、マテリアル、テクスチャ、空などのリソースにアクセスする必要があります。単一のシーン バッファーでこれらの入り口を集中管理できます。
開始方法:constant Mesh* meshesそしてconstant Material* materials入れますScene、意思Sceneとして[[buffer(0)]]レイ トレーシングまたはフラグメント シェーダーに渡されます。 -
やるべきこと: 加速構造を同じに配置します。
MTLHeap。
実行する価値がある理由: 1 回だけ使用可能useHeap:リソースのグループ全体が存在する場所にマークを付けて、useResourceCPU のオーバーヘッド。
開始方法: 最初に使用しますheapAccelerationStructureSizeAndAlignWithDescriptor:サイズ/アライメントをクエリし、ヒープの同期戦略に従ってビルドとレイ トレーシング作業を調整します。 -
内容: バインドレス リソース常駐のための自動チェックリストを作成します。
価値がある理由: インデックス バッファーまたはテクスチャが欠落していると、反射の欠落、コマンド バッファーの障害、または画像の破損が発生する可能性があります。
開始方法: 引数バッファを記述する同じコード部分に、各間接アクセス リソースを追加します。NSMutableSet、レンダリング前に統合されるuseResource; 開発中にシェーダ検証レイヤーを開きます。 -
対処方法: 依存関係ビューアを使用して、ヒープが無効な同期を引き起こしていないかどうかを確認します。
実行する価値がある理由: 追跡ヒープにより、フォールス シェアリングにより並列パスがシリアルに実行される可能性があります。
開始方法: Xcode 14 Metal Debugger では、同期の依存関係のみを表示し、追跡されたヒープのドットの依存関係を見つけて、追跡されていないヒープとMTLFence。
関連セッション
- Discover Metal 3 — Metal 3 の概要。今年のグラフィックスとツールチェーンのアップデートの全体像を理解するのに適しています。
- メタル レイ トレーシングのパフォーマンスを最大化する — このセッションで言及されたレイ トレーシングのパフォーマンスとアクセラレーション構造の更新についての詳細な補足。
- Metal 3 で GPU バイナリをターゲットにして最適化する — Metal 3 が GPU バイナリの生成をビルド フェーズに進める方法について説明します。
- Metal 3 でリソースをより高速にロードする - グラフィックスとゲームのリソースが、高速リソースの読み込みを通じて Metal リソースに非同期で入力される方法について説明します。
- metal-cpp を使用した C++ での Metal のプログラム - セッションで言及された Metal オブジェクトのライフサイクルと C++ API の使用法を補足します。
コメント
GitHub Issues · utterances