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フレーム台にまで上がるようでした。

No comments:

Post a Comment