DirectX12 で画面クリアまで (祝)

DirectX12 (D3D12) を使ってようやく画面のクリアだけエラーも起こらず動作させることができるようになりました。そもそも SDK が正式版ではないので、今後変更されることもあると思います。それでも気になる人のために、コードをさらしてみたいと思います。Windows8.x以降、 Win32 アプリケーションは肩身が狭くなってきた感がありますが、ここでのサンプルは相変わらず Win32アプリケーションで作成してみました。
以下の内容は既に古くなっています。以下のコードでも動きますが、よりコード量の少なくなったこちらの記事を参考にどうぞ
本内容は既に最新環境では正常にコンパイルできません。ご注意ください 2015/5

必要なもの

VisualStudio 2013 でも作業できそうに思いますが、すべて最新の状況で作業しました。作業している最中に Windows10 は 10049 に上がっちゃいましたが。以下のものをインストールしました。

  • VisualStudio 2015 CTP6
  • VisualStudio Tools for Windows10 Technical Preview
  • Windows10 Technical Preview 10041

d3d12_clear

とりあえず上記のように画面のクリア処理がうまく動くようになりました。結構手間取ったので喜びもひとしおです!

DirectX12 プログラミング開始

通常の Win32 アプリケーションを作成します。ウィンドウの作成関連は割愛して、DirectX12 部分だけ抜粋していきます。

なおMSDNドキュメント等が不十分なことと手探りで動作させたため、以下の内容は間違っていることが多く含まれる可能性があります。その点にはご注意ください。

デバイスとスワップチェインを作る

まずは ID3D12Device を作成します。以下のような感じで作成でき、 DirectX11 とそんなに変わりません。

D3D12CreateDevice(
  nullptr, 
  D3D_DRIVER_TYPE_HARDWARE, 
  D3D12_CREATE_DEVICE_DEBUG, 
  D3D_FEATURE_LEVEL_11_1, 
  D3D12_SDK_VERSION, 
  __uuidof(ID3D12Device), 
  (void**)&gpDevice
  );

スワップチェインの作成はドキュメント記載の方法ではうまくきませんでした。ドキュメントでは D3D12Device から QueryInterface で IDXGIDevice2 がとれるらしいのですが。
またスワップチェインの作成にあたり、 ID3D12CommandQueue が必要になります。適当に値を設定して作成します。スワップチェインの設定パラメーターも適当です。

IDXGIFactory2* dxgiFactory = nullptr;
CreateDXGIFactory2( 
  0, 
  __uuidof(IDXGIFactory2),
  (void**)&dxgiFactory);

D3D12_COMMAND_QUEUE_DESC descCommandQueue;
ZeroMemory(&descCommandQueue, sizeof(descCommandQueue) );
descCommandQueue.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
descCommandQueue.Priority = 0;
descCommandQueue.Flags = D3D12_COMMAND_QUEUE_NONE;
gpDevice->CreateCommandQueue( 
  &descCommandQueue, 
  __uuidof(ID3D12CommandQueue),
  (void**)&pCommandQueue);

DXGI_SWAP_CHAIN_DESC swapChainDesc;
ZeroMemory(&swapChainDesc, sizeof(swapChainDesc));
swapChainDesc.BufferCount = 1;
swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.OutputWindow = gHwnd;
swapChainDesc.SampleDesc.Count = 1;
swapChainDesc.Windowed = TRUE;
swapChainDesc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;

IDXGISwapChain* pSwapChain = nullptr;
dxgiFactory->CreateSwapChain( 
  pCommandQueue, 
  &swapChainDesc, 
  &pSwapChain);

ID3D12PipelineState を作成する

ここが難関でした。 D3D12_GRAPHICS_PIPELINE_STATE_DESC に値を設定して作成するのですが、設定すべき値がわからないものが多かったです。さらには設定する内容が多いのと、準備が必要だったりと手間がかかりました。

必要だったものをあげると以下のような感じでした。

  • VertexShaderのシェーダーバイナリ
  • InputLayoutの情報
  • ID3D12RootSignatureオブジェクト

ID3D12RootSignature は結構手間でした。この RootSignature もいくつかの情報を準備して作る必要がありました。 D3D12_ROOT_SIGNATURE 構造体にセットしたあとで、シリアライズしてそのバイナリをデバイスに渡して生成するという流れでした。
以下のようにして生成させましたが、値の各所は適当です。実際に使い始める際には再確認が必要ですね。。

D3D12_ROOT_PARAMETER rootParameter[4];
D3D12_DESCRIPTOR_RANGE DescriptorRange[2];
ID3D12RootSignature* pRootSignature = nullptr;
DescriptorRange[0].Init(D3D12_DESCRIPTOR_RANGE_SAMPLER, 2, 0);
DescriptorRange[1].Init(D3D12_DESCRIPTOR_RANGE_SRV, 5, 2);
rootParameter[0].InitAsDescriptorTable(1, &DescriptorRange[0]);
rootParameter[1].InitAsDescriptorTable(1, &DescriptorRange[1]);
rootParameter[2].InitAsConstantBufferView(7);
rootParameter[3].InitAsConstants(1, 0);

D3D12_ROOT_SIGNATURE rootSignature;
rootSignature.Init(4, rootParameter);
rootSignature.Flags = D3D12_ROOT_SIGNATURE_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
ID3DBlob* pRootSigBlob = nullptr;
ID3DBlob* pErrorBlob = nullptr;
D3D12SerializeRootSignature(
  &rootSignature, 
  D3D_ROOT_SIGNATURE_V1, 
  &pRootSigBlob, 
  &pErrorBlob);

gpDevice->CreateRootSignature(
  1, 
  pRootSigBlob->GetBufferPointer(), 
  pRootSigBlob->GetBufferSize(), 
  __uuidof(ID3D12RootSignature), 
  (void**)&pRootSignature);

これが出来ればあとは D3D12_GRAPHICS_PIPELINE_STATE_DESC に値を設定して ID3D12PipelineState を作成出来ると思います。

コマンドリスト( ID3D12GraphicsCommandList ) を作成

描画用のコマンドリストを作成します。これには ID3D12GraphicsCommandList を用います。 ID3D12CommandList ではない点に注意です。
コマンドリストには ID3D12CommandAllocator と ID3D12DescriptorHeap が必要になるのでこれも作ります。

gpDevice->CreateCommandAllocator(
  D3D12_COMMAND_LIST_TYPE_DIRECT, 
  __uuidof(ID3D12CommandAllocator), 
  (LPVOID*)&commandAllocators[0]);
gpDevice->CreateCommandList(
  0, 
  D3D12_COMMAND_LIST_TYPE_DIRECT, 
  commandAllocators[0],
  pPipelineState, 
  __uuidof(ID3D12GraphicsCommandList), 
  (void**)&pCommandList);

ID3D12DescriptorHeap はなぜか1つではエラーを取り切ることが出来なかったので2種類のものを作成して対処しました。作成後はこれをコマンドリストにセットします。

ID3D12DescriptorHeap* pDescriptorHeap[2] = { nullptr };
D3D12_DESCRIPTOR_HEAP_DESC descHeap;
ZeroMemory(&descHeap, sizeof(descHeap));
descHeap.NumDescriptors = 1;
descHeap.Type = D3D12_RTV_DESCRIPTOR_HEAP;
gpDevice->CreateDescriptorHeap(
  &descHeap, 
  __uuidof(ID3D12DescriptorHeap), 
  (void**)&pDescriptorHeap[0]);

descHeap.Type = D3D12_CBV_SRV_UAV_DESCRIPTOR_HEAP;
descHeap.Flags = D3D12_DESCRIPTOR_HEAP_SHADER_VISIBLE;
gpDevice->CreateDescriptorHeap(
  &descHeap, 
  __uuidof(ID3D12DescriptorHeap), 
  (void**)&pDescriptorHeap[1]);

pCommandList->SetDescriptorHeaps(&pDescriptorHeap[1], 1);

もう1つの DescriptorHeap は次のステップで使用します。

プライマリのRenderTargetViewの準備

ここは予想付く人も多いでしょう。スワップチェインから作成していきます。このとき D3D12_CPU_DESCRIPTOR_HANDLE なるものが要求され、これを準備するのに先ほど用意したもう1つの DescriptorHeap を使います。

ID3D12Resource* pRenderTarget = nullptr;
D3D12_CPU_DESCRIPTOR_HANDLE cpuDescriptor;
pSwapChain->GetBuffer(
  0, 
  __uuidof(ID3D12Resource), 
  (void**)&pRenderTarget);
cpuDescriptor = pDescriptorHeap[0]->GetCPUDescriptorHandleForHeapStart();
gpDevice->CreateRenderTargetView(
  pRenderTarget, 
  NULL, 
  cpuDescriptor);

画面クリアコマンドを積んで描画する

ようやく準備が出来たのでクリアのコマンドを積んで、実行させるところまでを示します。
以下のコードを見てもらうとわかるように、クリアの前にリソースの状態を変更するためのコマンドを積む必要があります。そしてクリア関連のコマンドを設定した後、Present のためにリソースの状態を変更するコマンドを積んでます。
一通りコマンドを積んだ後は、 Close して、コマンドキューにリストを投入します。このとき ExecuteCommandList が ID3D12GraphicsCommandList をそのまま受け付けないので、CommandListCast を介して渡すようにしています。これはヘッダを探っていたら発見したものです。

D3D12_RESOURCE_BARRIER_DESC descBarrier;
ZeroMemory(&descBarrier, sizeof(descBarrier));
descBarrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
descBarrier.Transition.pResource = pRenderTarget;
descBarrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
descBarrier.Transition.StateBefore = D3D12_RESOURCE_USAGE_PRESENT;
descBarrier.Transition.StateAfter = D3D12_RESOURCE_USAGE_RENDER_TARGET;
pCommandList->ResourceBarrier(1, &descBarrier);

clearColor[0] = 0.5f * sin(count*0.05f) + 0.5f;
clearColor[1] = 0.5f * sin(count*0.10f) + 0.5f;
pCommandList->RSSetViewports(1, &viewport);
pCommandList->ClearRenderTargetView(cpuDescriptor, clearColor, NULL, 0);

descBarrier.Transition.StateBefore = D3D12_RESOURCE_USAGE_RENDER_TARGET;
descBarrier.Transition.StateAfter = D3D12_RESOURCE_USAGE_PRESENT;
pCommandList->ResourceBarrier(1, &descBarrier);

pCommandList->Close();
pCommandQueue->ExecuteCommandLists(1, CommandListCast(&pCommandList));
pSwapChain->Present(1, 0);

その他

上記のままでは1度目のループのみうまく動きます。実際にはアロケーターを2つ作ってそれをフレームごとに交換、かつコマンドリストをリセットという処理を行って毎フレーム違う色のクリアをしています。
1つでやっていた場合、どんどんメモリが増えていくことと、DirectXデバッグレイヤから怒られるというこれらの問題を解決することが出来ませんでした。

まとめ

なんとか画面のクリアまではたどり着くことが出来ました。ヘッダを参照しつつ進める必要がありましたが、それだけではなく D3D12 でどのように処理が必要になったかなど把握する必要がありました。むしろこの後者の時間が多かったように思います。また情報が断片的だったもつらかった気がします。


今回のプログラムは GitHub のほうにあげてあります。気になる人はそちらから全貌をどうぞ。 http://github.com/techlabxe/testD3D12

上記リポジトリの内容は既に新しい内容で更新済みです。よりコード量の少なく実現できるこちらの記事「DirectX12 で画面クリアまでの最小サンプル実装」を参考にどうぞ。

また正しい情報をお持ちの方、是非とも教えてくださいませ。
本内容は既に最新環境では正常にコンパイルできません。ご注意ください 2015/5

DirectX プログラミング
すらりんをフォローする
すらりん日記

コメント

  1. フェ より:

    歓びもなおさら→喜びもひとしお

タイトルとURLをコピーしました