Thursday, July 28, 2016

[DX12] GetCopyableFootprintsの謎に迫る

DirectX12ではテクスチャを作る時にメインメモリでもテクスチャでもない、中間バッファ(UPLOAD heap)を経由します。その際、ID3D12Device::GetCopyableFootprintsという関数で取得したレイアウトに従ってピクセルを中間バッファに格納します。

GetCopyableFootprints関数と共にいまいち存在意義がわかりにくい中間バッファがなぜ必要なのでしょうか。

答えはMSDNにありました。
https://msdn.microsoft.com/en-us/library/windows/desktop/dn899215(v=vs.85).aspx

テクスチャはキャッシュ効率を上げるためGPU上ではDDSファイルのように一直線(linear)に配置されておらず、non-linearな未知のレイアウト(unknown layout)として隠蔽されています。中間バッファの内容をそのGPUに都合の良い形式に変換してくれるのが、ID3D12GraphicsCommandList::CopyTextureRegionというわけです。

中間バッファに配置するピクセルもまたGPUに都合の良いようにアラインメントを揃える必要があり、そのための定数が定義されています。

  • D3D12_TEXTURE_DATA_PITCH_ALIGNMENT 
  • D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT

MSDNによると、サブリソース(Subresource、ミップマップやキューブマップなどのテクスチャの構成単位)ごとに512バイト境界、テクスチャの各Row(行、つまりX方向一列)は256バイト境界に揃っている必要があります。

GetCopyableFootprintsを呼び出すと、各サブリソースをどのように一次元のバッファに格納すればいいのかD3D12_PLACED_SUBRESOURCE_FOOTPRINT構造体で教えてくれます。つまり、GetCopyableFootprintsはただのヘルパー関数であって、デバイスを操作したりデバイスに問い合わせたりしません。また、GetCopyableFootprintsを使わず自力で配置しても構いません。

中間バッファは二次元や三次元ではない、Constant Bufferなどと同じただの一次元バッファ(D3D12_RESOURCE_DIMENSION_BUFFERで、D3D12_HEAP_TYPE_UPLOAD)として生成するのですが、その中にGPUが読めるようにサブリソースを間違いなく配置する必要があります。

ところで、DDSなどから読み込んだテクスチャは多くの場合既にRowが256バイト境界に揃っています。そこに注目するとこんな手抜きアップローダーも書けます。

void afWriteBuffer(const ComPtr<ID3D12Resource> res, const void* buf, int size)
{
void* p;
D3D12_RANGE readRange = {};
res->Map(0, &readRange, &p);
memcpy(p, buf, size);
D3D12_RANGE wroteRange = {0, (SIZE_T)size};
id->Unmap(0, &wroteRange);
}
ComPtr<ID3D12Resource> afCreateBuffer(int size, const void* buf)
{
D3D12_HEAP_PROPERTIES prop = { D3D12_HEAP_TYPE_UPLOAD, D3D12_CPU_PAGE_PROPERTY_UNKNOWN, D3D12_MEMORY_POOL_UNKNOWN, 1, 1 };
D3D12_RESOURCE_DESC desc = { D3D12_RESOURCE_DIMENSION_BUFFER, 0, (UINT64)size, 1, 1, 1, DXGI_FORMAT_UNKNOWN, { 1, 0 }, D3D12_TEXTURE_LAYOUT_ROW_MAJOR, D3D12_RESOURCE_FLAG_NONE };
UBOID o;
deviceMan.GetDevice()->CreateCommittedResource(&prop, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&o));
afWriteBuffer(o, buf, size);
return o;
}
void UploadTexture(ComPtr<ID3D12Resource> tex, const void* buf)
{
D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint;
UINT64 uploadSize, rowSizeInBytes;
const D3D12_RESOURCE_DESC destDesc = tex->GetDesc();
deviceMan.GetDevice()->GetCopyableFootprints(&destDesc, 0, 1, 0, &footprint, nullptr, &rowSizeInBytes, &uploadSize);
assert(rowSizeInBytes == footprint.Footprint.RowPitch); // It is safe in most case!
ComPtr<ID3D12Resource> uploadBuf = afCreateBuffer((int)uploadSize, buf);
D3D12_TEXTURE_COPY_LOCATION uploadBufLocation = { uploadBuf.Get(), D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT, footprint }, nativeBufLocation = { tex.Get(), D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX, 0 };
ID3D12GraphicsCommandList* list = deviceMan.GetCommandList();
D3D12_RESOURCE_BARRIER transition1 = { D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, D3D12_RESOURCE_BARRIER_FLAG_NONE, { tex.Get(), 0, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_COPY_DEST } };
list->ResourceBarrier(1, &transition1);
list->CopyTextureRegion(&nativeBufLocation, 0, 0, 0, &uploadBufLocation, nullptr);
D3D12_RESOURCE_BARRIER transition2 = { D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, D3D12_RESOURCE_BARRIER_FLAG_NONE, { tex.Get(), 0, D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE } };
list->ResourceBarrier(1, &transition2);
deviceMan.AddIntermediateCommandlistDependentResource(uploadBuf); // keep until the command queue has been flushed
}
assert(rowSizeInBytes == footprint.Footprint.RowPitch) の行でひっかかるテクスチャは横が256バイト単位でない場合です。実はある程度のX方向の大きさがあって、サイズがPower of Twoで、ミップマップを無視するならひっかからないはずです。

(AddIntermediateCommandlistDependentResourceは、コマンドバッファの実行が終わるまで中間バッファを保持しておくためのものです。今回の主題ではないので説明は省略します)

Saturday, July 23, 2016

[DX12] 実例で学ぶ ID3D12Fence ダブルバッファリング編

前回やった、フェンスが指定の値に達するまで待つ関数を再掲載します。

void WaitFenceValue(ComPtr<ID3D12Fence> fence, UINT64 value)
{
HANDLE fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
fence->SetEventOnCompletion(value, fenceEvent);
WaitForSingleObject(fenceEvent, INFINITE);
CloseHandle(fenceEvent);
}

フェンスは通常、渡す値をインクリメントしながら使います。現在GPUに渡したコマンドリストが全て終了するまで待機するには以下のようにします。

commandQueue->Signal(fence, fenceValue);
WaitFenceValue(fence, fenceValue++);
view raw flush_gpu.cpp hosted with ❤ by GitHub

fenceValueはアプリケーション開発者が管理します。UINT64型で宣言しておき、インクリメントしながら使います。

DX12ではダブルバッファリングを講じない限り、GPUが参照しているリソースをCPUが壊してしまわないように毎フレームの描画ごとにこれを実行する必要があります。例えば、ID3D12CommandAllocatorなどは、GPUが対象のコマンドリストを実行している間は破棄したり書き換えたりするとData Raceが発生することになります。それを防ぐ為にも上のコードを毎フレームを実行します。

しかし、こうすると毎フレームCPUとGPUがお互いの処理を待つような動作になるためフレームレートが低下します。そこで、GPUの処理中には保持しなければいけないリソースは2つずつ用意することにします。以下のようなクラスを用意すると便利です。

class FrameResources {
public:
~FrameResources();
ComPtr<ID3D12Resource> renderTarget;
ComPtr<ID3D12CommandAllocator> commandAllocator;
ComPtr<ID3D12Resource> constantBuffer;
ComPtr<ID3D12DescriptorHeap> srvHeap;
struct { char buf[256]; } *mappedConstantBuffer = nullptr;
UINT64 fenceValueToGuard = 0;
} frameResources[2];
このクラスのインスタンスを2つ作って、現在描画対象のバックバッファ(IDXGISwapChain3::GetCurrentBackBufferIndexで取得)によって参照を切り替えたいリソースを宣言に追加していきます。 fenceValueToGuardは該当フレームに対する描画が終了したときのfenceValueを保持しておくためのもので、フェンスの値がfenceValueToGuardを超えたらこれらのリソースに対するGPU側の処理が終了したと知ることができます。

constantBufferは前々回の記事で書いた巨大なConstant Bufferで、アプリケーションはここから256バイト単位で割り当てながら使うものです。mappedConstantBufferはconstantBufferをMapしっぱなしにしたメモリの先頭アドレスです。これらも2フレーム分作って置きます。ID3D12DescriptorHeapも同様な理由で前々回に動的に割り当てるようにしてあり、同様の理由で二重化するためここに追加します。

これで、Constant Bufferの書き換えに関してGPUを待機するコードがなくなりました…と、言いたいところですが、2フレーム前のGPUの処理が終わっていない場合を想定しなければなりません。

frameIndex = swapChain->GetCurrentBackBufferIndex();
FrameResources& res = frameResources[frameIndex];
WaitFenceValue(fence, res.fenceValueToGuard);
view raw begin_frame.cpp hosted with ❤ by GitHub
毎回フレームの描画開始前にFrameResourcesに保存しておいたfenceValueToGuardを取得してを呼び出します。ダブルバッファリングしているときはIDXGISwapChain3::GetCurrentBackBufferIndexが0と1が交互に来るので、結果的に2フレーム前のフェンスの値でWaitFenceValueを呼ぶことになります。

フレームの描画終了時には、以下のようにしてコマンドキューとfenceValueToGuard双方にfenceValue値を記録します。

FrameResources& res = frameResources[frameIndex];
commandQueue->Signal(fence.Get(), res.fenceValueToGuard = fenceValue++);
view raw end_frame.cpp hosted with ❤ by GitHub
こうすることで、GPUを待つことが完全になくなるわけではありませんが、真のダブルバッファリングが完成しました。

これで、どれだけ速くなるかみてみます。垂直同期をなくすためには、swapChain->Present(0, 0) と、最初のパラメータに0を指定します。うちのGTX 650ではダブルバッファリング前は400フレーム台だったのが500~600フレーム台にまで上がるようでした。

Wednesday, July 20, 2016

[DX12] 実例で学ぶ ID3D12Fence 基本編

DX11以前やOpenGLでは、基本的にCPUとGPU間でのいわゆるData Raceが生じないようにAPIレベルである程度保証してくれますが、DX12では同期を取る事は開発者に委ねられることになります。

そこで、GPUがどこまで実行したのか調べるのに使うフェンスですが、ID3D12Fence、HANDLE、ID3D12CommandQueue、それにUINT64型の変数と登場するオブジェクトが多くて分かり難いです。

結局、大雑把に何をするか書きだすと以下の2点に絞られます。

  • GPUが実行位置に合わせてフェンスの値をインクリメント
  • CPUはフェンスが期待した値になるまで待機する

GPUがフェンスの値をインクリメントと書きましたが、コマンドをGPUに送るのはCPUなのでその値もCPUから指定します。以下のようにコマンドリストの実行に混じって呼び出しているSignalがそれです。

commandQueue->ExecuteCommandLists(1, &A);
commandQueue->Signal(fence, 1);
commandQueue->ExecuteCommandLists(1, &B);
commandQueue->Signal(fence, 2);
commandQueue->ExecuteCommandLists(1, &C);
commandQueue->Signal(fence, 3);
A、B、Cがそれぞれ描画命令等が記録されたID3D12CommandListです。CPUから見て、フェンスの値が1になっていたらGPUでコマンドリストAの実行が終了しています。同様に2ならBが、3ならCの終了が保証されます。

GPUが設定したフェンスの値が指定値以上になるまで待つ関数はこんな感じです。

void WaitFenceValue(ComPtr<ID3D12Fence> fence, UINT64 value)
{
HANDLE fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
fence->SetEventOnCompletion(value, fenceEvent);
WaitForSingleObject(fenceEvent, INFINITE);
CloseHandle(fenceEvent);
}
ここでvalueに1を指定した場合、Aの実行が完了していなければAが終わるまで待機します。Aの実行が終わっている状態、すなわちフェンスの値が1以上になったらすぐに戻ってきます。「1」ではなく「1以上」であることは重要です。Aが終了したかを知りたいためで、Bの実行終了時の「2」であってもやはりすぐ戻ってこなければいけないからです。

また、Signal呼び出しは常にvalueをインクリメントしながら行うのが定石で、その値を管理するためUINT64型の変数を用意することになります。

同期に使うHANDLEは使い捨てにしています。生成と破棄のオーバーヘッドが気になるなら保持しておく方法もありますが、何か意味のある状態をグローバルに保持するかのように見えるため可読性が落ちる気がします。個人的には使い捨てがおすすめです。
DirectX-Graphics-Samplesでは保持するように書かれています。

上のWaitFenceValue関数は必要最低限のコードでしたが、通常はGetCompletedValueを組み合わせて使うようです。待機する必要が無い場合を切り分けると少しパフォーマンスが上がる為と思われます。

void WaitFenceValue(ComPtr<ID3D12Fence> fence, UINT64 value)
{
if (fence->GetCompletedValue() >= value) {
return;
}
HANDLE fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
fence->SetEventOnCompletion(value, fenceEvent);
WaitForSingleObject(fenceEvent, INFINITE);
CloseHandle(fenceEvent);
}

Saturday, July 9, 2016

DX12でGPUメモリを自分で管理する

今回のソースは https://github.com/yorung/dx12playground/tree/huge_global_constant です。

問題提起


前回シンプルなRoot Signatureをやってみましたが、毎回書き換えるConstant bufferのアドレスを固定してしまうのは何かと不都合です。

DX11までは度々以下のようなコードで描画することがありました。

ConstantBuffer cb;
for (obj : objs) {
void *p;
cb.Map(&p);
memcpy(p, obj.data, sizeof());
cb.Unmap();
Draw()
}
見た目のConstantBufferは1個なのですが、内部的なConstant Bufferがobjの数だけ確保され、それぞれ別のアドレスを持ちます(リネーム)。もし内部でも見た目通り1個のConstant Bufferしかなければ、Mapする時に前回のDrawが終わっていなければいけません。前回のDrawを待つなら遅くなりますし、待たなければGPUが参照中のメモリを壊す事態となります。

DX12で同じことをやるとGPUが参照中のメモリを壊してしまいます。複数のobjを描画するためにはobjの数だけConstantBufferを確保しなければならず、結局Constant bufferはアドレスを変えながらバインドしなければなりません。そうなると、Constant bufferのアドレスを保持するDescriptor Heapもまたバインドの数だけ必要になります。


GPUメモリのアロケータを作る


そこで、Constant BufferとDescriptor Heapを動的に確保するシステムを作ります。 

DX12のいいところは直接GPUのアドレスを指定できることです。巨大なConstant BufferやDescriptor Heapを1個ずつだけ作って、その中から適当な先頭アドレスをGPUに渡して描画、ということが出来るようになりました。

以下はその唯一のConstant Bufferを作るところです。

static const UINT maxConstantBufferBlocks = 1000;
ComPtr<ID3D12Resource> constantBuffer;
struct { char buf[0x100]; } *mappedConstantBuffer = nullptr;
int size = maxConstantBufferBlocks * 0x100;
D3D12_HEAP_PROPERTIES prop = { D3D12_HEAP_TYPE_UPLOAD, D3D12_CPU_PAGE_PROPERTY_UNKNOWN, D3D12_MEMORY_POOL_UNKNOWN, 1, 1 };
D3D12_RESOURCE_DESC desc = { D3D12_RESOURCE_DIMENSION_BUFFER, 0, (UINT64)size, 1, 1, 1, DXGI_FORMAT_UNKNOWN, { 1, 0 }, D3D12_TEXTURE_LAYOUT_ROW_MAJOR, D3D12_RESOURCE_FLAG_NONE };
device->CreateCommittedResource(&prop, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&constantBuffer));
D3D12_RANGE readRange = {};
HRESULT hr = constantBuffer->Map(0, &readRange, (void**)&mappedConstantBuffer);

Constant Bufferの先頭アドレスとサイズは256バイトのアラインメントの制限があります。ここでは256 x 1000バイト確保しました。また、MapしっぱなしにしてmemcpyだけすればGPUから変更が見えているというのもDX12のいいところです。

次は、唯一のDescriptor Heapを作ってみます。

static const UINT maxSrvs = 1024;
ComPtr<ID3D12DescriptorHeap> srvHeap;
const D3D12_DESCRIPTOR_HEAP_DESC srvHeapDesc = { D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, maxSrvs, D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE };
device->CreateDescriptorHeap(&srvHeapDesc, IID_PPV_ARGS(&srvHeap));

1024個作ってみます。個数に特に意味はありません。
使用者はここから必要な数だけ確保し、CBVやSRVを書き込んでAPIに渡します。

int DeviceManDX12::AssignDescriptorHeap(int numRequired)
{
if (numAssignedSrvs + numRequired > maxSrvs) {
assert(0);
return -1;
}
int head = numAssignedSrvs;
numAssignedSrvs = numAssignedSrvs + numRequired;
return head;
}
void DeviceManDX12::AssignSRV(int descriptorHeapIndex, ComPtr<ID3D12Resource> res)
{
D3D12_CPU_DESCRIPTOR_HANDLE ptr = srvHeap->GetCPUDescriptorHandleForHeapStart();
ptr.ptr += device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV) * descriptorHeapIndex;
device->CreateShaderResourceView(res.Get(), nullptr, ptr);
}
void DeviceManDX12::SetAssignedDescriptorHeap(int descriptorHeapIndex)
{
ID3D12DescriptorHeap* ppHeaps[] = { srvHeap.Get() };
deviceMan.GetCommandList()->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
D3D12_GPU_DESCRIPTOR_HANDLE addr = srvHeap->GetGPUDescriptorHandleForHeapStart();
addr.ptr += descriptorHeapIndex * device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
deviceMan.GetCommandList()->SetGraphicsRootDescriptorTable(0, addr);
}

AssignDescriptorHeapが確保関数で、先頭から順に返しているだけで、足し算しかしていません。使用者は「先頭から何番目を何個」という情報を知っているだけで十分だからです。ちなみに、numAssignedSrvsは毎フレーム0にリセットしています。

AssignSRVは確保したDescriptor HeapにSRVを書き込みます。CreateShaderResourceViewはDX11と名前が同じですが、何か新たにインスタンスが生成されたりするわけではなく、書き込むだけです。

最後に、SetAssignedDescriptorHeapでDescriptor HeapをGPUに渡します。今回のサンプルも前回のように単純化のためにDescriptor Tableは1個だけということにしています。descriptorHeapIndexを先頭位置として、何個のSRVやCBVを参照するかはDescriptor Tableの定義次第です。

以下はSRVの代わりにConstant Buffer開始位置をDescriptor Heapに書き込み、更にそれも唯一のConstant Bufferから動的に確保したものを使う関数です。

void DeviceManDX12::AssignConstantBuffer(int descriptorHeapIndex, const void* buf, int size)
{
int sizeAligned = (size + 0xff) & ~0xff;
int numRequired = sizeAligned / 0x100;
if (numAssignedConstantBufferBlocks + numRequired > maxConstantBufferBlocks) {
assert(0);
return;
}
int top = numAssignedConstantBufferBlocks;
numAssignedConstantBufferBlocks += numRequired;
memcpy(mappedConstantBuffer + top, buf, size);
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
cbvDesc.BufferLocation = constantBuffer->GetGPUVirtualAddress() + top * 0x100;
cbvDesc.SizeInBytes = sizeAligned;
assert((cbvDesc.SizeInBytes & 0xff) == 0);
D3D12_CPU_DESCRIPTOR_HANDLE ptr = srvHeap->GetCPUDescriptorHandleForHeapStart();
ptr.ptr += deviceMan.GetDevice()->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV) * descriptorHeapIndex;
deviceMan.GetDevice()->CreateConstantBufferView(&cbvDesc, ptr);
}
唯一のConstant Bufferから256バイトにアラインメントされた単位で先頭から確保しますが、これもただの足し算です。渡されたデータを書き込むのはmemcpyです。これでGPUから見えます。
最後に、CreateConstantBufferViewでDescriptor Heapに先頭アドレスを書き込んで完了です。

実際の描画コードはこんな感じです。

afSetPipeline(pipelineState, rootSignature);
Mat invVP = inv(matV * matP);
int descriptorHeapIndex = deviceMan.AssignDescriptorHeap(2);
deviceMan.AssignConstantBuffer(descriptorHeapIndex, &invVP, sizeof(invVP));
deviceMan.AssignSRV(descriptorHeapIndex + 1, texId);
deviceMan.SetAssignedDescriptorHeap(descriptorHeapIndex);
afDraw(PT_TRIANGLESTRIP, 4);
1個だけのDescriptor Tableは2つの連続したDescripor Heapを参照し、最初がCBV、次がSRVになっています。使用者がGPUメモリを管理する手間が減ってコードがすっきりしました。

まとめ


GPUメモリのアロケータをやってみました。

こうしてみると、最後にGPUに渡すのはGPUのアドレスになっていて、GPUメモリの管理方法はほぼアプリケーション開発者に委ねられているのがわかります。

上では複数オブジェクトを描画するときのDX11のリネームを例にあげましたが、単一のオブジェクトを描画していても次のフレームのDraw Callの時点で前のフレームをまだGPUが処理していた場合、リネームが発生します。
つまり、DX12ではダブルバッファリングやトリプルバッファリングを行う際にレンダーターゲットだけでなくコンスタントバッファもプログラマの責任で多重化します。

今回の作業はDX12を使ったダブルバッファリングやトリプルバッファリングの下準備でもあります。

今回はシンプルに実装するために全てを動的に確保するようにしましたが、マテリアル情報のような変化しないConstant Bufferを変化しないDescriptor Heapに入れるなど、変化の有無でDescriptor Tableを分けるような工夫が必要になりそうです。