はじめに
今回は、Descriptor,DescriptorHeap,RootSignatureとリソースバインディングについて解説していきたいと思います。
このあたりはDirectX12の特に難解な部分の一つなので「DirectX12を勉強し始めたけどリソースバインディングがよく分からない!」という方の参考になれば幸いです。
実際のコード例も載せてあります。
目次
- Descriptorとは?
- DescriptorHeapとは?
- RootSignatureとは?
- RootParameterについて
- RootParameterの設定
- RootSignature作成コード例
- リソースバインディング
本記事は"d3dx12.h"をインクルードしてある事を前提にしています。
DirectX12でプログラミングされてる方は大抵の場合こちらのヘッダをインクルードしてあると思いますが、ご注意ください。
Descriptorとは?
DirectX12ではシェーダーで使用したいパラメーター(頂点情報から行列やベクトル、テクスチャなど)は全てID3D12Resourceというオブジェクトで管理します。
このID3D12Resourceを利用することでGPU側のメモリ領域を確保してデータを送ることができます。
しかし、シェーダーはこのResourceに対して直接アクセスすることはできません。
Resourceにアクセスするには「Descriptor」というものが必要になります。
(ただし、後述するRootDescriptorなどDescriptorの作成が必要ない場合もあります。このあたりはRootSignatureの解説時にお話しします。)
「Descriptor」とは、Resourceの場所や種類、サイズなどの情報を持ったものです。
DirectX12ではShaderResourceViewなど末尾にViewとつくものがそれにあたります。
ResourceとDescriptorは常に1対1という訳ではなく、単一のResource内の別々の領域に対する複数のDescriptorを作成したり、同じ領域に対する異なる複数のDescriptorを作成することもできます。
Descriptorにはさまざまな種類があり、作成方法やセット方法、場所も異なります。
先に各Descriptorについて簡単に説明させていただきます。
VertexBufferView(VBV),IndexBufferView(IBV)
頂点バッファ、インデックスバッファのDescriptorです。
それぞれD3D12_VERTEX_BUFFER_VIEW,D3D12_INDEX_BUFFER_VIEWという構造体になっています。
ID3D12GraphicsCommandList::IASetVertexBuffers,ID3D12GraphicsCommandList::IASetIndexBuffer関数にて構造体そのものを引数に指定してセットします。
これらは他のDescriptorと違いDescriptorHeap,RootSignatureとは関係ないので今回は説明を省かせていただきます。
RenderTargetView(RTV),DepthStencilView(DSV)
テクスチャを描画先や深度ステンシルバッファとして指定する際に使用するDescriptorです。
これらは他のDescriptorと異なり、Resourceを読み込むためではなく書き込むために使用されます。
それぞれID3D12Device::CreateRenderTargetView,ID3D12Device::CreateDepthStencilView関数で作成し、
ID3D12GraphicsCommandList::OMSetRenderTargets関数でセットします。
ConstantBufferView(CBV),ShaderResourceView(SRV),UnorderedAccessView(UAV)
CBVは、行列やベクトルなどの定数データを送るのに使用します。
SRVはテクスチャや複数の構造体を送るのに使用します。
CBV、SRVは読み込み専用なのに対し、UAVは書き込みも可能なリソースを示すのに使用します。
ID3D12Device::CreateConstantBufferView,ID3D12Device::CreateShaderResourceView,ID3D12Device::CreateUnorderedAccessView関数で作成します。
セット方法は非常に多彩なのでRootSignatureの解説時に説明します。
DescriptorHeap,RootSignatureと密に関わるDescriptorで、恐らくこれらのDescriptorの管理に非常に悩まされると思います。
Sampler
Samplerはテクスチャサンプリングの方法や挙動を指定したものです。
他のDescriptorとは違いパラメータがID3D12ResourceではなくDescriptorそのものに格納されています。
SamplerはD3D12_SAMPLER_DESC構造体で情報を設定し、ID3D12Device::CreateSampler関数で作成します。
また、後述のRootSignatureに「Static Sampler」として直接設定することもできます。
DescriptorHeapとは?
DescriptorHeapとは名前の通り、多数のDescriptorを格納するHeapの事です。
RTV,DSV,CBV&SRV&UAV,SamplerはこのHeapのHandleを指定して作成されます。
DescriptorHeapの作成
HRESULT CreateDescriptorHeap(
[in] const D3D12_DESCRIPTOR_HEAP_DESC *pDescriptorHeapDesc,
REFIID riid,
[out] void **ppvHeap
);
第二引数はインターフェースを一意に特定する識別子、第三引数はID3D12DescriptorHeapへのダブルポインタです。
DirectX12のコードを書いたことがある方はわかると思いますが、IID_PPV_ARGSマクロを使用することで簡単に引数を設定できます。
第一引数のD3D12_DESCRIPTOR_HEAP_DESCはHeapの設定をします。
構造体のメンバは次のようになっています。
typedef struct D3D12_DESCRIPTOR_HEAP_DESC {
D3D12_DESCRIPTOR_HEAP_TYPE Type;
UINT NumDescriptors;
D3D12_DESCRIPTOR_HEAP_FLAGS Flags;
UINT NodeMask;
} D3D12_DESCRIPTOR_HEAP_DESC;
TypeはそのHeapが取り扱うDescriptorを指定する列挙型です。
メンバは次のようになっています。
typedef enum D3D12_DESCRIPTOR_HEAP_TYPE {
D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV = 0,
D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER,
D3D12_DESCRIPTOR_HEAP_TYPE_RTV,
D3D12_DESCRIPTOR_HEAP_TYPE_DSV,
D3D12_DESCRIPTOR_HEAP_TYPE_NUM_TYPES
} ;
同一のHeapに複数種のDescriptorを作成することはできず、Typeで指定したDescriptor以外は作成することができません。
NumDescriptorはHeap内のDescriptorの数です。他の設定項目もそうですが、Heap内のDescriptor数は後からの変更ができません。
FlagsはDescriptorHeapのGPUからの可視性を設定する列挙型です。
メンバは次のようになっています。
typedef enum D3D12_DESCRIPTOR_HEAP_FLAGS {
D3D12_DESCRIPTOR_HEAP_FLAG_NONE = 0,
D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE = 0x1
} ;
D3D12_DESCRIPTOR_HEAP_FLAG_NONEに設定したHeap内のDescriptorはCPU側にのみ存在します。
D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLEに設定した場合、DescriptorはCPU側とGPU側両方に存在します。
またD3D12_DESCRIPTOR_HEAP_TYPEでRTV又はDSVを指定していた場合はD3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLEを指定することはできません。
DescriptorHeapの可視性については後のRootDescriptorTableのリソースバインドの解説で少し書いてます。
NodeMaskは複数のアダプタ(GPU)を指定している場合に設定する項目です。単一のアダプタを使用している場合は0を指定します。
公式のリファレンスを貼っておくので詳しい概要を知りたい方はこちらをご覧ください。
RootSignatureとは?
リソースとDescriptorを作成しただけではシェーダーでそのリソースにアクセスすることはできません。
RootSignatureとはシェーダーで使用するリソースのDescriptorをバインドするためのものです。
ここでのDescriptorは、CBV,SRV,UAV,Samplerを指しています。
RootSignatureのRootParameterにシェーダーで使用したいリソースのDescriptorの設定を行い、そのRootへリソースを指すDescriptorをセットすることで初めてシェーダーでリソースを使用できるようになります。
RootSignatureの作成
HRESULT CreateRootSignature(
[in] UINT nodeMask,
[in] const void *pBlobWithRootSignature,
[in] SIZE_T blobLengthInBytes,
REFIID riid,
[out] void **ppvRootSignature
);
第一、第四、第五引数は先ほども説明したので割愛させていただきます。
第二引数と第三引数はRootSignatureの設定情報が格納されたバイナリオブジェクトへのポインタとそのサイズです。
このバイナリオブジェクトは以下の関数で作成することができます。
HRESULT inline D3DX12SerializeVersionedRootSignature(
_In_ const D3D12_VERSIONED_ROOT_SIGNATURE_DESC *pRootSignatureDesc,
D3D_ROOT_SIGNATURE_VERSION MaxVersion,
_Out_ ID3DBlob **ppBlob,
_Out_opt_ ID3DBlob **ppErrorBlob
);
第一引数のpRootSignatureDescはRootSignatureの設定を行う構造体へのポインタです。
こちらは後程説明いたします。
第二引数のMaxVersionはサポートされているRootSignatureのバージョンを指定します。
サポートされているバージョンを調べるには、ID3D12Device::CheckFeatureSupport関数を使用します。
以下にRootSignatureのバージョンを調べるコード例を書いておきます。
//! ID3D12Deviceは作成済みとします
ID3D12Device* pDevice;
D3D12_FEATURE_DATA_ROOT_SIGNATURE featureData{};
//! 関数が失敗した場合D3D_ROOT_SIGNATURE_VERSION_1_0
if (FAILED(pDevice->CheckFeatureSupport(
D3D12_FEATURE_ROOT_SIGNATURE,
reinterpret_cast<void*>(&featureData),
sizeof(D3D12_FEATURE_DATA_ROOT_SIGNATURE))))
{
featureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_0;
}
//! 成功した場合はD3D_ROOT_SIGNATURE_VERSION_1_1
else
{
featureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_1;
}
第三引数のppBlobはID3DBlobというバイナリデータを格納するオブジェクトへのダブルポインタです。こちらに作成されたRootSignatureのバイナリデータが格納されます。
第四引数のppErrorBlobは関数失敗時にエラー情報が格納されます。不要な場合はnullptrを指定することもできます。
D3D12_VERSIONED_ROOT_SIGNATURE_DESC構造体の設定
第一引数のpRootSignatureDescはd3dx12.hのヘルパー構造体であるCD3DX12_VERSIONED_ROOT_SIGNATURE_DESC使用することで簡単に作成できます。
以下がその構造体内のメンバ関数です。
(ここではRootSignatureバージョン1.1で解説していますが1.0の場合でもほぼ変わりません。)
void CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC::Init1_1(
UINT numParameters,
const D3D12_ROOT_PARAMETER1* _pParameters,
UINT numStaticSamplers = 0,
const D3D12_STATIC_SAMPLER_DESC* _pStaticSamplers = nullptr,
D3D12_ROOT_SIGNATURE_FLAGS flags = D3D12_ROOT_SIGNATURE_FLAG_NONE
);
第一引数のnumParametersはルートパラメータの数です。
第二引数の_pParametersはD3D12_ROOT_PARAMETER1配列への先頭ポインタです。
こちらがRootParameterを設定するための項目で、後程詳しく説明します。
第三引数のnumStaticSamplersはStaticなサンプラーの数です。Staticなサンプラーを使用しない場合は0を指定します。
第四引数の_pStaticSamplersはD3D12_STATIC_SAMPLER_DESC配列への先頭ポインタです。
Static Samplerについて
DescriptorのSamplerの解説時にも書きましたが、サンプラーはID3D12Device::CreateSampler関数で作成しなくてもStatic SamplerとしてRootSignatureに直接指定することで、シェーダー上でサンプラーを使用することができます。
また、Static SamplerとID3D12Device::CreateSampler関数で作成されるサンプラーは共存することができます。
設定を変更することのない固定のサンプラーはStatic Samplerで、変更される可能性のあるサンプラーはID3D12Device::CreateSampler関数で作成すると良いでしょう。
第五引数のflagsはRootSignatureのオプションを指定するフラグです。
フラグの概要は公式のリファレンスを見た方が早いのでリンクを貼っておきます。
基本的にD3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUTを指定するだけで問題ないでしょう。
RootParameterについて
バインドするDescriptorはRootParameterというもので設定します。
RootParameterは複数定義することができますがサイズ制限があり、全体で256バイト以内に収める必要があります。(Static Samplerはこのサイズ制限には含まれません。)
RootParameterには3つの種類があるので順に解説していきます。
RootConstant
RootConstantはRootParameterに直接シェーダーで使用する値を格納する方式です。
4バイト単位のサイズを指定できます。
Descriptorを介さないので最もアクセス速度に優れていますが、RootParameterは全体で256バイトまでという制限があるので使用頻度は低いと思われます。
RootDescriptor
RootDescriptorはRootParameterに単一のDescriptorを指定することができます。
ただし明示的にDescriptorの作成を行う必要は無く、セットする時はID3D12ResourceのD3D12_GPU_VIRTUAL_ADDRESSを指定します。
そのためDescriptorHeapが必要ありません。
RootDescriptorのサイズは8バイトです。
RootDescriptorにはCBV,SRV,UAVのいずれかを指定することができます。
ただし、Texture2Dなどの複雑なDescriptorをRootDescriptorとして指定することはできません。 後述のRootDescriptorTableを使用する必要があります。
StructuredBufferなどのSRV,UAVはRootDescriptorを使用することができます。
RootConstantの次にアクセス速度に優れていて明示的にDescriptorを作成する必要もないので便利です。
RootDescriptorTable
RootDescriptorTableは複数のDescriptorを指定することができます。
例えば1つのRootDescriptorTableにCBV1つ、SRV3つ、UAV2つなどを指定することができます。
Samplerも指定することができますが、1つのTable内にCBV,SRV,UAVと混合させることはできません。 これはSamplerとCBV,SRV,UAVでDescriptorを格納するDescriptorHeapが異なるためです。
RootDescriptorTableのサイズは4バイトです。
アクセス速度はRootConstant、RootDescriptorに比べると遅いですが、テクスチャのDescriptorはDescriptorTableでしかバインドすることができないので必然的に使用することになると思います。
特にこのDescriptorTableとDescriptorHeapまわりが難解で管理も難しいので悩んでいる方も多いと思いますので、後のリソースバインディングの項で詳しく解説したいと思います。
RootParameterの設定
RootSignatureバージョン1.1で解説していきます。
RootParameterの設定はD3D12_ROOT_PARAMETER1という構造体に設定します。
こちらもd3dx12ヘッダーにCD3DX12_ROOT_PARAMETER1というヘルパー構造体で簡単に設定を行えるのでこちらを使用していきます。
共通引数
RootParameter関連の共通の引数を解説します。
shaderRegister(UINT)
HLSL側で使用されるレジスタ番号の事です。
registerSpace(UINT)
HLSL側で使用されるレジスタ番号の空間を表しています。これを使用することでリソースの種類や用途別に空間を分けることができます。
例えばTextureのSRVをspace0、StructuredBufferのSRVをspace1に分けるとすると以下のような書き方ができます。
Texture2D gColorMap : register(t0, space0);
StructuredBuffer<Structure> gStructured : register(t0, space1);
visibility(D3D12_SHADER_VISIBILITY)
そのパラメータを視認できるシェーダーステージを制限することができます。
D3D12_SHADER_VISIBILITY_ALLなら全てのシェーダーステージからそのパラメータにアクセスすることができます。
パラメータを特定のシェーダーステージからのみアクセスできるようにすることの利点は、重複する名前空間でシェーダーステージごとに異なるDescriptorをバインドさせることができます。
Texture2D gTexture : register(t0, space0);
Texture2D gColorMap : register(t0, space0);
上記のような頂点シェーダーとピクセルシェーダーがあるとして、
D3D12_SHADER_VISIBILITY_ALLを指定したRootParameterを作成した場合両方のシェーダーで同じDescriptorがバインドされます。
それぞれ別々のDescriptorをバインドさせたい場合、D3D12_SHADER_VISIBILITY_VERTEXを指定したRootParameterとD3D12_SHADER_VISIBILITY_PIXELを指定したRootParameterの2つを作成することで重複する名前空間を使用してシェーダーステージ別でバインドされるDescriptorを分けることができます。
D3D12_ROOT_DESCRIPTOR_FLAGS, D3D12_DESCRIPTOR_RANGE_FLAGS
バインドするDescriptorや、そのDescriptorが指すリソースの性質を表すフラグです。
適切なフラグを指定することで最適化が行われる可能性があります。
こちらはMicrosoft公式が詳しい説明をしているのでこちらをご覧ください。
それでは各RootParameterの作成方法を見ていきましょう。
RootConstantを設定
RootConstantを設定する場合、以下の関数を使用します。
void CD3DX12_ROOT_PARAMETER1::InitAsConstants(
UINT num32BitValues,
UINT shaderRegister,
UINT registerSpace,
D3D12_SHADER_VISIBILITY visibility
);
第一引数のnum32BitValuesは4バイト型定数データの数です。例えば1つの4次元ベクトルを送りたい場合4を指定します。
RootConstantはHLSL側で定数バッファとして認識されます。そのためレジスターキーワードは「b」を使用します。
RootDescriptorを設定
RootDescriptorを設定する場合以下の関数を使用します。
ここではConstantBufferViewを例として挙げていますが、
SRVを指定したい場合はInitAsShaderResourceView関数を、
UAVを指定したい場合はInitAsUnorderedAccessView関数を使用します。
使用する関数が違うだけで引数は変わりません。
void CD3DX12_ROOT_PARAMETER1::InitAsConstantBufferView(
UINT shaderRegister,
UINT registerSpace,
D3D12_ROOT_DESCRIPTOR_FLAGS flags,
D3D12_SHADER_VISIBILITY visibility
);
引数は先ほどの共通引数で説明した通りです。
RootDescriptorTableを設定
RootDescriptorTableを指定する場合は以下の関数を使用します。
void CD3DX12_ROOT_PARAMETER1::InitAsDescriptorTable(
UINT numDescriptorRanges,
const D3D12_DESCRIPTOR_RANGE1* pDescriptorRanges,
D3D12_SHADER_VISIBILITY visibility
);
新たにD3D12_DESCRIPTOR_RANGE1という構造体が出てきました。
この構造体はDescriptorの種類や数を示すものです。
こちらの構造体もd3dx12ヘッダーにCD3DX12_DESCRIPTOR_RANGE1というヘルパー構想体があるので見ていきましょう。
void CD3DX12_DESCRIPTOR_RANGE1::Init(
D3D12_DESCRIPTOR_RANGE_TYPE rangeType,
UINT numDescriptors,
UINT baseShaderRegister,
UINT registerSpace,
D3D12_DESCRIPTOR_RANGE_FLAGS flags,
UINT offsetInDescriptorsFromTableStart
);
第一引数のrangeTypeはその範囲のDescriptorの種類を指定します。
第二引数のnumDescriptorsは範囲のDescriptorの数です。
第三引数のbaseShaderRegisterは範囲のRegisterIndexの開始位置です。
例えばnumDescriptorsを3に、baseShaderRegisterを0に指定した場合、その範囲は0~2のRegisterIndexを使用します。
第四、第五引数は共通引数で説明した通りです。
第六引数のoffsetInDescriptorsFromTableStartはDescriptorTableの先頭からのオフセット位置を表しています。D3D12_DESCRIPTOR_RANGE_OFFSET_APPENDというマクロ名があり、こちらを指定すると内部的に自動でオフセットを計算してくれます。
RootSignatureの作成コード例
例としてStaticSampler2つと下記のeRootParametersに書かれている構成でRootSignatureを作成するコードを書いてみます。
//! 使用するRootParameterを列挙
enum eRootParameters
{
//! RootConstant b0 space0 (32bit * 3 = 96)
RP0,
//! RootCBV b0 space1
RP1,
//! RootSRV t0 space0
RP2,
//! RootDescriptorTable b1~b2 space1, t0~t2 space1, u0~u1 space0
RP3,
//! RootDescriptorTable s0~s2 space1
RP4,
NumRootParameters,
};
CD3DX12_DESCRIPTOR_RANGE1 ranges[4]{};
//! RP3 CBV
ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 2, 1, 1, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC);
//! RP3 SRV
ranges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 3, 0, 1, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC);
//! RP3 UAV
ranges[2].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 2, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_VOLATILE);
//! RP4 Sampler
ranges[3].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER, 2, 0, 1, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC);
//! RootParameters
CD3DX12_ROOT_PARAMETER1 rootParam[NumRootParameters]{};
rootParam[RP0].InitAsConstants(3, 0, 0, D3D12_SHADER_VISIBILITY_ALL);
rootParam[RP1].InitAsConstantBufferView(0, 1, D3D12_ROOT_DESCRIPTOR_FLAG_DATA_STATIC, D3D12_SHADER_VISIBILITY_ALL);
rootParam[RP2].InitAsShaderResourceView(0, 0, D3D12_ROOT_DESCRIPTOR_FLAG_DATA_STATIC, D3D12_SHADER_VISIBILITY_ALL);
rootParam[RP3].InitAsDescriptorTable(3, &ranges[0], D3D12_SHADER_VISIBILITY_ALL);
rootParam[RP4].InitAsDescriptorTable(1, &ranges[3], D3D12_SHADER_VISIBILITY_PIXEL);
//! Static Samplers
D3D12_STATIC_SAMPLER_DESC sSamplers[2]{};
//! サンプラーの設定は省略
//! D3D12_VERSIONED_ROOT_SIGNATURE_DESC構造体の作成
CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC rootSigDesc{};
rootSigDesc.Init_1_1(
_countof(rootParam),
rootParam,
_countof(sSamplers),
sSamplers,
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT
);
using Microsoft::WRL::ComPtr;
//! Deviceは作成済みとします
ComPtr<ID3D12Device> pDevice;
//! RootSignatureVersionは事前に確認済みとします
D3D_ROOT_SIGNATURE_VERSION rootSigVersion;
ComPtr<ID3D12RootSignature> pRootSignature = nullptr;
ComPtr<ID3DBlob> pRootSigBlob = nullptr;
ComPtr<ID3DBlob> pErrorBlob = nullptr;
//! RootSignatureのバイナリを作成
D3DX12SerializeVersionedRootSignature(
&rootSigDesc,
pRootSigVersion,
pRootSigBlob.GetAddressOf(),
pErrorBlob.GetAddressOf()
);
//! RootSignatureを作成
pDevice->CreateRootSignature(
0,
pRootSigBlob->GetBufferPointer(),
pRootSigBlob->GetBufferSize(),
IID_PPV_ARGS(pRootSignature.ReleaseAndGetAddressOf())
);
リソースバインディング
ここまでで各オブジェクトの概要は書き終わりました。
ここからは作成したRootSignatureをセットしてリソースをバインドする部分の話をしたいと思います。
RootParameterを設定する関数にSetGraphicsRootXXXとSetComputeRootXXXがありますが、前者は頂点シェーダーやピクセルシェーダーなどの描画パイプラインに、後者はコンピュートシェーダーを使用するパイプラインにRootParameterをセットします。
今回は描画パイプラインでの使用を考えているので前者の関数を使用します。
RootConstant
RootConstantは最も簡単です。32ビット単体の値をセットする関数と複数の32ビット値をセットする関数の2通りあります。
SetGraphicsRoot32BitConstant(単体版)
void ID3D12GraphicsCommandList::SetGraphicsRoot32BitConstant(
UINT RootParameterIndex,
UINT SrcData,
UINT DestOffsetIn32BitValues
);
第一引数のRootParameterIndexはパラメータをセットするRootの番号です。
第二引数のSrcDataはRootにセットする32ビットの値です。
第三引数のDestOffsetIn32BitValuesはSrcDataをセットする場所へのオフセットです。
先ほどの「RootSignatureの作成コード例」を元にRootConstantをセットするコードを書いてみましょう。
using Microsoft::WRL::ComPtr;
//! 各DirectXオブジェクトは作成済みとします
ComPtr<ID3D12Device> pDevice;
ComPtr<ID3D12RootSignature> pRootSignature;
ComPtr<ID3D12GraphicsCommandList> pCommandList;
//! RootSignatureをセット
pCommandList->SetGraphicsRootSignature(pRootSignature.Get());
//! RootConstantにセットする値
UINT value0 = 0;
UINT value1 = 1;
UINT value2 = 2;
//! RP0(RootConstant)に値をセット
pCommandList->SetGraphicsRoot32BitConstant(RP0, value0, 0);
pCommandList->SetGraphicsRoot32BitConstant(RP0, value1, 1);
pCommandList->SetGraphicsRoot32BitConstant(RP0, value2, 2);
cbuffer gRootConstant : register(b0, space0)
{
uint value0; //! 0
uint value1; //! 1
uint value2; //! 2
}
これでシェーダー側に値が反映されるはずです。
SetGraphicsRoot32BitConstant関数のSrcDataが符号なし整数値型になっていますが、任意の型のビットパターンをセットしHLSL側でその型として定義することで符号なし整数値以外の値を渡すことができます。
例えば浮動小数点型を送りたい場合以下のようにします。
float valueF = 0.01f;
UINT valueF_bit = 0;
//! memcpyを使用してビットパターンをそのままコピー
memcpy(reinterpret_cast<void*>(&valueF_bit), reinterpret_cast<void*>(&valueF), sizeof(UINT));
pCommandList->SetGraphicsRoot32BitConstant(RP0, valueF_bit, 0);
//! std::bit_castを使用してビットパターンを維持したままキャスト(C++20以降)
valueF_bit = std::bit_cast<UINT>(valueF);
pCommandList->SetGraphicsRoot32BitConstant(RP0, valueF_bit, 0);
//! コメントで教えていただいたのですが、reinterpret_castや共用体を使用したキャストは未定義動作になるらしいので注意してください。
//! 以下のようなコードの事です。
//! //! reinterpret_castを使用しビットパターンをそのままにUINT型にキャストする。
//! valueF_bit = *reinterpret_cast<UINT*>(&valueF);
//!
//! //! 共用体を使用してfloat型変数のビットパターンを得る
//! union
//! {
//! float valueF = 0.05f;
//! UINT valueF_bit;
//! };
//!
//! 詳しくは「Strict Aliasing Rules」と調べれば出てきます。
SetGraphicsRoot32BitConstants(複数版)
void ID3D12GraphicsCommandList::SetGraphicsRoot32BitConstants(
UINT RootParameterIndex,
UINT Num32BitValuesToSet,
const void *pSrcData,
UINT DestOffsetIn32BitValues
);
第一引数のRootParameterIndexはパラメータをセットするRootの番号です。
第二引数のNum32BitValuesToSetは32ビット値をセットする数です。
第三引数のpSrcDataはセットする値へのポインタです。
第四引数のDestOffsetIn32BitValuesはpSrcDataをセットする場所へのオフセットです。
こちらも「RootSignatureの作成コード例」を元にRootConstantをセットするコードを書いてみましょう。
using Microsoft::WRL::ComPtr;
//! 各DirectXオブジェクトは作成済みとします
ComPtr<ID3D12Device> pDevice;
ComPtr<ID3D12RootSignature> pRootSignature;
ComPtr<ID3D12GraphicsCommandList> pCommandList;
//! RootSignatureをセット
pCommandList->SetGraphicsRootSignature(pRootSignature.Get());
//! RootConstantにセットする値
float position[3] = {0.01f, 0.02f, 0.03f};
//! RP0(RootConstant)に値をセット
pCommandList->SetGraphicsRoot32BitConstants(RP0, 3, reinterpret_cast<void*>(position), 0);
cbuffer gRootConstant : register(b0, space0)
{
float3 Position; //! 0.01f, 0.02f, 0.03f
}
RootDescriptor
RootDescriptorは先ほどのRootParameterの解説時にも書いた通りリソースのタイプによって使用できない場合がありますが、明示的にDescriptorを作成せずにリソースへアクセスできるようになるタイプです。
CBV,SRV,UAV用でそれぞれRootDescriptorをセットする関数が用意されています。
void ID3D12GraphicsCommandList::SetGraphicsRootConstantBufferView(
[in] UINT RootParameterIndex,
[in] D3D12_GPU_VIRTUAL_ADDRESS BufferLocation
);
void ID3D12GraphicsCommandList::SetGraphicsRootShaderResourceView(
[in] UINT RootParameterIndex,
[in] D3D12_GPU_VIRTUAL_ADDRESS BufferLocation
);
void ID3D12GraphicsCommandList::SetGraphicsRootUnorderedAccessView(
[in] UINT RootParameterIndex,
[in] D3D12_GPU_VIRTUAL_ADDRESS BufferLocation
);
第一引数のRootParameterIndexはパラメータをセットするRootの番号です。
第二引数のBufferLocationはID3D12Resourceから取得できるD3D12_GPU_VIRTUAL_ADDRESSです。
先ほどの「RootSignatureの作成コード例」を元にRootDescriptorをセットするコードを書いてみましょう。
ConstantBufferを例にしてみます。
using Microsoft::WRL::ComPtr;
//! シェーダーに送るデータ
struct Constant
{
float value1;
float value2;
}
//! 各DirectXオブジェクトは作成済みとします
ComPtr<ID3D12Device> pDevice;
ComPtr<ID3D12RootSignature> pRootSignature;
ComPtr<ID3D12GraphicsCommandList> pCommandList;
//! Constant構造体のサイズを256バイトにアライメント
const auto PerConstantSize = (sizeof(Constant) + 255) & ~255;
//! バッファのサイズを計算(オフセットの説明の為3つ分のサイズを確保します)
const auto BufferSize = PerConstantSize * 3;
auto heapProp = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
auto resDesc = CD3DX12_RESOURCE_DESC::Buffer(BufferSize);
//! リソース作成
pDevice->CreateCommittedResource(
&heapProp,
D3D12_HEAP_FLAG_NONE,
&resDesc,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(pResource.ReleaseAndGetAddressOf())
);
//! CPUからリソースにデータを書き込めるようにする
Constant* pConstantData = nullptr;
pResource->Map(0,nullptr,reinterpret_cast<void**>(&pConstantData));
//! pCommandList = ID3D12GraphicsCommandList*;
//! RootSignatureをセット
pCommandList->SetGraphicsRootSignature(pRootSignature.Get());
//! リソースに任意のデータを書き込む
Constant[0].value1 = 0.1f;
Constant[0].value2 = 0.2f;
Constant[1].value1 = 0.01f;
Constant[1].value2 = 0.02f;
Constant[2].value1 = 0.001f;
Constant[2].value2 = 0.002f;
//! ID3D12ResourceからD3D12_GPU_VIRTUAL_ADDRESSを取得
auto GPUAddress = pResource->GetGPUVirtualAddress();
//! 送りたいデータの場所へのオフセットを加算(今回は3つ目のデータを送ってみます)
GPUAddress += (PerConstantSize * 2);
//! RP1(RootConstantBufferView)にセット
pCommandList->SetGraphicsRootConstantBufferView(RP1, GPUAddress);
cbuffer gRootCBV : register(b0, space1)
{
float value1; //! 0.001f
float value2; //! 0.002f
}
RootDescriptorTable
RootDescriptorTableを使用する場合は少々複雑になります。
RootDescriptorTableを使用する場合はD3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLEを指定したDescriptorHeap上にシェーダーへ送りたいリソースのDescriptorを作成し、そのDescriptorHeapをコマンドリストにセットする必要があります。
以下がそのDescriptorHeapをコマンドリストへセットする関数です。
void ID3D12GraphicsCommandList::SetDescriptorHeaps(
UINT NumDescriptorHeaps,
ID3D12DescriptorHeap** ppDescriptorHeaps
);
第一引数のNumDescriptorHeapsはセットするDescriptorHeapの数です。
第二引数のppDescriptorHeapsはDescriptorHeapを示すポインタの配列へのポインタです。
この関数はいくつか注意点があります。
まず引数にセットするDescriptorHeapの数を指定する項目がありますが、
セットできるのは
D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAVを指定したDescriptorHeapと、D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLERを指定したDescriptorHeap
の各1つずつです。
つまり最大2つのDescriptorHeapしかセットできません。
また最初にも書きましたがDescriptorHeapのフラグはD3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLEである必要があります。
そしてもう1つの注意点が、DescriptorHeapのセットは1フレーム内で複数回行うべきではないことです。
既にDescriptorHeapがセットされているコマンドリストに再度DescriptorHeapをセットすると、以前セットされたDescriptorHeapはバインドを解除されてしまいます。
そしてこのSetDescriptorHeaps関数は非常にコストが高い処理になります。
1フレーム内に同じコマンドリストに対しSetDescriptorHeaps関数を複数回呼び出すべきではありません。
そのためRootDescriptorTableを使用してバインドするリソースのDescriptorは、全て1つのDescriptorHeap内に収める必要があります。
またDescriptorHeapの解説時にも書きましたがDescriptorHeapのサイズを後から変更することはできません。
そして一番重要で面倒なのが、RootDescriptorTableとしてバインドするDescriptorは。DescriptorHeap上で連続していなければならないという点です。
「RootSignatureの作成コード例」の「RP3」のRootDescriptorTableを例にすれば、このRootParameterをセットする場合、セットするDescriptorがDescriptorHeap上で「CBV,CBV,SRV,SRV,SRV,UAV,UAV」となっている必要があります。
例えば2つ目のCBVだけ別のCBVに変更したいという場合でも、既存の連続Descriptorとは別に新たな連続Descriptorを作成する必要があります。
既存の連続Descriptorに手を加えることもできますが、既存の連続Descriptorを使用する描画処理がある場合、その描画処理が完了するまでは既存の連続Descriptorが残っている必要があります。
DirectX12は描画処理を遅延実行させるためコマンドリスト実行時点でそのDescriptorがHeap上に無い場合アクセスすることができなくなります。
これだけでもRootDescriptorTableとDescriptorHeapまわりの管理の大変さが分かると思います。
余談ですが、Descriptorの作成というのは単体で見ればそこまでコストの高い処理ではないですがフレーム毎に行っているとそれなりにコストがかかってしまいます。
先ほどの「既存の連続Descriptorとは別に新たな連続Descriptorを作成する」場合、そのRootDescriptorTable内のDescriptorの数だけ再度作成することになります。
実はDescriptorはDescriptorHeapを跨いでコピーすることができます。
フラグをD3D12_DESCRIPTOR_HEAP_FLAG_NONEに指定したDescriptorHeap(シェーダーから視認することのできないHeapなので非シェーダー可視Heapと呼ばれたりします)を作成し、これをDescriptorの作成専用とします。
そしてDescriptor作成専用Heapからコマンドリストにセットする、フラグをD3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLEに指定したDescriptorHeap(シェーダーから視認できるのでシェーダー可視Heapと呼びます)へDescriptorをコピーするのです。
コピー元となるDescriptor作成専用の非シェーダー可視Heapを用意しておくことの利点は、
・新しくDescriptorを作成するよりもコピーする方がコストが低い。
・コピー元のDescriptorHeapでDescriptorがバラバラに存在していても、コピー先で連続したDescriptorに直すことができる。
・コピー元の非シェーダー可視Heapは複数あっても問題がない。
特に、2つ目のコピー先で連続Descriptorにすることができるのが大きいと思います。
このDescriptorのコピーについては今度記事を書いてみたいと思います。
ちなみに、RootDescriptorTable内の特定のDescriptorだけ何もバインドさせたくない場合でもDescriptorの作成を行う必要があります。
こちらの記事に特定のリソースを指さないDescriptorについて書いてあるので興味ある方はご覧ください。
それではRootDescriptorTableをセットする関数を見てみましょう。
void ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable(
[in] UINT RootParameterIndex,
[in] D3D12_GPU_DESCRIPTOR_HANDLE BaseDescriptor
);
第一引数のRootParameterIndexはパラメータをセットするRootの番号です。
第二引数のBaseDescriptorは連続するDescriptorの先頭GPUハンドルです。
RootDescriptorTableのサイズ(Descriptorの数)はRootParameterの設定時に指定してあるので先頭のDescriptorの場所だけが必要になります。
それでは先ほどの「RootSignatureの作成コード例」を元にRootDescriptorTableをセットするコードを書いてみましょう。
using Microsoft::WRL::ComPtr;
//! 各DirectXオブジェクトは作成済みとします
ComPtr<ID3D12Device> pDevice;
ComPtr<ID3D12RootSignature> pRootSignature;
ComPtr<ID3D12GraphicsCommandList> pCommandList;
ComPtr<ID3D12DescriptorHeap> pCBVSRVUAV_heap;
//! Descriptorのサイズを取得
const auto IncrementSize = pDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
//! DescriptorHeapの先頭Handleを取得
auto CPUHandle = pCBVSRVUAV_heap->GetCPUDescriptorHandleForHeapStart();
const auto GPUHandleBase = pCBVSRVUAV_heap->GetGPUDescriptorHandleForHeapStart();
//! 各Descriptorを作成(Descriptorの設定は省略)
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
pDevice->CreateConstantBufferView(&cbvDesc, CPUHandle);
//! オフセット
CPUHandle.ptr += IncrementSize;
//! ~~~~~~~~~~~~~~~~~~~~~~~~~
//! 以降のDescriptor作成は省略
//! ~~~~~~~~~~~~~~~~~~~~~~~~~
//! RootSignatureをセット
pCommandList->SetGraphicsRootSignature(pRootSignature.Get());
//! DescriptorHeapをセット
ID3D12DescriptorHeap* pHeaps[] = { pCBVSRVUAV_heap.Get() };
pCommandList->SetDescriptorHeaps(1, pHeaps);
//! RP3(RootDescriptorTable)に連続するDescriptorの先頭ハンドルを渡してセット
pCommandList->SetGraphicsRootDescriptorTable(RP3, GPUHandleBase);
cbuffer gCB1 : register(b1, space1)
{
//! 任意のパラメータ
}
cbuffer gCB2 : register(b2, space1)
{
//! 任意のパラメータ
}
Texture2D gTexture1 : register(t0, space1);
Texture2D gTexture2 : register(t1, space1);
Texture2D gTexture3 : register(t2, space1);
RWTexture2D gRWTexture1 : register(u0, space0);
RWTexture2D gRWTexture2 : register(u1, space0);
おわりに
かなり長い記事になってしまいました。
DirectX12のこの部分は自身も苦労したところなので誰かの手助けになれば幸いです。
記事の間違いや不明点、質問等あればお気軽にコメントください!
参考文献