3D 低レベル API の違い Direct3D 12/Metal

前回はこちら→ 「3D 低レベル API の現状 Direct3D 12/Metal

新しい API はあらゆる面で負荷の低減が行われています。

・バッファの無駄なコピーの排除
・Command など動的な変換をできるだけ避ける
・GPU との同期も暗黙に行わずアプリケーションに委ねる

自動化されていると便利ですが、アプリケーションによっては内部の仕組みが見えづらく、最適化の妨げになる場合があります。API の低レベル化は、オーバーヘッドを減らすと同時に用途に合わせて最適化が出来る範囲が広がりました。
具体的にどのあたりがこれまでと異なっているのか、いくつかまとめてみます。

● CommandBuffer と CommandQueue (D3D12/Metal)

従来の API では Context に暗黙の CommandBuffer が含まれていました。必要に応じてその都度 Command 生成 (Compile) やバッファ構築が行われており、実行や GPU との同期も表面上見えません。

新しい API では明示的に CommandBuffer (CommandList) を作成します。CommandBuffer はいくつでも生成可能でスレッド化も容易です。実行も直接 Command Queue に登録することで行います。API によっては再利用可能な事前コンパイルされた Buffer を作っておくことも可能です。CommandBuffer の完了は自分で判定する必要があります。

● PipelineState (D3D12/Metal)

以前の API では Context が State を所有していました。State は常に変更される可能性があるため Draw の直前まで内容を確定できません。Draw 命令のタイミングで必要な State を集めて Command 化が行われるため Draw Call API の負担が大きくなります。

新しい API では描画に必要な State の大半を Pipeline State に集約しています。この Object は事前に生成できるので、Command Compile や Error 判定など負荷のかかる処理を予め済ませておくことが可能。Draw Call の負担を大きく減らすことに繋がります。

● Resource Binding Table (D3D12)

Shader に割り当てるリソースのテーブルもこれまでは Context が所有していました。CBV 14個、SRV 128個、Sampler 16個 など API 毎に決められた数のスロットがあります。描画のたびに上書きされるため、Draw 毎に CommandBuffer へのコピーが必要でした。

新しい API では Resource Binding Table (Descriptor Table) もユーザーサイドで用意します。Table のサイズに制限はなくなり API 上の上限は撤廃。任意の部分を Register に割り当てるなどマッピングの自由度も高くなっています。また必要な Table を事前に生成しておけるため動的なコピーも減らせます。

● Resource 同期 (D3D12/Metal)

直前に書き込んだ Buffer を次の描画で Texture 参照する場合など、リソースの依存が発生する場合があります。ShaderUnit の実行は並列化されるので、複数の描画命令が部分的にオーバラップする可能性があるからです。従来の API では依存が発生した場合の完了待ちやキャッシュの同期はドライバの役割でした。

D3D12 ではリソースに State を設けており、読み書きのアクセスタイプが切り替わる場合 Barrier 命令を挿入する必要があります。

Metal では同期よりも PowerVR の Tile を最適化する目的で State (Action) が設けられています。RenderTarget を Texture 参照する場合は Store で、逆に Rendering 前に Tile に書き戻す場合は Load が必要です。本来の目的は違いますが他の GPU では Barrier に相当する役割を担っていると思われます。

● Buffer Renaming (D3D12/Metal)

従来の API では CPU 側から見て簡単に扱えるよう Buffer は複雑な構造を持っていました。同じバッファを部分的に書き換えて何度も描画に用いることができます。これを実現するには内部的にバッファをコピーしたり、描画のたびに異なるバッファを割り当てる Renaming が必要です。

新しい API では GPU/CPU から見える Buffer は常に同一なので、描画アクセス中のバッファ書き換えは結果に影響を与えます。Map() が返すアドレスは常に同じもので Renaming しません (Unmap が必須ではない)。動的に書き換える場合は多重化が必要です。CommandBuffer, Descriptor Table なども同様です。

従来の API でも usage パラメータとしてヒントがありましたが、内部動作の違いはわかりにくいものでした。低レベル API ではこれらの区別を自分で実装することになるため、どこで無駄が生じるのか明快です。個人的にはとてもわかりやすくなったと思っています。

関連エントリ
3D 低レベル API の現状 Direct3D 12/Metal