記述子ヒープの概要

記述子ヒープに格納されているのは、パイプライン状態オブジェクト (PSO) の一部ではないさまざまな種類のオブジェクトです。たとえば、シェーダー リソース ビュー (SRV)、順序指定されていないアクセス ビュー (UAV)、定数バッファー ビュー (CBV)、サンプラーです。

記述子ヒープの目的

記述子ヒープの主目的は、シェーダーによって参照されるオブジェクトの種類の記述子仕様を格納するためのメモリを、できるだけ大きなレンダリング ウィンドウのために一括して割り当てることです (理想的にはレンダリングのフレーム全体以上)。 アプリケーションの中で、どのテクスチャをパイプラインから参照するかを API からすばやく切り替える場合は、必要な状態設定ごとにその場で記述子テーブルを定義するための領域を、記述子ヒープの中で確保する必要があります。 アプリケーションでは、定義を再利用するか (たとえばリソースを別のオブジェクトで再び使用する場合)、オブジェクトの種類を切り替えるたびにヒープ領域を順に割り当てていくかを選択できます。

記述子ヒープを使用すると、個々のソフトウェア コンポーネントごとに別の記述子記憶域を管理することもできます。

ヒープはすべて CPU から参照可能です。 アプリケーションからは、必要に応じて、どの CPU アクセス プロパティを記述子ヒープに持たせるかを要求することもできます。たとえば、ライト コンバインやライトバックなどです。 アプリで作成できる記述子の数に制限はなく、どのようなプロパティでも持たせることができます。 アプリはいつでも、純粋にステージングを目的としたサイズ無制約の記述子ヒープを作成することができ、レンダリングに使用される記述子ヒープに必要に応じてコピーすることができます。

同じ記述子ヒープに何を入れることができるかについては、いくつかの制限事項があります。 CBV、UAV、SRV のエントリは、同じ記述子ヒープに入れることができます。 ただし、サンプラーのエントリが同じヒープを CBV、UAV、または SRV のエントリと共有することはできません。 一般的に、記述子ヒープは大きく 2 つに分けられます。1 つは共通リソース用で、2 つ目はサンプラー用です。

Direct3D 12 による記述子ヒープの使い方は、ほとんどの GPU ハードウェアで行われることと同じです。つまり、記述子が記述子ヒープ内にのみ存在するようにすること、またはそのようなヒープが使用されるときに必要なアドレス指定ビットを少なくすることを目的とします。 Direct3D 12 では、記述子ヒープの使用が必要であり、記述子をメモリ内の別の場所に入れることはできません。

記述子ヒープの編集は、CPU による即時編集のみが可能です。GPU によって記述子ヒープを編集することはできません。

Synchronization

記述子ヒープの内容の変更は、そのヒープを参照するコマンド リストの記録の前でも、記録中でも、記録後でも行うことができます。 ただし、実行のために提出されたコマンド リストが記述子の場所を参照する可能性がある間は、その記述子を変更することはできません。変更すると、競合状態を引き起こす可能性があるためです。

バインド

最大 1 つの CBV/SRV/UAV 結合ヒープと 1 つのサンプラー ヒープを一度にバインドできます。 これらのヒープは、グラフィックスと計算の両パイプライン間で共有されます (それぞれの PSO で記述)。

ヒープの切り替え

アプリケーションで SetDescriptorHeapsReset の API を使用してヒープを同じコマンド リストの中で、または別のコマンド リストとの間で切り替えることは許容されます。 ハードウェアによっては、これは高コストの操作になる可能性があります。その時点でバインドされている記述子ヒープに依存するすべての作業をフラッシュするための GPU 停止が必要になるからです。 したがって、アプリケーションで記述子ヒープを変更する必要がある場合は、GPU のワークロードが相対的に少ないときに行うようにしてください。できれば、変更を行うのはコマンド リストの開始時に限定します。

バンドル

バンドルに関して呼び出すことができるのは SetDescriptorHeaps メソッドだけです。設定される記述子ヒープは、バンドルを呼び出すコマンド リストのものと正確に一致する必要があります。 バンドルによって記述子テーブルが変更されない場合は、記述子ヒープを設定する必要はありません。

バンドルと一緒に使用できない API 呼び出しの一覧については、「コマンド一覧およびバンドルの作成と記録」を参照してください。

管理

1 つのシーンのすべてのオブジェクトをレンダリングするには、多数の記述子が必要になりますが、そのためのさまざまな管理戦略を紹介します。

最も基本的な戦略は、記述子ヒープの未使用領域に、次の描画呼び出しに必要なすべてのものを入れておくことです。 描画呼び出しをコマンド リストに対して発行する直前に、記述子テーブルのポインターを、この新しくデータを入れたテーブルの先頭に設定します。 この利点は、特定の記述子がヒープ内のどこにあるかを記録する必要がないということです。

この戦略の欠点は、同じ記述子の大量の繰り返しが記述子ヒープ内に存在する可能性があることと (特に、非常によく似たシーンをレンダリングするとき)、記述子ヒープ領域がすぐに使い果たされることです。 競合を防止するために、記述子ヒープを、GPU 上でレンダリングされるもの用と CPU によって記録されるもの用で分けることが必要になります。 別の方法として、サブ割り当てシステムを使用することもできます。

この基本的システムをさらに最適化するには、互いに重なり合う記述子テーブルを、ある描画呼び出しと次の描画呼び出しとの間で慎重に使用します。これは、必要な新しい記述子だけが追加されるようにするためです。

基本的な戦略よりも効率的な戦略としては、そのシーンの一部であることがわかっているオブジェクト (またはマテリアル) に必要な記述子を、あらかじめ記述子ヒープに入れておくというものがあります。 この意図は、描画時には記述子テーブルの設定だけが必要になるようにすることです。記述子ヒープの内容は、あらかじめ設定されているからです。

事前に入れておく戦略のバリエーションとして、記述子ヒープを 1 つの大きな配列として扱い、必要なすべての記述子を、固定された既知の場所に格納するというものがあります。 これで、描画呼び出しには一連の定数だけを渡すことができます。これらの定数は、使用する必要がある記述子が存在する配列へのインデックスです。

さらに最適化するには、定数を記述子ヒープに配置する代わりに、変更される頻度が高いものをルート定数とルート記述子の中に入れておくことです。 ほとんどのハードウェアにとって、これは定数を扱う効率的な方法です。

実際には、グラフィックス エンジンで使用される戦略は状況によって異なり、そのときの描画の要件に合わせて各戦略の要素の組み合わせが使用されることもあります。

記述子ヒープ