Direct3D12 覚え書き

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()
    • Fence の現在の値を取得する
  • 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);

コマンドリストに記録したコマンドをサブミットします。

Direct3D12 Synchronization

Fence

単純なアプリであれば、CPU-GPU 間の同期を取るだけなので、Fence は1つあれば十分か。D3D12Fence と HANDLE と uint32_t の変数をセットで使用する。

複数の Queue を作ったときの同期も Fence で行う。

ResourceBarrier

リソースの状態を管理する。例えば RenderTarget として使っていたリソースを、ShaderResoruce として使うような場合に、どのような順序でタスクを実行するか(この場合だと先に ShaderResource タスクが実行されると結果がおかしくなるため) ResourceBarrier を使って状態が変わる事をドライバに通知する。

CommandList->ResourceBarrier() に設定する。該当のリソースが、CommandList に積んだ後に、StateBefore から StateAfter に指定した状態として使うことを明示する。

SwapChain なら、StatePresent -> StateRenderTarget として、ResoruceBarrier を設定して、レンダーターゲットに指定する。描画コマンドが一通り終わったら、最後に StateRenderTarget -> StatePresent の ResourceBarrier を積み、Queue を Submit (ExecuteCommandLists) して Present できる。

Direct3D12 Command

一般的な初期化の流れとして、Queue -> Allocator -> List の順番で作成する。Command 周りの話は GPUView Tool などで追うと動きが分かりやすいか。

CommandQueue

通常は Type Direct で作成する。Direct であれば Render と Compute の両方のコマンドを追加することが出来るが、非同期 Compute などを行いたい場合は、明確に Type Compute を指定した Queue を作って Fense で同期する。

CommandAllocator

サンプルによっては、SwapChain の枚数分 Allocator を作成している。

CommandList

複数スレッドから追加する場合は、複数個作成するのがセオリーか?

Direct3D12 Descriptor

Root Signature

Root Signature にはサイズがあるみたいだが何の制限?
Descriptor Table を経由すると、ポインタアクセス分だけ遅くなる?

Descriptor

Descriptor は D3D12 でリソースをシェーダーにバインドする(シェーダーからアクセスするためにリソースを設定する)単位。必ずしも一対一になる訳では無いが、CBV や SRV などの1リソースが 1Descriptor になる。

Descriptor Heap の作成

まずは、Descriptor のメモリー領域となる Descriptor Heap を作成する。専用の構造体に、これから作成する Descriptor Heap の個数とどのようなタイプ(CBV, SRV, UAV や Sampler) として使うのかを指定して、Device::CreateDescriptorHeap() に渡してヒープを作成する。

RTV や CBV など、Type によって Heap は分けて作成する。

作成時に Flags に対して、ShaderVisible フラグがある。CBV, SRV, UAV など、シェーダーから参照される Descriptor の場合は指定する?

Descriptor Handle の作成

作った Descriptor Heap から Descriptor Handle を得る。これは Heap::GetCPUDescriptorHandleForHeapStart() を呼び出して取得する。一つの Descriptor Handle から複数の Descriptor を作成できるが、その際は、 Device::GetDescriptorHandleIncrementSize() を使って、次の Descriptor Handle へのポインタ位置を計算して取得する。

D3D11 の時は View を作成するときは専用の ShaderResourceView などを渡していたが、D3D12 では Descriptor Handle を渡す。CreateConstantBufferView() や CreateRenderTargetView() の引数には、この Descriptor Handle を設定する。

RenderTargetView

  • CommandList->OMSetRenderTargets()
  • CommandList->ClearRenderTargetView()

などを呼び出すときは、RTV の Descriptor Heap から Descriptor Handle を取得して、その Handle を設定する。

これらのメソッドの引数は D3D12_CPU_DESCRIPTOR_HANDLE* になる。D3D11 の時は ID3D11RenderTargetView* だった。

SetDescriptorHeaps

サンプルの中で、CommandList->SetDescriptorHeaps() を呼び出している箇所があった。しかし CBV Heap を設定していたが、RTV heap は設定していなかった。SetDescriptorHeaps() は いつ何を設定する必要があるんだ…?

DirectX Raytracing 学習リソース

DirectX Raytracing – The life of a ray tracing kernel.
https://cedil.cesa.or.jp/cedil_sessions/view/1825

microsoft/DirectX-Graphics-Samples
https://github.com/microsoft/DirectX-Graphics-Samples

NVIDIAGameWorks/DxrTutorials
https://github.com/NVIDIAGameWorks/DxrTutorials

確率密度関数からモンテカルロ積分まで
https://qiita.com/Ushio/items/0040b3c74a480c46c80c

Introduction to DirectX RayTracing
http://intro-to-dxr.cwyman.org/

Ray Tracing Resources Page
http://www.realtimerendering.com/raytracing.html

DirectX12 のブックマーク

Direct3D 12 graphics
https://docs.microsoft.com/en-us/windows/desktop/direct3d12/direct3d-12-graphics

Direct3D 12 特集
https://www.isus.jp/games/directx-12/

Learning DirectX 12 – Lesson 1 – Initialize DirectX 12
https://www.3dgep.com/learning-directx12-1/
Lesson 1~3 まであり、分かりやすいです

Microsoft DirectX 12 and Graphics Education
https://www.youtube.com/channel/UCiaX2B8XiXR70jaN7NK-FpA/videos

HelloD3D12 DirectX 12 Sample
https://gpuopen.com/gaming-product/hellod3d12-directx-12-sdk-sample/

DirectX Raytracing

A Gentle Introduction To DirectX Raytracing
http://cwyman.org/code/dxrTutors/dxr_tutors.md.html

DX12 Ray Tracing Tutorials
https://news.developer.nvidia.com/dx12-raytracing-tutorials/

DX12 Raytracing tutorial – Part 1
https://developer.nvidia.com/rtx/raytracing/dxr/DX12-Raytracing-tutorial-Part-1

DX12 Raytracing tutorial – Part 2
https://developer.nvidia.com/rtx/raytracing/dxr/DX12-Raytracing-tutorial-Part-2