Saturday, September 3, 2016

[DX12] VBV、IBVはAPI呼出し後すぐ破棄してもOK

頂点バッファの定義で、例えば以下のように2つの変数を定義して使うのを見かけると思います。

ID3D12Resource* vertexBuffer;
D3D12_VERTEX_BUFFER_VIEW vertexBufferView;
view raw DefineABuffer.h hosted with ❤ by GitHub
頂点バッファはID3D12ResourceとD3D12_VERTEX_BUFFER_VIEW、インデックスバッファはID3D12ResourceとD3D12_INDEX_BUFFER_VIEWが必要です。一つのリソースに2つの宣言が必要で煩雑なのですが、実はVIEWの宣言は省略してもよさそうです。

DirectX11ではSRVやDSVなど、GPUから参照するリソースに付加情報を付けてバインドする構造をViewと呼んでいました。 DirectX12になってD3D12_VERTEX_BUFFER_VIEW(VBV)やD3D12_INDEX_BUFFER_VIEW(IBV)という構造体がいわゆるViewの一種に加わりましたが、Viewの概念が若干変わったようです。DX11ではViewはその実体がGPU側にあるのかCPU側にあるのかは隠蔽されていました。それが、DX12ではGPU側なのかCPU側なのかは明確に区別されます。

この辺は、マイクロソフトのYouTubeのチャンネル「Microsoft DirectX 12 and Graphics Education」に登録されている「Resource Binding in DirectX 12 (pt.1)」に詳しいです。



8:30付近で出る表と13:00の解説によると、CBV、SRV、UAV、SAMPLERはGPUに配置され、それ以外のIBV、VBV、SOV、RTV、DSVはCPU側にのみ存在し、更にはドライバもVIEW(descriptor)の複製をコマンドリストに積むのでVIEWへの参照を保持しないとのことです。

MicrosoftのDirectX-Graphics-Samplesサンプル中、D3D12HelloWorldソリューションでは頂点バッファのID3D12Resourceに加えてD3D12_VERTEX_BUFFER_VIEWやD3D12_INDEX_BUFFER_VIEWを保持していますが、1つのバッファに対して2つの変数を管理するのは二度手間に感じます。

この二度手間を解決するため、上の動画から得られた事実を利用します。アプリケーションはID3D12Resourceのみ保持し、VIEWはスタック上に毎回作ることにします。例えばインデックスバッファをコマンドリストに積むためにこんな関数を作れば、今後D3D12_INDEX_BUFFER_VIEW構造体の存在を忘れてしまうことができます。

void SetIndexBuffer(ID3D12GraphicsCommandList* list, ID3D12Resource* indexBuffer)
{
D3D12_RESOURCE_DESC desc = indexBuffer->GetDesc();
D3D12_INDEX_BUFFER_VIEW indexBufferView = { indexBuffer->GetGPUVirtualAddress(), (UINT)desc.Width, AFIndexTypeToDevice };
list->IASetIndexBuffer(&indexBufferView);
}
同じDirectX-Graphics-SamplesでもMiniEngineの中では、スタック上に作ったD3D12_VERTEX_BUFFER_VIEWやD3D12_INDEX_BUFFER_VIEWを直接コマンドリストに流すようになっているようです。

ところで、D3D12_INDEX_BUFFER_VIEWとD3D12_VERTEX_BUFFER_VIEWは構造体でしたが、RTVやDSVはID3D12DescriptorHeapの形をとっています。これは実に奇妙に見えます。なぜなら、同じID3D12DescriptorHeapを使うSRVやCBVの場合、DescriptorがGPUメモリ上に存在し、シェーダーから参照されるため、プログラマはフェンスを駆使してID3D12DescriptorHeapの寿命管理を行わなければいけません。ところが、RTVやDSVはAPI呼び出し後はコマンドリストが実行前でもID3D12DescriptorHeapを破棄してもRTVやDSVを書き換えても構わないという事です。

内部の実装がも異なるオブジェクトが同じID3D12DescriptorHeapの形を取っているため、混乱の元になりそうです。

ともかく、この事実を利用して管理するオブジェクトを減らすことができます。以下はID3D12DescriptorHeapをスタック上に作ってOMSetRenderTargetsした後バッファをクリアする例です。ComPtrなのでID3D12DescriptorHeapは関数を抜けると消滅します。

残念ながら以下のコードはDebug Layerがエラーを出します。コマンドリスト実行終了前にID3D12DescriptorHeapを解放していないか検出するためにDebug Layerが参照しているようです。Debug Layerを外せば問題無さそうですが、まともにデバッグできないのでやめたほうがよさそうです。ただし、ID3D12DescriptorHeapを使わないVBVやIBVは前述の通りMiniEngineでもスタックに作って即破棄いるのでAPI呼出し後の破棄が合法と見て間違いなさそうです。

// DON'T DO THIS!!
// It seems working fine, however the DX12 debug layer treat this as following error.
//
// "An ID3D12DescriptorHeap object referenced in a command list ID3D12GraphicsCommandList Object "..." was
// deleted prior to executing the command list. This is invalid and can result in application instability.
// [ EXECUTION ERROR #921: OBJECT_DELETED_WHILE_STILL_IN_USE]"
//
ComPtr<ID3D12DescriptorHeap> rtvHeap, dsvHeap;
const D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = { D3D12_DESCRIPTOR_HEAP_TYPE_RTV, 1 };
device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&rtvHeap));
D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = rtvHeap->GetCPUDescriptorHandleForHeapStart();
device->CreateRenderTargetView(renderTarget.Get(), nullptr, rtvHandle);
const D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = { D3D12_DESCRIPTOR_HEAP_TYPE_DSV, 1 };
device->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&dsvHeap));
D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle = dsvHeap->GetCPUDescriptorHandleForHeapStart();
device->CreateDepthStencilView(depthStencil.Get(), nullptr, dsvHandle);
commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, &dsvHandle);
const float clearColor[] = { 0.0f, 0.2f, 0.3f, 1.0f };
commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
commandList->ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);

使い捨てにするとDebug Layerでエラーが出ますが、ID3D12DescriptorHeapをそれぞれ1個だけ作って毎回書き換えるように使うのはエラーにはなりません。上の動画の13:00に大丈夫と書いてあります。(もちろんSRVやCBVでこれをやるとアウトです)

ComPtr<ID3D12DescriptorHeap> rtvHeap, dsvHeap;
ID3D12GraphicsCommandList* commandList;
ID3D12Device* device;
void CreateHeaps()
{
D3D12_DESCRIPTOR_HEAP_DESC rtvDesc = { D3D12_DESCRIPTOR_HEAP_TYPE_RTV, 1 };
device->CreateDescriptorHeap(&rtvDesc, IID_PPV_ARGS(&rtvHeap));
D3D12_DESCRIPTOR_HEAP_DESC dsvDesc = { D3D12_DESCRIPTOR_HEAP_TYPE_DSV, 1 };
device->CreateDescriptorHeap(&dsvDesc, IID_PPV_ARGS(&rtvHeap));
}
void SetRenderTarget(ComPtr<ID3D12Resource> color, ComPtr<ID3D12Resource> depthStencil)
{
D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle = dsvHeap->GetCPUDescriptorHandleForHeapStart();
D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = rtvHeap->GetCPUDescriptorHandleForHeapStart();
// this overwrites descriptor heaps in every single call of SetRenderTarget, but it's no problem.
// because, the descriptor heap for RTV or DSV is in CPU side, not in VRAM like SRV and CBV.
// refer https://youtu.be/Uwhhdktaofg for more detail.
device->CreateRenderTargetView(color.Get(), nullptr, rtvHandle);
device->CreateDepthStencilView(depthStencil.Get(), nullptr, dsvHandle);
commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
commandList->ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, &dsvHandle);
}

DX12はアプリケーションがローレベルを意識するAPIなので、RTVやDSVもD3D12_VERTEX_BUFFER_VIEWのようにただの構造体にしてもよかったのではないでしょうか。そうすればGPU側はID3D12DescriptorHeap、CPU側はただの構造体、という風に住み分けができて分かりやすくなると思うからです。この辺は、おそらくはDX11時代のID3D11RenderTargetViewやID3D11DepthStencilView等の歴史的な経緯も関係がありそうです。