DXGI とは何か
https://docs.microsoft.com/en-us/windows/desktop/direct3ddxgi/dx-graphics-dxgi
Microsoft DirectX Graphics Infrastructure (DXGI) とはグラフィックスアダプター(GPU)の列挙、
デバイスの作成
SwapChain の作成
// Describe and create the swap chain.
DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
swapChainDesc.BufferCount = FrameCount;
swapChainDesc.Width = m_width;
swapChainDesc.Height = m_height;
swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.BufferUsage =DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.SwapEffect =DXGI_SWAP_EFFECT_FLIP_DISCARD;
swapChainDesc.SampleDesc.Count = 1;
ComPtr<IDXGISwapChain1> swapChain;
ThrowIfFailed(factory->CreateSwapChainForHwnd(
m_commandQueue.Get(), // Swap chain needs the queue so that it can force a flush on it.
Win32Application::GetHwnd(),
&swapChainDesc,
nullptr,
nullptr,
&swapChain
));
CreateSwapChainForHwnd() の中に「SwapChain は強制的にフラッシュするために、Command Queue を必要とする」というコメントがあります。Window のリサイズ時とか、デバイスに変更が入った時とか?何かしらシステムから現在のコマンドへのフラッシュが必要なタイミングがあるのかな?
Command Queue の作成
// Describe and create the command queue.
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
m_device->CreateCommandQueue(&queueDesc,IID_PPV_ARGS(&m_commandQueue));
特筆するような指定はなさそう? Flags と Type ぐらい。
Command List の作成
// Create the command list.
ThrowIfFailed(m_device->CreateCommandList(0,D3D12_COMMAND_LIST_TYPE_DIRECT,m_commandAllocator.Get(), nullptr, IID_PPV_ARGS(m_commandList)));
// Command lists are created in the recordingstate, but there is nothing
// to record yet. The main loop expects it to beclosed, so close it now.
ThrowIfFailed(m_commandList->Close());
サンプルだと CreateCommandList した直後に、Close() している。コマンドリストは記録状態で作成されるが、作られた直後では記録するものが無いため閉じている。Command List は記録が終わった場合と、記録するものが無い場合などは Close() を呼び出して閉じておく必要がある。
MS ドキュメントには「記録中にオペレーティングシステムのメモリが不足した場合は E_OUTOFMEMORY を返す」とあるが、実際に E_OUTOFMEMORY メモリ不足が返されるのは、Close() が呼び出された時になるのか?どのコマンドによって E_OUTOFMEMORY が返されるか判別する方法はあるのかな?
Command List Type
コマンドリストにはタイプが存在している。
https://docs.microsoft.com/en-us/windows/desktop/api/d3d12/ne-d3d12-d3d12_command_list_type
DIRECT, BUNDLE, COMPUTE, COPY など。通常のグラフィックス用途に使用するコマンドリストと、Compute Shader で使用するコマンドリストなどは分ける必要がある? Command Allocator と Command List の両方に Type を指定できるが、両方は同じにしないといけないのかな。
Fence の作成
// Create synchronization objects.
ThrowIfFailed(m_device->CreateFence(0,D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_fence)));
m_fenceValue = 1;
// Create an event handle to use for framesynchronization.
m_fenceEvent = CreateEvent(nullptr, FALSE, FALSE,nullptr);
if (m_fenceEvent == nullptr)
{
ThrowIfFailed(HRESULT_FROM_WIN32(GetLastError()));
}
DirectX12 のサンプルだと m_fence(ID3D12Fence) と m_fenceValue(UINT64) と m_fenceEvent(HANDLE) の 3つのオブジェクトを使って同期を管理。
同期待ちに関しては、CreateEvent() と WaitForSignalObject() という昔からある WinAPI を使って処理している。
キーワードとして、下記「イベントオブジェクト」「イベントオブジェクトの使用」「プロセス間通信」などの MSDN を参照
DirectX12 では使われていないが、HANDLE は WinAPI の SetEvent などで signaled に移行できる。今回、後述するが、
m_fence->SetEventOnCompletion(fence, m_fenceEvent)
という、ID3D12Fence::SetEventOnCompletion 関数によって、m_fenceEvent を signaled に移行させている。Fence には値を書き込むことができ、Fence に書き込んだ値によって同期を取っている。
CreateEvent()
CreateEvent() は m_fenceEvent の HANDLE を生成します。
WaitForSingleObject()
指定したオブジェクト(HANDLE)の状態を確認します。オブジェクトのステートは nonsignaled と signaled の2つあり、signaled になるかタイムアウトするまで、そのスレッドが待機する。
https://docs.microsoft.com/en-us/windows/desktop/direct3d12/memory-management
DirectX12 は、メモリ管理と同期を自分で管理しなければいけない。ここで言うメモリ管理とは、CPU から GPU へのアップロードと GPU から CPU へのリードバック。
Command Queue の完了を待つ
App::WaitForPreviousFrame() アプリケーション側で実装している、前フレームの完了(同期)を待っている処理。この処理は、GPU 効率の良いやり方ではないみたい。
const UINT64 fence = m_fenceValue;
ThrowIfFailed(m_commandQueue->Signal(m_fence.Get(), fence));
m_fenceValue++;
// Wait until the previous frame is finished.
if (m_fence->GetCompletedValue() < fence)
{
ThrowIfFailed(m_fence->SetEventOnCompletion(fence, m_fenceEvent));
WaitForSingleObject(m_fenceEvent, INFINITE);
}
重要なのが、下記関数の組み合わせ。シグナルをコマンドキューに追加し、コマンドキューの完了を待っている。
- ID3D12CommandQueue::Signal(ID3D12Fence *pFence, UINT64 Value)
- GPU から Fence に対して、指定した値に更新することができる
- つまり Command Queue の完了を待つことができる
- CPU から Fence に値を設定したい場合は ID3D12Fence::Signal を使う
- ID3D12Fence::GetCompletedValue()
- ID3D12Fence::SetEventOnCompletion(UINT64 Value, HANDLE hEvent)
- Fence が特定の値に達したときに発生させるイベントを指定します。
この一連の処理は、まず CommandQueue が完了後に m_fence(ID3D12Fence) が fence(UINT64) の値に遷移するように Signal() を設定している。直後の GetCompletedValue() が返す値は、まだ前フレームの値のため、この条件分岐は成立して中に入る。if 内の SetEventOnCompletion() は描画完了後に m_fence(ID3D12Fence) の値が fence(UINT64) に達したら、m_fenceEvent(HANDLE) の値を signaled に設定する。
全てのコマンドが処理されて、m_fence(ID3D12Fence) が fence(UINT64) に達するまで WaitForSingleObject() でスレッドが待機する。これが一連の処理。難しい。
Populate CommandList
m_commandAllocator->Reset();
m_commandList->Reset(m_commandAllocator.Get(), m_pipelineState.Get());
m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));
D3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart(), m_frameIndex, m_rtvDescriptorSize);
const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));
m_commandList->Close();
Populate CommandList だと、コマンドリストに投入する。Poplate Data だと、(DBなどに)データを投入するという意味で使われたりするみたいです。
m_commandAllocator->Reset();
// Command list allocators can only be reset when the associated
// command lists have finished execution on the GPU; apps should use
// fences to determine GPU execution progress.
m_commandAllocator->Reset();
コマンドリストアロケータは、関連するコマンドの実行が終了した時にだけリセットすることができる。アプリケーションは、フェンスを使用して GPU の実行状況を判定する必要がある。
この関数はコマンドリストアロケータに関連づけられたメモリを再利用することを示します。
m_commandList->Reset()
// However, when ExecuteCommandList() is called on a particular command
// list, that command list can then be reset at any time and must be before
// re-recording.
m_commandList->Reset(m_commandAllocator.Get(), m_pipelineState.Get());
コマンドリストを初期状態にリセットします。リセット(再生成)するための、Command Allocator を指定する。
ResourceBarrier 1
// Indicate that the back buffer will be used as a render target.
m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));
バックバッファーに設定されていたメモリーを、レンダーターゲットとして使用することを指定する
ResourceBarrier 2
// Indicate that the back buffer will now be used to present.
m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));
レンダーターゲットとして設定されていたメモリーを Present として使用するように設定する。
Execute CommandList
// Execute the command list.
ID3D12CommandList* ppCommandLists[] = { m_commandList.Get() };
m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
コマンドリストに記録したコマンドをサブミットします。