TOP > PROGRAM

バインドレスリソース

1 はじめに

今回は,Microsoftがいつまでたってもサンプルを作らないので,手探りでShader Model 6.6で追加されたDynamic Resourceというやつを試してみました。いわゆるバインドレステクスチャとかバインドレスリソースというやつです。

バインドレスリソース
図 1: バインドレスリソース

2 準備

基本的には,RESOURCE_BINDING_TIER3とSHADER_MODEL_6_6をサポートしているかどうかチェックして,ルートシグニチャに専用のフラグを立てます。このフラグの定義ですが,Agility SDKに含まれているので,まずはAgility SDKの設定を行います。

Agility SDKの設定ですが,Nugetでパッケージを落として,必須設定項目を設定すればいいだけです。Getting Started with the Agility SDKに書かれている通りに設定していきましょう。

まず,NugetでDirectX SDKなどで検索して,Microsoft.Direct3D.D3D12 - DirectX 12 Agility SDKというようなパッケージを探して,インストールを行います。ソリューションエクスプローラーからソリューションを右クリックして,「ソリューションのNuGetパッケージの管理 (N)…」を選択します。ページが表示されたらページ内の左上にある「参照」を選択して,検索ボックスに「DirectX SDK」をタイピングします。下図のように候補一覧の中にMicrosoft.Direct3D.D3D12というものが出てくるはずなので,これを選択し,インストールをします。

NuGetからインストール
図 2: NuGetからインストール

続いて,Agility SDK parametersを設定します。今回はバージョン4のSDKを使うため,App.cppに次のコードを入れました。

// For Agility SDK
extern "C" { __declspec(dllexport) extern const UINT D3D12SDKVersion = 4; }
extern "C" { __declspec(dllexport) extern const char* D3D12SDKPath = u8".\\D3D12\\"; }

これで,Agility SDKの設定は終了です。

3 バインドレスリソース

続いて,バインドレスリソースの設定と使い方についてです。設定の仕方はいたって簡単です。ルートシグニチャを設定するときに D3D12_ROOT_SIGNATURE_CBV_SRV_UAV_HEAP_DIRECTLY_INDEXED を設定すれば,CBV/SRV/UAVのディスクリプタヒープからインデックスを使ってシェーダ上でアクセスできるようになります。これまで設定していたD3D12_ROOT_PARAMETERなどはバインドレスリソースを使う場合は,不要になるので設定する必要がなくなります。ちなみに,サンプラーについてもフラグがあり,D3D12_ROOT_SIGNATURE_FLAG_SAMPLER_HEAP_DIRECTLY_INDEXEDを設定してあげれば同様にインデックスで参照できるようになります。
 今回のサンプルの実装で用いた設定は次のような感じです。

    // ルートシグニチャを生成.
    {
        // 静的サンプラーの設定.
        D3D12_STATIC_SAMPLER_DESC sampler = {};
        sampler.Filter           = D3D12_FILTER_MIN_MAG_MIP_LINEAR;
        sampler.AddressU         = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
        sampler.AddressV         = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
        sampler.AddressW         = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
        sampler.MipLODBias       = 0;
        sampler.MaxAnisotropy    = 0;
        sampler.ComparisonFunc   = D3D12_COMPARISON_FUNC_NEVER;
        sampler.BorderColor      = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK;
        sampler.MinLOD           = 0.0f;
        sampler.MaxLOD           = D3D12_FLOAT32_MAX;
        sampler.ShaderRegister   = 0;
        sampler.RegisterSpace    = 0;
        sampler.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;

        // ルートシグニチャの設定.
        D3D12_ROOT_SIGNATURE_DESC desc = {};
        desc.NumParameters     = 0;
        desc.pParameters       = nullptr;
        desc.NumStaticSamplers = 1;
        desc.pStaticSamplers   = &sampler;
        desc.Flags             = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT 
                               | D3D12_ROOT_SIGNATURE_FLAG_CBV_SRV_UAV_HEAP_DIRECTLY_INDEXED;

        asdx::RefPtr<ID3DBlob> pSignature;
        asdx::RefPtr<ID3DBlob> pError;

        // シリアライズする.
        hr = D3D12SerializeRootSignature(
            &desc,
            D3D_ROOT_SIGNATURE_VERSION_1,
            pSignature.GetAddress(),
            pError.GetAddress() );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : D3D12SerializeRootSignataure() Failed." );
            return false;
        }

        // ルートシグニチャを生成.
        hr = m_pDevice->CreateRootSignature(
            0,
            pSignature->GetBufferPointer(),
            pSignature->GetBufferSize(),
            IID_PPV_ARGS(m_pRootSignature.GetAddress()) );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : ID3D12Device::CreateRootSignature() Failed." );
            return false;
        }
    }

今回は定数バッファとテクスチャをバインドレスリソースでアクセスしてみます。予め下記のよう感じでcpp側で定数バッファにDescriptorHeapの0番,テクスチャに1番が設定されています。

    // 定数バッファビューの設定.
    D3D12_CONSTANT_BUFFER_VIEW_DESC bufferDesc = {};
    bufferDesc.BufferLocation = m_pConstantBuffer->GetGPUVirtualAddress();
    bufferDesc.SizeInBytes    = sizeof(ResConstantBuffer);

    // 定数バッファビューを生成 (0番を設定)
    m_pDevice->CreateConstantBufferView( &bufferDesc, m_pCbvSrvHeap->GetCPUDescriptorHandleForHeapStart() );

    // …中略…

    // シェーダリソースビューの設定.
    D3D12_SHADER_RESOURCE_VIEW_DESC viewDesc = {};
    viewDesc.ViewDimension             = D3D12_SRV_DIMENSION_TEXTURE2D;
    viewDesc.Shader4ComponentMapping   = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
    viewDesc.Format                    = DXGI_FORMAT_R8G8B8A8_UNORM;
    viewDesc.Texture2D.MipLevels       = 1;
    viewDesc.Texture2D.MostDetailedMip = 0;

    // シェーダリソースビューを生成 (1番を設定)
    D3D12_CPU_DESCRIPTOR_HANDLE handle = m_pCbvSrvHeap->GetCPUDescriptorHandleForHeapStart();
    handle.ptr += m_CbvSrvDescriptorSize;
    m_pDevice->CreateShaderResourceView( m_pTexture.GetPtr(), &viewDesc, handle );

続いて,シェーダ側ですが,ResourceDescriptorHeapを使ってアクセスするだけです。頂点シェーダの実装は次のようになります。

//-------------------------------------------------------------------------------------------------
// Includes
//-------------------------------------------------------------------------------------------------
#include "SimpleDef.hlsli"

struct TransformBuffer
{
    float4x4 World;
    float4x4 View;
    float4x4 Proj;
};

//-------------------------------------------------------------------------------------------------
//      頂点シェーダのメインエントリーポイントです.
//-------------------------------------------------------------------------------------------------
VSOutput VSFunc(const VSInput input)
{
    VSOutput output = (VSOutput)0;

    ConstantBuffer<TransformBuffer> cb = ResourceDescriptorHeap[0];

    float4 localPos = float4(input.Position, 1.0f);
    float4 worldPos = mul(cb.World, localPos);
    float4 viewPos  = mul(cb.View,  worldPos);
    float4 projPos  = mul(cb.Proj,  viewPos);

    output.Position = projPos;
    output.Normal   = input.Normal;
    output.TexCoord = input.TexCoord;
    output.Color    = input.Color;

    return output;
}

ピクセルシェーダ側も同様に,ResourceDescriptorHeapを使ってテクスチャを参照してやればよいです。

//-------------------------------------------------------------------------------------------------
// Includes
//-------------------------------------------------------------------------------------------------
#include "SimpleDef.hlsli"

SamplerState ColorSmp : register( s0 );

//-------------------------------------------------------------------------------------------------
//      ピクセルシェーダのメインエントリーポイントです.
//-------------------------------------------------------------------------------------------------
PSOutput PSFunc(const VSOutput input)
{
    PSOutput output = (PSOutput)0;

    Texture2D<float4> colorTexture = ResourceDescriptorHeap[1];

    output.Color = input.Color * colorTexture.Sample( ColorSmp, input.TexCoord );

    return output;
}

シェーダが作成出来たら,Shader Model 6.6に対応したdxcでシェーダをコンパイルしてください。サンプログラムの方ではバッチファイルでやっています。

mkdir ..\res\shader\Compiled
call .\dxc.exe -Fh "..\res\shader\Compiled\SimpleVS.inc" -T vs_6_6 -E VSFunc "..\res\SimpleVS.hlsl"
call .\dxc.exe -Fh "..\res\shader\Compiled\SimplePS.inc" -T ps_6_6 -E PSFunc "..\res\SimplePS.hlsl"

 注意点として,ルートシグニチャを設定する前にID3D12GraphicsCommandList::SetDescriptorHeap()を呼び出しておかないといけないらしいです(参考:https://microsoft.github.io/DirectX-Specs/d3d/HLSL_SM_6_6_DynamicResources.html)。
 あとは,SetGraphicsRootParameter()などの設定が不要になるので,そのあたりのコードを削除していつも通りにポリゴンを描画していけばよいだけです。cpp側の描画コードは下記のような感じです。

//-------------------------------------------------------------------------------------------------
//      描画時の処理です.
//-------------------------------------------------------------------------------------------------
void App::OnRender(FLOAT elapsedSec)
{
    // 回転角を増やす.
    m_RotateAngle += ( elapsedSec / 0.5f );

    // ワールド行列を更新.
    m_ConstantBufferData.World = asdx::Matrix::CreateRotationY( m_RotateAngle );

    // 定数バッファを更新.
    memcpy( m_pCbvDataBegin, &m_ConstantBufferData, sizeof(m_ConstantBufferData) );

    // コマンドアロケータとコマンドリストをリセット.
    m_pCmdAllocator->Reset();
    m_pCmdList->Reset( m_pCmdAllocator.GetPtr(), m_pPipelineState.GetPtr() );

    // ディスクリプタヒープを設定.
    // ※ DynamicResourceを使用する場合は必ずルートシグニチャの設定より前に行うこと!.
    m_pCmdList->SetDescriptorHeaps( 1, m_pCbvSrvHeap.GetAddress() );

    // ルートシグニチャを設定.
    m_pCmdList->SetGraphicsRootSignature( m_pRootSignature.GetPtr() );

    // ビューポートの設定.
    m_pCmdList->RSSetViewports( 1, &m_Viewport );

    // シザー矩形の設定.
    m_pCmdList->RSSetScissorRects( 1, &m_ScissorRect );

    // リソースバリアの設定.
    // Present ---> RenderTarget
    D3D12_RESOURCE_BARRIER barrier;
    ZeroMemory( &barrier, sizeof(barrier) );
    barrier.Type                    = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
    barrier.Flags                   = D3D12_RESOURCE_BARRIER_FLAG_NONE;
    barrier.Transition.pResource    = m_pRenderTarget[m_FrameIndex].GetPtr();
    barrier.Transition.StateBefore  = D3D12_RESOURCE_STATE_PRESENT;
    barrier.Transition.StateAfter   = D3D12_RESOURCE_STATE_RENDER_TARGET;
    barrier.Transition.Subresource  = D3D12_RESOURCE_BARRIER_FLAG_NONE;
    m_pCmdList->ResourceBarrier( 1, &barrier );

    // レンダーターゲットのハンドルを取得.
    auto handleRTV = m_pRtvHeap->GetCPUDescriptorHandleForHeapStart();
    auto handleDSV = m_pDsvHeap->GetCPUDescriptorHandleForHeapStart();
    handleRTV.ptr += ( m_FrameIndex * m_RtvDescriptorSize );

    // レンダーターゲットの設定.
    m_pCmdList->OMSetRenderTargets( 1, &handleRTV, FALSE, &handleDSV );

    // レンダーターゲットビューをクリア.
    const FLOAT clearColor[] = { 0.39f, 0.58f, 0.92f, 1.0f };
    m_pCmdList->ClearRenderTargetView( handleRTV, clearColor, 0, nullptr );

    // 深度ステンシルビューをクリア.
    m_pCmdList->ClearDepthStencilView( handleDSV, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr );

    // プリミティブトポロジーの設定.
    m_pCmdList->IASetPrimitiveTopology( D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST );

    // 頂点バッファビューを設定.
    m_pCmdList->IASetVertexBuffers( 0, 1, &m_VertexBufferView );

    // 描画コマンドを生成.
    m_pCmdList->DrawInstanced( 6, 1, 0, 0 );

    // リソースバリアの設定.
    // RenderTarget ---> Present
    barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
    barrier.Transition.StateAfter  = D3D12_RESOURCE_STATE_PRESENT;
    m_pCmdList->ResourceBarrier( 1, &barrier );

    // コマンドの記録を終了.
    m_pCmdList->Close();

    // コマンド実行.
    ID3D12CommandList* ppCmdLists[] = { m_pCmdList.GetPtr() };
    m_pCmdQueue->ExecuteCommandLists( _countof(ppCmdLists), ppCmdLists );

    // 表示する.
    m_pSwapChain->Present( 1, 0 );

    // コマンドの完了を待機.
    WaitForGpu();
}

4 おわり

 今回は,バインドレスリソースの使用方法について紹介してみました。新機能用意するときはサンプルコードも一緒に用意して欲しいものですね。

サンプルコード

本ソースコードおよびプログラムはMIT Licenseに準じます。 プログラムの作成にはMicrosoft Visual Studio Community 2019を用いています。