はじめに
みなさん、DirectX Tool Kit for DirectX12やDear ImGui(for DirectX12)ライブラリを使用したことはありますでしょうか?
これらはDirectX的な処理を意識することなくテキストやGUIを簡単に描画することのできるライブラリです。
DirectX12経験者ならわかると思うのですが、これらのライブラリの機能を使用するのにID3D12DeviceへのポインタやDescriptorHeap上のハンドルを渡しただけで描画できることに疑問を抱いたことはないでしょうか?
今回は自作のフレームワークやエンジンを作る方の参考として、固定のRootSignatureを解説したいと思います。
なぜDirectX Tool KitやDear ImGui(DirectX12)がRootSignatureを必要としないのか
Dear ImGuiで説明させていただきます。
DirectX12環境でDear ImGuiを使用するにはライブラリに同梱されている「imgui_impl_dx12.h」というヘッダをインクルードして初期化処理を行います。
初期化関数の引数を見るとこのようになっています。
bool ImGui_ImplDX12_Init(
ID3D12Device* device,
int num_frames_in_flight, //! フレームバッファの数
DXGI_FORMAT rtv_format, //! フレームバッファのフォーマット
ID3D12DescriptorHeap* cbv_srv_heap,
D3D12_CPU_DESCRIPTOR_HANDLE font_srv_cpu_desc_handle,
D3D12_GPU_DESCRIPTOR_HANDLE font_srv_gpu_desc_handle
);
初期化に必要なDirectX12のオブジェクトはDeviceとフォントテクスチャ用のDescriptorHandleのみです。
(なぜかDescriptorHeapへのポインタが引数にありますが、内部のコードを見ても使用している箇所が見当たりませんでした。実際、nullptrを指定しても問題なく動作します。)
これだけでImGuiを使用できるようになります。後は描画コマンドを積むのにID3D12GraphicsCommandListへのポインタを渡す必要があるぐらいです。
DirectX12経験者ならこれだけで描画できることに不思議に思わないでしょうか?
描画処理をしている以上シェーダーの実行が必要になります。シェーダーを実行するということはRootSignatureやPipelineStateObjectが必要です。
しかし、Dear ImGuiはこれらを外部から設定することなく描画処理を実行することができます。
なぜこのような事ができるのかというと答えは単純で、ライブラリ側で固有のRootSignatureを持っているからです。
ライブラリ内部でそのRootSignatureを使用してPipelineStateObjectを作成しているのでコマンドリストへのポインタを渡すだけで描画コマンドを作成できるのです。
1つのRootSignatureに全てのバインド設定をしてしまうことのデメリット
Microsoft公式によるRootSignatureのサイズ制限は次のように書かれています。
メモリの制限とコスト
ルート署名の最大サイズは 64 DWORD です。この最大サイズは、バルク データを保存する方法としてルート署名が不正に使用されるのを防ぐために選択されています。 ルート署名内の各エントリは、この 64 DWORD 制限に対してコストを持ちます。
記述子テーブルのコストはそれぞれ 1 DWORD です。
ルート定数は 32 ビット値であるため、コストはそれぞれ 1 DWORD です。
ルート記述子 (64 ビット GPU の仮想アドレス) のコストはそれぞれ 2 DWORD です。
静的サンプラーには、ルート署名のサイズでのコストはありません。
つまり、全てのRootParameterをDescriptorTableとして使用する場合でも最大64個のTableしか設定できないのです。(実際にそこまで使うことはないでしょうが)
また描画のシーンによって使用するパラメータも異なるので、シーンが変わればRootSignatureの構成も変わります。
しかし、どのシーンでも共通して使用するパラメータもあるはずです。
例で言えば、テキストやUIの描画はシーン共通で使用することが多いと思います。
単一のRootSignatureで完結させようとすると、そのたびにテキスト、UI用のRootParameterを設定しなければなりません。
また、RootSignatureが変わればテキストUI描画用のPipelineStateObjectも再作成する必要があります。
更にRootParameterIndexやShaderRegisterIndex,RegisterSpaceが競合しないように意識する必要があります。面倒ですね。
シーンごとに可変のRootSignatureとは別に固定のRootSignatureを作る
そこで、シーン共通で使用するようなパラメータだけの固定RootSignatureを作成してしまい、描画ループの最後にポストプロセス的に描画してしまうのです。
そうすれば毎回共通パラメータをシーンのRootSignatureに設定する必要もありませんし、PipelineStateObjectの再作成も不要になります。
RootSignatureが違うのでShaderRegisterIndex,RegisterSpaceの競合も気にする必要がありません。
そのためわざわざシェーダーファイルを作成しなくてもC++側に直接シェーダーを書いてしまうこともできます。(Dear ImGuiではchar配列にシェーダーを書いていました)
シーンごとに可変のRootSignatureはフレームワーク使用者が自由に構成を決めてもらい、
共通で使用されるパラメータをバインドする固定RootSignatureはフレームワーク内部で隠蔽して、使用者はその固定RootSignatureを意識することなく機能を使用できるようにする。
という考えです。
おわりに
今回は可変RootSignatureとは別にシーンで共通の固定のRootSignatureを用意する手法について解説させていただきました。
自作のフレームワーク、エンジンを作る方の参考になれば幸いです!
今後もDirectX12やC++に関する情報を発信していくのでこの記事が良いと思ったらいいねとフォローを頂けるとモチベーションが上がるのでよろしくお願いいたします!
参考文献