はじめに
DirectX12完全に理解したいとおもったんでDirectX12始めました。
実行画面
プログラムの流れ
- 初期化
- 繰り返す
- 更新
- 描画
- 破棄
初期化
Windowを作成する
省略します。
####パイプラインを初期化する
- デバッグレイヤーを有効にする
UINT dxgiFactoryFlags = 0;
ComPtr<ID3D12Debug> debugController;
if(SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
{
debugController->EnableDebugLayer();
}
D3D12GetDebugInterfaceでデバッグレイヤーを取得してEnableDebugLayer()でデバッグレイヤーを有効化する。
このプログラムではスマートポインタ使ってるのでRelease()を呼ぶ必要ないです
- デバイスを作成する
ComPtr<IDXGIFactory4> factory;
CreateDXGIFactory2(dxgiFactoryFlags, IID_PPV_ARGS(&factory));
if(m_useWarpDevice)
{
ComPtr<IDXGIAdapter> warpAdapter;
factory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter));
D3D12CreateDevice(
warpAdapter.Get(),
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&m_device));
}
else
{
ComPtr<IDXGIAdapter1> hardwareAdapter;
GetHardwareAdapter(factory.Get(), &hardwareAdapter);
D3D12CreateDevice(
hardwareAdapter.Get(),
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&m_device));
}
D3D12CreateDevice でデバイスを作成します。
- コマンドキューを作成する
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));
コマンドキューとは実行の待ち行列です。
ID3D12CommandQueue::ExecuteCommandListsを呼び出して、指定した順にGPUが実行すべき命令のリスト(CommandList)を実行します。
D3D12_COMMAND_LIST_TYPEでは送信するコマンドリストのタイプを指定します。
D3D12_COMMAND_LIST_TYPE_DIRECT 汎用。
D3D12_COMMAND_LIST_TYPE_COMPUTE 計算用に使用。
D3D12_COMMAND_LIST_TYPE_COPY コピーコマンドのみ実行可能。
- スワップチェインを作成する
DXGI_SWAP_CHAIN_DESC swapChainDesc = {};
swapChainDesc.BufferCount = FrameCount;
swapChainDesc.BufferDesc.Width = m_width;
swapChainDesc.BufferDesc.Height = m_height;
swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
swapChainDesc.OutputWindow = m_hwnd;
swapChainDesc.SampleDesc.Count = 1;
swapChainDesc.Windowed = TRUE;
ComPtr<IDXGISwapChain> swapChain;
factory->CreateSwapChain(m_commandQueue.Get(),
&swapChainDesc,
&swapChain);
swapChain.As(&m_swapChain);
フレームバッファの描画側と表示側を切り替えるもの。
- ディスクリプタヒープを作成します。
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
rtvHeapDesc.NumDescriptors = FrameCount;
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
m_device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&m_rtvHeap));
m_rtvDescriptorSize = m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
ディスクリプタヒープからディスクリプタを格納するメモリが確保されます
ディスクリプタとはリソースパラメーターを定義したデータです。GPUメモリに確保すると同時にシステムにもミラーが作られ、CPUとGPU両方からアクセスできます
ディスクリプタが必要なリソースはConstantBufferView,SRV,UAV,RTV,DSV,Samplerです
- レンダーターゲットビューを作成する
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart());
// フロントとバッグであわせて2つのRTVを作成
for (UINT n = 0; n < FrameCount; n++)
{
m_swapChain->GetBuffer(n, IID_PPV_ARGS(&m_renderTargets[n]));
m_device->CreateRenderTargetView(m_renderTargets[n].Get(), nullptr, rtvHandle);
rtvHandle.Offset(1, m_rtvDescriptorSize);
}
- コマンドアロケーターを作成します
m_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&m_commandAllocator));
コマンドアロケーターは、コマンドリストに割り当てられたメモリを管理します。
コマンドアロケーターのコマンドタイプはコマンドリストのタイプと同じでなければならないのでD3D12_COMMAND_LIST_TYPE_DIRECTをつかいます。複数のコマンドリストから1つのコマンドアロケーターをバインドすることもできます。
また、コマンドアロケータによって割り当てられたメモリを再利用するためにResetを呼ぶ必要がありますが、フリースレッドではないので複数のスレッドから同じアロケータで同時に呼び出すことはできません。
リソースを初期化
- ルートシグネチャを作成する
CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc;
rootSignatureDesc.Init(0,
nullptr,
0,
nullptr,
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
ComPtr<ID3DBlob> signature;
ComPtr<ID3DBlob> error;
D3D12SerializeRootSignature(&rootSignatureDesc,
D3D_ROOT_SIGNATURE_VERSION_1,
&signature,
&error);
m_device->CreateRootSignature(0,
signature->GetBufferPointer(),
signature->GetBufferSize(),
IID_PPV_ARGS(&m_rootSignature));
ルートシグネチャとは、シェーダーに渡すパラメーターのフォーマットを決めるものです。
リソースとシェーダの対応付けを行います。
ルートシグネチャの定義の内容が一致していれば同じオブジェクトとしてみなされます。
シェーダーと同じようにhlslに記述することもできます。
- シェーダーをコンパイル
ComPtr<ID3DBlob> vertexShader;
ComPtr<ID3DBlob> pixelShader;
#if defined(_DEBUG)
UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#else
UINT compileFlags = 0;
#endif
D3DCompileFromFile(L"shaders.hlsl", nullptr, nullptr, "VSMain", "vs_5_0", compileFlags, 0, &vertexShader, nullptr);
D3DCompileFromFile(L"shaders.hlsl", nullptr, nullptr, "PSMain", "ps_5_0", compileFlags, 0, &pixelShader, nullptr);
- インプットレイアウト
D3D12_INPUT_ELEMENT_DESC inputElementDescs[] =
{
{"POSITION", 0 ,DXGI_FORMAT_R32G32B32_FLOAT , 0 , 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0 },
{ "COLOR" , 0, DXGI_FORMAT_R32G32B32A32_FLOAT , 0 , 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0 }
};
頂点の情報を定義します。
- パイプラインステートオブジェクトを作成する
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
psoDesc.InputLayout = { inputElementDescs, _countof(inputElementDescs) };
psoDesc.pRootSignature = m_rootSignature.Get();
psoDesc.VS = CD3DX12_SHADER_BYTECODE(vertexShader.Get());
psoDesc.PS = CD3DX12_SHADER_BYTECODE(pixelShader.Get());
psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
psoDesc.DepthStencilState.DepthEnable = FALSE;
psoDesc.DepthStencilState.StencilEnable = FALSE;
psoDesc.SampleMask = UINT_MAX;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
psoDesc.SampleDesc.Count = 1;
m_device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&m_pipelineState));
パイプラインステートオブジェクトとはインプットレイアウト、シェーダ、各ステートなどをまとめたオブジェクトです。
DirectX12ではシェーダやステートを単体で設定を変えるAPIはありません。1つでも変える場合はパイプラインステートオブジェクトをもう一度つくることになります。
- コマンドリストを作成する
m_device->CreateCommandList(0,
D3D12_COMMAND_LIST_TYPE_DIRECT,
m_commandAllocator.Get(),
m_pipelineState.Get(),
IID_PPV_ARGS(&m_commandList));
コマンドリストには描画や状態を変更する命令を記録していきます。記録された時点ではまだ実行されずExecuteCommandListsを呼び出して実行します。
- コマンドリストを閉じる
m_commandList->Close();
- 頂点バッファを作成する
Vertex triangleVertices[] =
{
{ { 0.0f, 0.25f * m_aspectRatio, 0.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } },
{ { 0.25f, -0.25f * m_aspectRatio, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } },
{ { -0.25f, -0.25f * m_aspectRatio, 0.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } }
};
const UINT vertexBufferSize = sizeof(triangleVertices);
m_device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&m_vertexBuffer));
ここで、三角形を作ります
- 頂点データを頂点バッファにコピー
UINT8* pVertexDataBegin;
CD3DX12_RANGE readRange(0, 0);
m_vertexBuffer->Map(0, &readRange, reinterpret_cast<void**>(&pVertexDataBegin));
memcpy(pVertexDataBegin, triangleVertices, sizeof(triangleVertices));
m_vertexBuffer->Unmap(0, nullptr);
- 頂点バッファビューを初期化する
m_vertexBufferView.BufferLocation = m_vertexBuffer->GetGPUVirtualAddress();
m_vertexBufferView.StrideInBytes = sizeof(Vertex);
m_vertexBufferView.SizeInBytes = vertexBufferSize;
コマンドリストに登録する際に使います。
-フェンスを作成して初期化する
m_device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_fence));
m_fenceValue = 1;
フェンスはコマンドリストが完了したかどうかを知るために使います。コマンドリストが実行されている間はコマンドアロケーターのReset呼び出しはよくないです。ID3D12CommandAllocator::Resetを呼ぶとコマンドアロケータで記録されたコマンドを持つコマンドリストをGPUがもう実行しないと判断します。
- フレーム同期に使用するイベントハンドルを作成する
m_fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
- GPUが終了するのを待ちます
更新
必要に応じて、定数、頂点、インデックスのバッファやその他を変更します
void Direct3D12HelloTriangle::OnUpdate()
{
// TODO:更新処理
}
描画
コマンドリストを入力
- コマンドリストアロケータをリセット
m_commandAllocator->Reset();
m_commandList->Reset(m_commandAllocator.Get(), m_pipelineState.Get());
- グラフィックスのルートシグネチャを設定, ビューポートとシーザーを設定
m_commandList->SetGraphicsRootSignature(m_rootSignature.Get());
m_commandList->RSSetViewports(1, &m_viewport);
m_commandList->RSSetScissorRects(1, &m_scissorRect)
- バックバッファがレンダーターゲットとして使用されることを示すリソースバリアを設定する。
m_commandList->ResourceBarrier(1,
&CD3DX12_RESOURCE_BARRIER::Transition(m_renderTargets[m_frameIndex].Get(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET));
- コマンドを記録します
const float clearColor[] = { 0.0f,0.2f,0.4f,1.0f };
m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
m_commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
m_commandList->IASetVertexBuffers(0, 1, &m_vertexBufferView);
m_commandList->DrawInstanced(3, 1, 0, 0);
- 使っているバックバッファを指定する
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();
コマンドリストへの記録が完了したことを合わします。
- コマンドリストを実行
ID3D12CommandList* ppCommandLists[] = { m_commandList.Get() };
m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
- フレームを表示
m_swapChain->Present(1, 0);
- コマンドの実行されるのを待つ。(ここに書いてあるフレームを完了するのを待つ処理は非効率。)
void Direct3D12HelloTriangle::WaitForPreviousFrame()
{
const UINT64 fence = m_fenceValue;
m_commandQueue->Signal(m_fence.Get(), fence);
m_fenceValue++;
if (m_fence->GetCompletedValue() < fence)
{
m_fence->SetEventOnCompletion(fence, m_fenceEvent);
WaitForSingleObject(m_fenceEvent, INFINITE);
}
m_frameIndex = m_swapChain->GetCurrentBackBufferIndex();
}
ID3D12CommandQueue :: Signalを呼び出してフェンスを進めると、値が更新されます。フェンスの値をチェックして、次の処理を開始できるかどうかを判断できます。
破棄
- GPUが終了するのを待ち, イベントを閉じます。
void Direct3D12HelloTriangle::OnDestroy()
{
WaitForPreviousFrame();
CloseHandle(m_fenceEvent);
}
おわり
長かった。かなり駆け足だったけど流れだけは説明しました。
ソース:DirectX12Triangle