Compute Shaderと頂点バッファの連携の話

はじめに

今回は,Compute Shaderで頂点バッファのデータを書き換えるという話題を扱います.

現在のレンダリングではモデルデータなどから,頂点バッファやインデックスバッファを構築してシェーダで描画処理を行いますが,絵を出すまでに1つの頂点バッファを複数回描画することがあります.

例えば,Deferred RenderingならG-bufferへの書き込みとShadow Map(複数枚のケースもあり)への書き込み,Forward RenderingでもShadowとColorとやはり複数回描画のケースがあります.この時に,頂点データがモデルデータから読み込んだまま使うのであればいいのですが,スキニングやBlendShapeなど変形するようなもののような場合,Shadow passやG-bufferパスなどパスごとに毎回変形用の処理を走らせるのは効率が良くないのでメモリに余裕があるのであれば一度だけ変形の計算をCompute Shaderで実行してキャッシュしてしまう,みたいな実装を行うケースはあります.

その他に,パーティクルなどの運動処理などをCompute Shaderで計算してその結果をそのまま頂点バッファとしてセットして描画したいというようなことはああるかもしれません.

今回は基礎的なサンプルではあるのですが,Direct3D12の以下のトピックについては理解が必須です.Direct3D11の使用経験があってCompute Shaderの使用経験がある,というのは必須ではないですがあると理解の手助けにはなると思います.

  • リソースバリアの使い方
  • Compute Shaderの基本的な使い方

サンプルについて

今回のサンプルは,頂点バッファに入っている座標や色をCompute Shader毎フレーム変えてキャッシュして,描画パスで再利用するものです.サンプルをシンプルにするためにCompute Shaderの中の処理は単純にしています.

今回のサンプルのリポジトリは下記になります.

https://github.com/shaderjp/ShaderjpDirect3D12Samples/tree/master/D3D12ComputeVertexBuffer

今回のサンプルはMicrosoftのDirect3D12サンプルのHello, constant buffers!をベースにしています.

https://github.com/microsoft/DirectX-Graphics-Samples/tree/master/Samples/Desktop/D3D12HelloWorld

サンプルの構成

上記サンプルからいくつかのファイルの名称を変えていますが,今回記述しているのは下記のファイルです.

  • C++コード
    • D3D12ComputeVertexBuffer.h
    • D3D12ComputeVertexBuffer.cpp
  • シェーダファイル
    • ComputeShader.hlsl
      • 頂点バッファ書き換え用
    • shaders.hlsl
      • 描画用

実装

C++のコードから順に触れていきます.

ヘッダーファイル(D3D12ComputeVertexBuffer.h)

ヘッダーファイルが下記です.基本的にはMicrosoftのサンプルをベースにしたものになっています.

今回は,Constant Bufferが2つ,Shader Resource Viewが1つ,Unorderd Access Viewが1つという構成でリソースを使うのですがDescriptor Heapへの登録するインデックスはeumc class SampleHeapIndexにしてます.あまりいい管理方法ではないですが,サンプルで決め打ちということで勘弁ください.

コマンドリスト,コマンドアロケータはCompute Shaderのパスと描画のパス用に2つあります.分ける必要性はないんですが,次回の記事のために事前に分けておきます.

Compute Shaderで読みだすConstant Bufferの構造体はComputeConstantBufferでoffsetは頂点座標に対するオフセットで,Colorは頂点カラーに乗算するための変数です.

#pragma once #include "DXSample.h" using namespace DirectX; // Note that while ComPtr is used to manage the lifetime of resources on the CPU, // it has no understanding of the lifetime of resources on the GPU. Apps must account // for the GPU lifetime of resources to avoid destroying objects that may still be // referenced by the GPU. // An example of this can be found in the class method: OnDestroy(). using Microsoft::WRL::ComPtr; class D3D12ComputeVertexBuffer : public DXSample { public: D3D12ComputeVertexBuffer(UINT width, UINT height, std::wstring name); virtual void OnInit(); virtual void OnUpdate(); virtual void OnRender(); virtual void OnDestroy(); private: static const UINT FrameCount = 2; // Descriptor Heap Index enum class SampleHeapIndex : int { PerDrawCbv = 0, ComputeCbv, SrcVertexSRV, CacheVertexUAV, SampleHeapIndexMax, }; struct Vertex { XMFLOAT3 position; XMFLOAT4 color; }; struct SceneConstantBuffer { XMFLOAT4 ; float padding[60]; // Padding so the constant buffer is 256-byte aligned. }; static_assert((sizeof(SceneConstantBuffer) % 256) == 0, "Constant Buffer size must be 256-byte aligned"); struct ComputeConstantBuffer { XMFLOAT4 offset; XMFLOAT4 color; float padding[56]; // Padding so the constant buffer is 256-byte aligned. }; static_assert((sizeof(ComputeConstantBuffer) % 256) == 0, "Constant Buffer size must be 256-byte aligned"); // Pipeline objects. CD3DX12_VIEWPORT m_viewport; CD3DX12_RECT m_scissorRect; ComPtr<IDXGISwapChain3> m_swapChain; ComPtr<ID3D12Device> m_device; ComPtr<ID3D12Resource> m_renderTargets[FrameCount]; ComPtr<ID3D12CommandAllocator> m_commandAllocator; ComPtr<ID3D12CommandAllocator> m_computeCommandAllocator; ComPtr<ID3D12CommandQueue> m_commandQueue; ComPtr<ID3D12RootSignature> m_rootSignature; ComPtr<ID3D12RootSignature> m_computeRootSignature; ComPtr<ID3D12DescriptorHeap> m_rtvHeap; ComPtr<ID3D12DescriptorHeap> m_resouceHeap; ComPtr<ID3D12PipelineState> m_pipelineState; ComPtr<ID3D12PipelineState> m_computePipelineState; // コマンドリスト. ComPtr<ID3D12GraphicsCommandList> m_commandList; ComPtr<ID3D12GraphicsCommandList> m_computeCommandList; UINT m_rtvDescriptorSize; UINT m_descripterIncrementSize; // App resources. ComPtr<ID3D12Resource> m_vertexBuffer; D3D12_VERTEX_BUFFER_VIEW m_vertexBufferView; ComPtr<ID3D12Resource> m_cacheVertexBuffer; D3D12_VERTEX_BUFFER_VIEW m_cacheVertexBufferView; // 描画用コンスタントバッファ. ComPtr<ID3D12Resource> m_PerDrawCBV; SceneConstantBuffer m_PerDrawData; UINT8* m_pCbvDataBegin; // Compute Shader用コンスタントバッファ ComPtr<ID3D12Resource> m_ComputeCbv; ComputeConstantBuffer m_ComputeCbvData; UINT8* m_ComputeCbvDataBegin; // Synchronization objects. UINT m_frameIndex; HANDLE m_fenceEvent; ComPtr<ID3D12Fence> m_fence; UINT64 m_fenceValue; void LoadPipeline(); void LoadAssets(); void CreateRootSignatures(); void CreatePipelineStates(); void CreateConstantBuffers(); void CreateVertexBuffer(); void PopulateComputeCommandList(); void PopulateCommandList(); void WaitForPreviousFrame(); };
Code language: C++ (cpp)

D3D12ComputeVertexBuffer.cpp

以降はcppの方の実装の解説をしていきます.

頂点バッファの生成

今回の記事の中で一番のポイントになる部分かと思います.

頂点バッファの生成時に注意する点ですが, Compute Shaderで読みだすためにShader Resource View(SRV)やUnordered Access View(UAV)に使用するためにはD3D12_HEAP_TYPE_DEFAULTを指定して生成する必要がある点です.

頂点バッファだとD3D12_HEAP_TYPE_UPLOADも指定が可能で,このフラグでCreateCommittedResourceで生成するとMapが使えるのでCPUから頂点データの書き替えができるのですが,リソースステートがD3D12_RESOURCE_STATE_GENERIC_READにしか指定ができません.そうなるとShader Resource View(SRV)やUnordered Access View(UAV)にTransition barrierでD3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCEやD3D12_RESOURCE_STATE_UNORDERED_ACCESSに変更ができないためCompute Shaderでの利用ができなくなります.

D3D12_HEAP_TYPE_DEFAULTでリソースを生成すると今度は頂点データをMapで設定ができないのでコマンドリストでコピー処理を行ってコマンドキューで実行する必要があります.

D3D12ComputeVertexBuffer::CreateVertexBuffer関数の中では以下のような処理が行われています.

  1. D3D12_HEAP_TYPE_DEFAULTの頂点バッファリソースにコピーするためのD3D12_HEAP_TYPE_UPLOADのリソースを作成
    • 一時頂点バッファ
    • コピーが終わったら破棄されます
  2. 1をコピーするD3D12_HEAP_TYPE_DEFAULTのリソースの作成
    • Compute ShaderからはSRVで読み出し
  3. 1の一時頂点バッファにMapを使って頂点データをセットアップ
    • 今回はPositionとColorを持つデータ
  4. Compute Shaderでの書き換えをキャッシュするD3D12_HEAP_TYPE_DEFAULTのリソースを作成する
    • Compute ShaderUAVとして使用する
    • リソース内のデータの初期化などは不要
  5. ID3D12GraphicsCommandList::CopyResourceを使って1のリソースを2のリソースにコピーする
    • コマンドキューに投入後にコピー処理が走る
    • コマンドの実行がキューで終了するまで待つ

コマンドリストを使ってコピーするという話が一見ややこしそうですが,同じサイズのD3D12_HEAP_TYPE_UPLOADとD3D12_HEAP_TYPE_DEFAULTの間でコピーするだけならそれほど面倒ではありません.大きなリソース確保してその中でオフセットとサイズを指定してということであればID3D12GraphicsCommandList :: CopyBufferRegionを使うとよいかと思います.

今回は頂点バッファの話だけしますが,インデックスバッファをCompute Shaderで操作したい場合も事情的には同じです.

<span class="has-inline-color has-black-color">// 頂点バッファの生成とコピー</span> void D3D12ComputeVertexBuffer::CreateVertexBuffer() { ComPtr<ID3D12Resource> vertexBufferHeap; UINT vertexBufferSize = 0; // Create the vertex buffer. { // Define the geometry for a triangle. 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 } } }; vertexBufferSize = sizeof(triangleVertices); // 一時頂点バッファ ThrowIfFailed(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(&vertexBufferHeap))); // SRVとして使用する頂点バッファ ThrowIfFailed(m_device->CreateCommittedResource( &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT), D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize), D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&m_vertexBuffer))); m_vertexBuffer->SetName(L"vertexBuffer"); // Copy the triangle data to the vertex buffer. UINT8* pVertexDataBegin; CD3DX12_RANGE readRange(0, 0); // We do not intend to read from this resource on the CPU. ThrowIfFailed(vertexBufferHeap->Map(0, &readRange, reinterpret_cast<void**>(&pVertexDataBegin))); memcpy(pVertexDataBegin, triangleVertices, sizeof(triangleVertices)); vertexBufferHeap->Unmap(0, nullptr); // Initialize the vertex buffer view. m_vertexBufferView.BufferLocation = m_vertexBuffer->GetGPUVirtualAddress(); m_vertexBufferView.StrideInBytes = sizeof(Vertex); m_vertexBufferView.SizeInBytes = vertexBufferSize; CD3DX12_CPU_DESCRIPTOR_HANDLE handle(m_resouceHeap->GetCPUDescriptorHandleForHeapStart()); handle.Offset(static_cast<int>(SampleHeapIndex::SrcVertexSRV), m_descripterIncrementSize); D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc; srvDesc.Format = DXGI_FORMAT_UNKNOWN; srvDesc.ViewDimension = D3D12_SRV_DIMENSION_BUFFER; srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; srvDesc.Buffer.FirstElement = 0; srvDesc.Buffer.NumElements = _countof(triangleVertices); srvDesc.Buffer.StructureByteStride = sizeof(Vertex); srvDesc.Buffer.Flags = D3D12_BUFFER_SRV_FLAG_NONE; m_device->CreateShaderResourceView(m_vertexBuffer.Get(), &srvDesc, handle); // キャッシュ用頂点バッファ(Compute Shaderの計算結果書き込み用) // 描画用にはこっちを使う ThrowIfFailed(m_device->CreateCommittedResource( &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT), D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, nullptr, IID_PPV_ARGS(&m_cacheVertexBuffer))); m_cacheVertexBuffer->SetName(L"cacheVertexBuffer"); m_cacheVertexBufferView.BufferLocation = m_cacheVertexBuffer->GetGPUVirtualAddress(); m_cacheVertexBufferView.StrideInBytes = sizeof(Vertex); m_cacheVertexBufferView.SizeInBytes = vertexBufferSize; D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc{}; uavDesc.Format = DXGI_FORMAT_UNKNOWN; uavDesc.ViewDimension = D3D12_UAV_DIMENSION_BUFFER; uavDesc.Buffer.NumElements = _countof(triangleVertices); uavDesc.Buffer.StructureByteStride = sizeof(Vertex); CD3DX12_CPU_DESCRIPTOR_HANDLE uavHandle(m_resouceHeap->GetCPUDescriptorHandleForHeapStart()); uavHandle.Offset(static_cast<int>(SampleHeapIndex::CacheVertexUAV), m_descripterIncrementSize); m_device->CreateUnorderedAccessView(m_cacheVertexBuffer.Get(), nullptr, &uavDesc, uavHandle); } // 一時頂点バッファからコピーを行う ThrowIfFailed(m_commandAllocator->Reset()); ThrowIfFailed(m_commandList->Reset(m_commandAllocator.Get(), m_pipelineState.Get())); // コピーの書き込み先としてのステートに変える m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_vertexBuffer.Get(), D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_RESOURCE_STATE_COPY_DEST)); m_commandList->CopyResource(m_vertexBuffer.Get(), vertexBufferHeap.Get()); // Shader Resouceのためのステートに変える(ComputeなのでNON PIXEL) m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_vertexBuffer.Get(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE)); ThrowIfFailed(m_commandList->Close()); ID3D12CommandList* ppCommandLists[] = { m_commandList.Get() }; m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists); // Create synchronization objects and wait until assets have been uploaded to the GPU. { ThrowIfFailed(m_device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_fence))); m_fenceValue = 1; // Create an event handle to use for frame synchronization. m_fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); if (m_fenceEvent == nullptr) { ThrowIfFailed(HRESULT_FROM_WIN32(GetLastError())); } // Wait for the command list to execute; we are reusing the same command // list in our main loop but for now, we just want to wait for setup to // complete before continuing. WaitForPreviousFrame(); } }
Code language: C++ (cpp)

Root Signatureの生成

今回,RootSignatureを2つ生成しています.描画用とCompute Shader用ですね.

// RootSignatureの生成 void D3D12ComputeVertexBuffer::CreateRootSignatures() { // 描画用Root Signature { D3D12_FEATURE_DATA_ROOT_SIGNATURE featureData = {}; // This is the highest version the sample supports. If CheckFeatureSupport succeeds, the HighestVersion returned will not be greater than this. featureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_1; if (FAILED(m_device->CheckFeatureSupport(D3D12_FEATURE_ROOT_SIGNATURE, &featureData, sizeof(featureData)))) { featureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_0; } CD3DX12_DESCRIPTOR_RANGE1 ranges[1]; CD3DX12_ROOT_PARAMETER1 rootParameters[1]; ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC); rootParameters[0].InitAsDescriptorTable(1, &ranges[0], D3D12_SHADER_VISIBILITY_VERTEX); // Allow input layout and deny uneccessary access to certain pipeline stages. D3D12_ROOT_SIGNATURE_FLAGS rootSignatureFlags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT | D3D12_ROOT_SIGNATURE_FLAG_DENY_HULL_SHADER_ROOT_ACCESS | D3D12_ROOT_SIGNATURE_FLAG_DENY_DOMAIN_SHADER_ROOT_ACCESS | D3D12_ROOT_SIGNATURE_FLAG_DENY_GEOMETRY_SHADER_ROOT_ACCESS | D3D12_ROOT_SIGNATURE_FLAG_DENY_PIXEL_SHADER_ROOT_ACCESS; CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC rootSignatureDesc; rootSignatureDesc.Init_1_1(_countof(rootParameters), rootParameters, 0, nullptr, rootSignatureFlags); ComPtr<ID3DBlob> signature; ComPtr<ID3DBlob> error; ThrowIfFailed(D3DX12SerializeVersionedRootSignature(&rootSignatureDesc, featureData.HighestVersion, &signature, &error)); ThrowIfFailed(m_device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&m_rootSignature))); } // Compute用Root Signature { D3D12_FEATURE_DATA_ROOT_SIGNATURE featureData = {}; // This is the highest version the sample supports. If CheckFeatureSupport succeeds, the HighestVersion returned will not be greater than this. featureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_1; if (FAILED(m_device->CheckFeatureSupport(D3D12_FEATURE_ROOT_SIGNATURE, &featureData, sizeof(featureData)))) { featureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_0; } CD3DX12_DESCRIPTOR_RANGE1 ranges[3]; CD3DX12_ROOT_PARAMETER1 rootParameters[1]; ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC); ranges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_VOLATILE); ranges[2].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_VOLATILE); rootParameters[0].InitAsDescriptorTable(_countof(ranges), ranges, D3D12_SHADER_VISIBILITY_ALL); D3D12_ROOT_SIGNATURE_FLAGS rootSignatureFlags = D3D12_ROOT_SIGNATURE_FLAG_DENY_VERTEX_SHADER_ROOT_ACCESS | D3D12_ROOT_SIGNATURE_FLAG_DENY_HULL_SHADER_ROOT_ACCESS | D3D12_ROOT_SIGNATURE_FLAG_DENY_DOMAIN_SHADER_ROOT_ACCESS | D3D12_ROOT_SIGNATURE_FLAG_DENY_GEOMETRY_SHADER_ROOT_ACCESS | D3D12_ROOT_SIGNATURE_FLAG_DENY_PIXEL_SHADER_ROOT_ACCESS; CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC rootSignatureDesc; rootSignatureDesc.Init_1_1(_countof(rootParameters), rootParameters, 0, nullptr, rootSignatureFlags); ComPtr<ID3DBlob> signature; ComPtr<ID3DBlob> error; ThrowIfFailed(D3DX12SerializeVersionedRootSignature(&rootSignatureDesc, featureData.HighestVersion, &signature, &error)); ThrowIfFailed(m_device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&m_computeRootSignature))); } }
Code language: C++ (cpp)

PipelineStateの作成

PipelineStateも2つ生成しています.描画用とCompute Shader用ですね.

Compute Shader用のPipelineState描画に比べると深度バッファやラスタライズステートやInput Layoutなどの設定がないので作成がシンプルですね.

// PipelineStateの生成 void D3D12ComputeVertexBuffer::CreatePipelineStates() { // 描画用パイプラインステート { ComPtr<ID3DBlob> vertexShader; ComPtr<ID3DBlob> pixelShader; #if defined(_DEBUG) // Enable better shader debugging with the graphics debugging tools. UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; #else UINT compileFlags = 0; #endif ThrowIfFailed(D3DCompileFromFile(GetAssetFullPath(L"shaders.hlsl").c_str(), nullptr, nullptr, "VSMain", "vs_5_0", compileFlags, 0, &vertexShader, nullptr)); ThrowIfFailed(D3DCompileFromFile(GetAssetFullPath(L"shaders.hlsl").c_str(), nullptr, nullptr, "PSMain", "ps_5_0", compileFlags, 0, &pixelShader, nullptr)); // Define the vertex input layout. 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 } }; // Describe and create the graphics pipeline state object (PSO). 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; ThrowIfFailed(m_device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&m_pipelineState))); } // Compute ShaderのコンパイルとPipelineState { ComPtr<ID3DBlob> computeShader; #if defined(_DEBUG) // Enable better shader debugging with the graphics debugging tools. UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; #else UINT compileFlags = 0; #endif ThrowIfFailed(D3DCompileFromFile(GetAssetFullPath(L"ComputeShader.hlsl").c_str(), nullptr, nullptr, "main", "cs_5_0", compileFlags, 0, &computeShader, nullptr)); D3D12_COMPUTE_PIPELINE_STATE_DESC psoDesc = {}; psoDesc.pRootSignature = m_computeRootSignature.Get(); psoDesc.CS = CD3DX12_SHADER_BYTECODE(computeShader.Get()); ThrowIfFailed(m_device->CreateComputePipelineState(&psoDesc, IID_PPV_ARGS(&m_computePipelineState))); } }
Code language: C++ (cpp)

Constant Bufferの更新

毎フレームのConstant Bufferの更新はOnUpdate関数で行っています.

// Update frame-based values. void D3D12ComputeVertexBuffer::OnUpdate() { const float translationSpeed = 0.005f; const float offsetBounds = 1.25f; static float time = 0.0f; // ピクセルシェーダに渡すConstant Bufferの値書き抱え m_PerDrawData.offset.x += translationSpeed; if (m_PerDrawData.offset.x > offsetBounds) { m_PerDrawData.offset.x = -offsetBounds; } memcpy(m_pCbvDataBegin, &m_PerDrawData, sizeof(m_PerDrawData)); // ComputeShaderに渡すConstant Bufferの値書き抱え m_ComputeCbvData.offset.x += translationSpeed; if (m_ComputeCbvData.offset.x > offsetBounds) { m_ComputeCbvData.offset.x = -offsetBounds; } float c = fabsf( sinf( time+=0.01f ) ); m_ComputeCbvData.color = XMFLOAT4(c, c, c, 1.0f); memcpy(m_ComputeCbvDataBegin, &m_ComputeCbvData, sizeof(m_ComputeCbvData)); }
Code language: C++ (cpp)

Compute Shaderのコマンドリストの設定

Compute Shaderのコマンドを積む関数です.

頂点バッファの読み込み元はSRV,キャッシュ先はUAVとしてシェーダでは解釈されます.計算が終わったあとは描画時に頂点バッファとしてバインドします.

この時にリソースステートの変更が必要でTransition barrierする必要がありますが,そちらは描画用のコマンドリスト積み込み関数で書いています.

void D3D12ComputeVertexBuffer::PopulateComputeCommandList() { ThrowIfFailed(m_computeCommandAllocator->Reset()); ThrowIfFailed(m_computeCommandList->Reset(m_computeCommandAllocator.Get(), m_computePipelineState.Get())); ID3D12DescriptorHeap* ppHeaps[] = { m_resouceHeap.Get() }; m_computeCommandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps); m_computeCommandList->SetComputeRootSignature(m_computeRootSignature.Get()); CD3DX12_GPU_DESCRIPTOR_HANDLE handle; handle = m_resouceHeap->GetGPUDescriptorHandleForHeapStart(); handle.Offset(static_cast<INT>(SampleHeapIndex::ComputeCbv), m_descripterIncrementSize); m_computeCommandList->SetComputeRootDescriptorTable(0, handle); m_computeCommandList->Dispatch(1, 1, 1); ThrowIfFailed(m_computeCommandList->Close()); }
Code language: C++ (cpp)

描画のコマンドリストの設定

描画用のコマンドリストを積んでる関数です.三角形を描画するだけなので特殊なことをしていません.

取り上げるところとしては,Compute Shaderで書き込みをしたUAVのリソースをTransition barrierでD3D12_RESOURCE_STATE_GENERIC_READにしているところですね.描画が終わったら次のフレームのCompute ShaderのためにD3D12_RESOURCE_STATE_UNORDERED_ACCESS戻します.

void D3D12ComputeVertexBuffer::PopulateCommandList() { // 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. ThrowIfFailed(m_commandAllocator->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. ThrowIfFailed(m_commandList->Reset(m_commandAllocator.Get(), m_pipelineState.Get())); // Set necessary state. m_commandList->SetGraphicsRootSignature(m_rootSignature.Get()); ID3D12DescriptorHeap* ppHeaps[] = { m_resouceHeap.Get() }; m_commandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps); m_commandList->SetGraphicsRootDescriptorTable(0, m_resouceHeap->GetGPUDescriptorHandleForHeapStart()); m_commandList->RSSetViewports(1, &m_viewport); m_commandList->RSSetScissorRects(1, &m_scissorRect); // 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)); m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_cacheVertexBuffer.Get(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_GENERIC_READ)); CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart(), m_frameIndex, m_rtvDescriptorSize); m_commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr); // Record commands. 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_cacheVertexBufferView); m_commandList->DrawInstanced(3, 1, 0, 0); // 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)); m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_cacheVertexBuffer.Get(), D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_RESOURCE_STATE_UNORDERED_ACCESS)); ThrowIfFailed(m_commandList->Close()); }
Code language: C++ (cpp)

コマンドリストのコマンドキューへの投入

PopulateComputeCommandList()とPopulateCommandList()でコマンドリストが構築出来たらキューに投入するだけですね.実行順序はコマンドキューに積まれた順になりますので,コマンドリストの積み込み自体はスレッド分けて並列化しても構いません.

void D3D12ComputeVertexBuffer::OnRender() { // Record all the commands we need to render the scene into the command list. PopulateComputeCommandList(); PopulateCommandList(); // Execute the command list. ID3D12CommandList* ppCommandLists[] = { m_computeCommandList.Get(), m_commandList.Get() }; m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists); // Present the frame. ThrowIfFailed(m_swapChain->Present(1, 0)); WaitForPreviousFrame(); }
Code language: C++ (cpp)

シェーダコード

今回のサンプルでは2つのシェーダファイルがあります.Compute Shaderが先に実行されてそのあとに,計算結果をキャッシュした頂点バッファを描画しています.

Compute Shader(ComputeShader.hlsl)

Compute Shaderでは,もともとの頂点バッファがg_srcVertexBufferにあたり,座標と色を変えた結果をg_destVertexBufferのバッファに書き出しています.

シェーダの中ではConstant Bufferの位置のオフセットは加算,色は乗算しています.今回は3頂点ではありますが,Compute Shaderはスレッドグループのスレッド32で実行しています.無駄にスレッドが起動しますが,頂点数より多くスレッド起動してますがちゃんと範囲外アクセスしても大丈夫です.どのみちたいていのGPUはWave数が32や64なので減らしても効率は良くならないとは思います.

struct VertexBuffer { float3 position; float4 color; }; cbuffer SceneConstantBuffer : register(b0) { float4 offset; float4 color; float4 padding[14]; }; StructuredBuffer<VertexBuffer> g_srcVertexBuffer : register(t0); RWStructuredBuffer<VertexBuffer> g_destVertexBuffer: register(u0); [numthreads(32, 1, 1)] void main( uint3 DTid : SV_DispatchThreadID ) { float3 destPosition = g_srcVertexBuffer[DTid.x].position; g_destVertexBuffer[DTid.x].position = destPosition + offset; g_destVertexBuffer[DTid.x].color = g_srcVertexBuffer[DTid.x].color * color; }
Code language: C++ (cpp)

描画用シェーダ(shaders.hlsl)

描画用シェーダは元のサンプルから一部変えてます.#if 0してるブロックがもともとのMicrosoftのサンプルのコードですね.元々は頂点シェーダにConstant Bufferからオフセットを与えて三角形の座標を頂点シェーダで動かすものですが今回はCompute Shaderで動かすのでオフセットの加算をやめています.

その他は,特別に触れることはありませんね.今回は透視変換なしで,スクリーン座標系にそのまま三角形出すだけです.

cbuffer SceneConstantBuffer : register(b0) { float4 offset; float4 padding[15]; }; struct PSInput { float4 position : SV_POSITION; float4 color : COLOR; }; PSInput VSMain(float4 position : POSITION, float4 color : COLOR) { PSInput result; #if 0 result.position = position + offset; #else result.position = position; #endif result.color = color; return result; } float4 PSMain(PSInput input) : SV_TARGET { return input.color; }
Code language: C++ (cpp)

注意点

2つのシェーダで注意するポイントが実はいくつかあります.Compute ShaderのVertexBuffer構造体とVSMain関数の引数の頂点データを見てください.何か違いがあることに気づくでしょうか?

positionがCompute Shaderではfloat3でVSMainの引数ではfloat4になっています.

ヘッダーファイルにあるC++のVertex構造体ではfloatが3つのサイズなのでCompute Shader側はC++で作成したバッファのレイアウトになっています.

頂点シェーダのVSMainの引数に入る値は,PipelineState作成時のInputLayoutを設定しましたが,頂点バッファからの値がInput Assembler (IA)ステージ通じて引数に設定されるため構造が変わっています.この辺りはDirect3D11でも同じですね.Compute Shaderではこの辺は注意点です.

https://docs.microsoft.com/ja-jp/windows/uwp/graphics-concepts/graphics-pipeline

この話とは別な話題ですが,この辺のInputLayoutやIAのオーバーヘッドがなくなる点がMesh Shaderの利点の1つとしてあげられています.

余談:Direct3D11との違い

今回のサンプルではCompute Shaderで頂点バッファのリソースへのアクセスにStructured Bufferを使って読み書きを行いました.Direct3D11では実はStructured Bufferのリソースを頂点バッファとしてバインドすることはできませんでした.そのためByteAddressBufferでアクセスする必要があるのですが,この部分は便利になりましたね.

おわりに

この記事を書いた時点ではMesh Shaderがあるのですが,いまの時点では頂点シェーダを使うシーンは多くあると思いますし,クラスターカリングなどのアルゴリズムを使わないで描画するようなケースでは頂点シェーダから描画した方がパフォーマンスがよいケースもあり,今回の知識はまだまだ使うことはあると思います.

次回は今回の実装のうちPopulateComputeCommandList関数のCompute ShaderのコマンドリストやコマンドキューをCompute用のコマンドリストやコマンドキューに変えて非同期Compute(Async Compute)に書き換えてみることを取り上げます.