次の方法で共有


リソース バインドの概要

DirectX 12 のリソース バインドを理解するためのキーは、記述子、記述子テーブル、記述子ヒープ、ルート署名の概念です。

リソースとグラフィックス パイプライン

シェーダー リソース (テクスチャ、定数テーブル、画像、バッファーなど) は、シェーダー パイプラインに直接バインドされるのではなく、"記述子" を介して参照されます。 記述子は小さなオブジェクトであり、1 つのリソースに関する情報が格納されます。

複数の記述子をまとめたものが、"記述子テーブル" です。 各記述子テーブルには、ある範囲のリソースの種類に関する情報が格納されます。 リソースにはさまざまな種類があります。 最も一般的なリソースは次のとおりです。

  • 定数バッファー ビュー (CBV)
  • 順序指定されていないアクセス ビュー (UAV)
  • シェーダー リソース ビュー (SRV)
  • サンプラー

SRV、UAV、および CBV の記述子は、同じ記述子テーブルにまとめることができます。

グラフィックスと計算のパイプラインでは、リソースにアクセスするためにインデックスで記述子テーブルの中を参照します。

記述子テーブルは "記述子ヒープ" に格納されます。 記述子ヒープには、レンダリングされる 1 つまたは複数のフレームのすべての記述子を (記述子テーブルとして) 入れておくのが理想的です。 すべてのリソースはユーザー モード ヒープに格納されます。

もう 1 つの概念として、"ルート署名" があります。 ルート署名とはバインドの規則であり、アプリケーションによって定義され、シェーダーによって使用されます。その目的は、シェーダーがアクセスする必要があるリソースを見つけることです。 ルート署名には次のものを格納できます。

  • 記述子ヒープ内の記述子テーブルのインデックス。ここで記述子テーブルのレイアウトが事前定義されます。
  • 定数。アプリで記述子と記述子テーブルを経由することなく、ユーザー定義定数 (ルート定数 ともいいます) を直接シェーダーにバインドできるようにするためです。
  • ルート署名内に直接存在する、ごく少数の記述子。たとえば、描画ごとに変化する定数バッファー ビュー (CBV) です。これを利用すると、アプリケーションでその記述子を記述子ヒープに入れておく必要がなくなります。

つまり、ルート署名を利用すると、描画ごとに変化するデータが少量の場合のパフォーマンス最適化が可能です。

バインドに関する Direct3D 12 の設計では、バインドがその他のタスク (たとえばメモリ管理、オブジェクト有効期間の管理、状態追跡、メモリの同期) と切り離されています (「Direct3D 11 とのバインド モデルの違い」を参照)。 Direct3D 12 のバインドは低オーバーヘッドとなるように設計されており、呼び出し頻度の高い API に合わせて最適化されています。 また、拡張性の点でも、ローエンドのハードウェアからハイエンドまで対応し、グラフィックス エンジン プログラミングの古いアプローチ (線形の Direct3D 11 パイプライン) にも新しいアプローチ (並列的) にも対応します。

リソースの種類とビュー

リソースの種類は Direct3D 11 と同じです。つまり、次のとおりです。

  • Texture1D、および Texture1DArray
  • Texture2D、および Texture2DArray、Texture2DMS、Texture2DMSArray
  • Texture3D
  • バッファー (型指定、構造化および未加工)

リソース ビューは Direct3D 11 と似ていますが少し異なり、頂点とインデックスのバッファー ビューが追加されています。

  • 定数バッファー ビュー (CBV)
  • 順序指定されていないアクセス ビュー (UAV)
  • シェーダー リソース ビュー (SRV)
  • サンプラー
  • レンダー ターゲット ビュー (RTV)
  • 深度ステンシル ビュー (DSV)
  • インデックス バッファー ビュー (IBV)
  • 頂点バッファー ビュー (VBV)
  • ストリーム出力ビュー (SOV)

これらのビューのうち、最初の 4 つだけが実際にシェーダーに認識されます (「シェーダーに認識される記述子ヒープ」と「シェーダーが認識できない記述子ヒープ」を参照)。

リソース バインド フローの制御

ルート署名、ルート記述子、ルート定数、記述子テーブル、および記述子ヒープのみに注目すると、アプリのレンダリング ロジック フローは次のようになります。

  • 1 つまたは複数のルート署名オブジェクトを作成します (アプリケーションに必要なバインド構成ごとに 1 つ)。
  • シェーダーとパイプラインの状態を、これらとともに使用されるルート署名オブジェクトを指定して作成します。
  • 記述子ヒープを 1 つ (または、必要に応じて複数) 作成します。この中に、レンダリングの各フレームの SRV、UAV、および CBV 記述子がすべて格納されます。
  • 記述子を指定して記述子ヒープを初期化します。これは、一連の記述子を複数のフレームにまたがって再利用する場合に、可能であれば行います。
  • レンダリングされるフレームごとに:
    • コマンド リストごとに:
      • 使用される現行ルート署名を設定します (レンダリング時に必要に応じて変更しますが、変更が必要になることはほとんどありません)。
      • ルート署名の定数やルート署名記述子を、新しいビューに合わせて更新します (たとえばワールド/ビュー射影)。
      • 描画するアイテムごとに:
        • 新しい記述子を記述子ヒープ内に定義します (オブジェクトごとのレンダリングで必要な場合)。 シェーダーから認識可能な記述子ヒープについては、アプリが使用する記述子ヒープ領域が実行中のレンダリングで参照されていないことを保証する必要があります。たとえば、レンダリング中に、記述子ヒープを通して領域を連続的に割り当てるときです。
        • ルート署名を更新し、必要なヒープ記述子の領域へのポインターを反映します。 たとえば、ある記述子テーブルは初期化済みの静的な (不変の) 記述子を指し、別の記述子テーブルでは現在のレンダリングに対して構成されている動的な記述子を指すように設定します。
        • ルート署名の定数やルート署名記述子を、アイテムごとのレンダリングに合わせて更新します。
        • 描画するアイテムのパイプライン状態を設定します (変更が必要な場合のみ)。現在バインドされているルート署名に対応している必要があります。
        • Draw
      • 繰り返す (次のアイテム)
    • 繰り返す (次のコマンド リスト)
    • 厳密には、GPU によるメモリでの作業が完了してそのメモリが使用されなくなったときに、メモリは解放可能になります。 記述子からそのメモリへの参照は、その記述子を使用するレンダリングが他に提出されていなければ、削除する必要はありません。 したがって、後続のレンダリングでは記述子ヒープ内の他の領域を参照することも、不要になった記述子を有効な記述子で上書きすることによって記述子ヒープ領域を再利用することもできます。
  • 繰り返す (次のフレーム)

他の記述子の種類、レンダー ターゲット ビュー (RTV)、深度ステンシル ビュー (DSV)、インデックス バッファー ビュー (IBV)、頂点バッファー ビュー (VBV)、ストリーム出力ビュー (SOV) は、異なる方法で管理されることに注意してください。 コマンド リストへの記録時に、描画ごとにバインドされる一連の記述子のバージョン管理はドライバーが行います (ルート署名のバインドのバージョン管理がハードウェア/ドライバーで行われる方法に似ています)。 これは、シェーダーから認識可能な記述子ヒープの内容とは別のものです。これについてはアプリケーションが、描画ごとに別の記述子を参照するたびにヒープを介して手動で割り当てる必要があります。 シェーダーから認識可能なヒープの内容のバージョン管理はアプリケーションに任されます。その結果として、アプリケーションではたとえば、変化しない記述子を再利用することや、多数の記述子の静的なセットを使うことや、シェーダー インデックス指定 (たとえばマテリアル ID による) を利用して記述子ヒープからどの記述子を使用するかを選択することや、記述子に応じて異なる手法の組み合わせを使用することができます。 ハードウェアは、このような柔軟な取り扱いを他の記述子の種類 (RTV、DSV、IBV、VBV、SOV) について行うことはできません。

サブ割り当て

Direct3D 12 では、アプリが低水準の制御をメモリ管理に関して行うことができます。 以前のバージョンの Direct3D (これには Direct3D 11 も含まれます) では、割り当てはリソースごとに 1 つです。 Direct3D 12 では、アプリで API を使用して大きなメモリ ブロックを割り当てるときに、1 つのオブジェクトに必要な大きさを超えるサイズを指定することができます。 これが完了した後に、アプリで記述子を作成して、その大きなメモリ ブロックのセクションを指すことができます。 この何をどこに入れるか (小さいブロックを大きいブロックの内側に) を決めるプロセスを、サブ割り当てといいます。 これをアプリで実行できるようにすると、計算とメモリを効率的に利用できます。 たとえば、リソースの名前を変更することは現代的な方法ではありません。 この代わりに、アプリでフェンスを使用すると、あるリソースがいつ使用され、いつ使用されないかを特定できます。それには、コマンド リストの実行の中でそのリソースの使用が必要である部分にフェンスを置きます。

リソースの解放

パイプラインにバインドされているメモリを解放するには、GPU によるそのメモリの使用が完了している必要があります。

フレーム レンダリングを待つことは、GPU による使用の完了を確証するための最も粗い方法です。 より粒度を高めるには、ここでもフェンスを使用します。あるコマンドの完了を追跡したい場合は、そのコマンドをコマンド リストに記録するときに、コマンドの直後にフェンスを挿入します。 これで、フェンスを利用してさまざまな同期操作を行うことができます。 新しい作業 (コマンド リスト) を提出し、指定したフェンスが GPU を通過するまで待ちます。フェンスは、その前のすべてが完了していることを示します。あるいは、フェンスが通過したときに CPU イベントを生成するよう設定しておきます (アプリではそれまでの間、スレッドをスリープさせて待ちます)。 Direct3D 11 では、これは EnqueueSetEvent() でした。