Saturday, April 16, 2016

DirectX12のRoot Signatureをシンプルに定義する方法を考える

DirectX12はAPIが難解ですが、複雑さを隠蔽して使いやすくラップする方法を考えてみます。


例として、Photo Sphereと三角形ポリゴンを描画してみました。
ソースは https://github.com/yorung/dx12playground/tree/simple にアップしました。

Root SignatureとPSOの定義


Root Signatureをどう設計するかはMSDNの以下の記事が参考になります。

https://msdn.microsoft.com/en-us/library/windows/desktop/dn899123(v=vs.85).aspx

DirectX 12の難解さの第一はおそらくRoot Signatureだと思います。実行効率を最大化するには確かにRoot Signatureを理解するのは有用かもしれませんが、とりあえず三角ポリゴンを描きたいだけの時など、細かい事はとりあえず後回しにしたい時もあります。

そこで、必要最低限の機能でAPIを単純化したラッパーを設計してみます。それは、以下の要件を満たすものにします。
  • Descriptor Heap及びDescriptor Tableはそれぞれ1個のみ
  • static samplerのみを使う
  • SRV、CBV、static samplerの使用個数はRoot Signature毎に必要数定義できる

MSDN風に図にするとこうなります。


この仕様でRoot Signature及びPipeline State Object(PSO)まで定義するコードをラッパーで記述すると以下のようになります。

D3D12_DESCRIPTOR_RANGE descriptors[] = { // The descriptor table
CDescriptorCBV(0), // constant buffer view bound to register b0
CDescriptorSRV(0), // shader resource view bound to register t0
};
D3D12_STATIC_SAMPLER_DESC samplers[] = {
CSampler(0, D3D12_FILTER_MIN_MAG_LINEAR_MIP_POINT, D3D12_TEXTURE_ADDRESS_MODE_WRAP), // sampler bound to register s0
};
ComPtr<ID3D12RootSignature> rootSignature = afCreateRootSignature(dimof(descriptors), descriptors, dimof(samplers), samplers);
ComPtr<ID3D12PipelineState> pipelineState = afCreatePSO(shader, nullptr, 0, BM_NONE, DSM_DEPTH_CLOSEREQUAL_READONLY, CM_DISABLE, rootSignature);
view raw create_pso.cpp hosted with ❤ by GitHub
構造体の定義にラッパークラスを作って使っています。Shader Resource View(SRV)やConstant Buffer View(CBV)のD3D12_DESCRIPTOR_RANGEはレジスタのみ引数に取るように、サンプラーはレジスタとフィルタとアドレスモードを取るように単純化しています。
class CSampler : public D3D12_STATIC_SAMPLER_DESC
{
public:
CSampler(int shaderRegister, D3D12_FILTER samplerFilter, D3D12_TEXTURE_ADDRESS_MODE wrap)
{
Filter = samplerFilter;
AddressU = wrap;
AddressV = wrap;
AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
MipLODBias = 0;
MaxAnisotropy = 1;
ComparisonFunc = D3D12_COMPARISON_FUNC_NEVER;
BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK;
MinLOD = 0;
MaxLOD = D3D12_FLOAT32_MAX;
ShaderRegister = shaderRegister;
RegisterSpace = 0;
ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
}
};
class CDescriptorCBV : public D3D12_DESCRIPTOR_RANGE {
public:
CDescriptorCBV(int shaderRegister) {
RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
NumDescriptors = 1;
BaseShaderRegister = shaderRegister;
RegisterSpace = 0;
OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
}
};
class CDescriptorSRV : public D3D12_DESCRIPTOR_RANGE {
public:
CDescriptorSRV(int shaderRegister) {
RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
NumDescriptors = 1;
BaseShaderRegister = shaderRegister;
RegisterSpace = 0;
OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
}
};
Root Signatureを生成するラッパー関数は以下の通りです。本来複数個使えるD3D12_ROOT_PARAMETERを1個に制限しています。また、本来細かに設定すべきアクセス制限は単にD3D12_SHADER_VISIBILITY_ALLを指定しています。もちろん単純化のためですが、性能低下に繋がるようであれば今後の課題です。
ComPtr<ID3D12RootSignature> afCreateRootSignature(int numDescriptors, D3D12_DESCRIPTOR_RANGE descriptors[], int numSamplers, D3D12_STATIC_SAMPLER_DESC samplers[])
{
ComPtr<ID3D12RootSignature> rs;
ComPtr<ID3DBlob> signature;
ComPtr<ID3DBlob> error;
D3D12_ROOT_PARAMETER rootParameter = {};
rootParameter.ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
rootParameter.ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
rootParameter.DescriptorTable.NumDescriptorRanges = numDescriptors;
rootParameter.DescriptorTable.pDescriptorRanges = descriptors;
D3D12_ROOT_SIGNATURE_DESC rsDesc = { (UINT)(numDescriptors ? 1 : 0), &rootParameter, (UINT)numSamplers, samplers, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT };
HRESULT hr = D3D12SerializeRootSignature(&rsDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error);
assert(hr == S_OK);
hr = deviceMan.GetDevice()->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&rs));
assert(hr == S_OK);
return rs;
}

リソースの準備


テクスチャとコンスタントバッファを生成し、上のD3D12_DESCRIPTOR_RANGEの配列の順に並べてDescriptor Heapを生成します。
ComPtr<ID3D12Resource> texId = afLoadTexture(texFileName, texDesc);
ComPtr<ID3D12Resource> uboId = afCreateUBO(sizeof(Mat));
ComPtr<ID3D12Resource> srvs[] = {
uboId,
texId,
};
ComPtr<ID3D12DescriptorHeap> heap = afCreateDescriptorHeap(dimof(srvs), srvs);
Descriptor Heapを生成するヘルパー関数は以下のように定義しています。内部でSRVかCBVかでCreateShaderResourceViewとCreateConstantBufferViewに分岐していますが、呼び出し側は区別せずID3D12Resourceの配列として渡します。
ComPtr<ID3D12DescriptorHeap> afCreateDescriptorHeap(int numSrvs, ComPtr<ID3D12Resource> srvs[])
{
ComPtr<ID3D12DescriptorHeap> heap;
D3D12_DESCRIPTOR_HEAP_DESC srvHeapDesc = {};
srvHeapDesc.NumDescriptors = numSrvs;
srvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
srvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
HRESULT hr = deviceMan.GetDevice()->CreateDescriptorHeap(&srvHeapDesc, IID_PPV_ARGS(&heap));
assert(hr == S_OK);
for (int i = 0; i < numSrvs; i++) {
D3D12_RESOURCE_DESC desc = srvs[i]->GetDesc();
auto ptr = heap->GetCPUDescriptorHandleForHeapStart();
ptr.ptr += deviceMan.GetDevice()->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV) * i;
if (desc.Dimension == D3D12_RESOURCE_DIMENSION_BUFFER) {
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
cbvDesc.BufferLocation = srvs[i]->GetGPUVirtualAddress();
cbvDesc.SizeInBytes = (UINT)desc.Width;
assert((cbvDesc.SizeInBytes & 0xff) == 0);
deviceMan.GetDevice()->CreateConstantBufferView(&cbvDesc, ptr);
} else {
deviceMan.GetDevice()->CreateShaderResourceView(srvs[i].Get(), nullptr, ptr);
}
}
return heap;
}

描画


Root SignatureとPSO、Descriptor Heapを指定し、Draw Callして描画完了です。
テクスチャ及びコンスタントバッファのバインドはDescriptor Heapを1個指定するだけで終了します。これは、Descriptor TableとしてSRV及びCBVの並びが既に定義されているので、定義通りにSRV及びCBVを並べたDescriptor Heapを指定するだけでバインドが終わります。
ID3D12GraphicsCommandList* list = deviceMan.GetCommandList();
list->SetPipelineState(pipelineState.Get());
list->SetGraphicsRootSignature(rootSignature.Get());
afSetDescriptorHeap(heap);
afWriteBuffer(uboId, &invVP, sizeof(invVP));
list->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
list->DrawInstanced(numVertices, 1, 0, 0);
view raw draw.cpp hosted with ❤ by GitHub
こうして見ると、Descriptor HeapはSRVとCBVを配列の中に保持するコンテナとも言えますし、描画時の各種リソースのバインドをリソース生成時に完了させる媒体とも言えそうです。

Descriptor Heapのバインドは以下のヘルパー関数を使っています。
void afSetDescriptorHeap(ComPtr<ID3D12DescriptorHeap> heap)
{
ID3D12GraphicsCommandList* list = deviceMan.GetCommandList();
ID3D12DescriptorHeap* ppHeaps[] = { heap.Get() };
list->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
list->SetGraphicsRootDescriptorTable(0, heap->GetGPUDescriptorHandleForHeapStart());
}
view raw heap_binder.cpp hosted with ❤ by GitHub
バインドするDescriptor Heapを1個に制限しています。また、GetGPUDescriptorHandleForHeapStartで先頭アドレスをオフセットを指定したりせずそのまま渡すようになっています。

今回、頂点バッファは使っていません。頂点4つは頂点シェーダーで適当に作っています。通常ならここに頂点バッファやインデックスバッファのバインドが加わり、PSOの定義にInput Layoutの定義が入りますが、本題ではないので省略します。

考察


短いコードでRoot Signatureを定義できる反面、いくつかの課題があります。

1. Constant bufferのダブルバッファリングが考慮されていない
2. per draw, per material, per frameのConstant bufferの使い分けをどうするか

今回、Constant bufferのダブルバッファリングが為されていないので、毎回GPU側の描画の終了を待ってからConstant Bufferを書き換えています。ダブルバッファリングのためには描画の度にConstant Bufferを切り替える必要があるのですが、現在のConstant Bufferはテクスチャと一緒に1つのDescriptor Heapに紐付けられています。テクスチャは書き換えないリソースで、Constant Bufferは通常書き換えるリソースと見た場合、Descriptor Heapも書き換えの有無で分割するのがよいかもしれません。

また、通常レンダリングエンジンの設計上、per frame, per materialのように適用範囲に合わせてConstant Bufferを分割することが多いと思いますが、適用範囲毎にDescriptor Heapも分割するのが効率が良さそうに見えるので、今回のようにDescriptor Heapを1個に限ってしまうと不都合です。

逆に言うとパフォーマンスを気にしないのであれば必要十分な機能を有しているので、勉強用、プロトタイピング用としてDescriptor Table1個で済ますのは方法としてアリと言えるのではないでしょうか。

No comments:

Post a Comment