コマンド リストとバンドルの作成と記録

このトピックでは、Direct3D 12 アプリでコマンド一覧とバンドルを記録する方法について説明します。 コマンド一覧とバンドルはいずれも、GPU (Graphics Processing Unit) で後で実行する目的で、描画または状態変更の呼び出しを記録できます。

コマンド リストに加えて、API は GPU ハードウェアに存在する機能を活用するために第 2 のレベルのコマンド リストを使用します。これを "バンドル" といいます。 バンドルの目的は、アプリが少数の API コマンドをグループ化しておいて後で実行できるようにすることです。 バンドル作成時に、ドライバーは可能な限り多くの事前処理を実行します。これは、後で実行するときのコストを下げるためです。 バンドルは何回でも再利用されるように設計されています。 対照的に、コマンド リストは一般的に 1 回だけ実行されます。 ただし、1 つのコマンド リストを複数回実行することも可能です (新しい実行を提出する前に前の実行が確実に完了していることをアプリケーションが保証することが条件となります)。

ただし、通常、バンドルへの API 呼び出しのビルドアップと、コマンド リストへの API 呼び出しとバンドル、およびコマンド リストを 1 つのフレームにまとめて示します。次の図は、コマンド リスト 1コマンド リスト 2Bundle 1 の再利用に注目し、図の API メソッド名が例と同じであることを示しています。 さまざまな API 呼び出しを使用できます。

コマンド、バンドル、コマンド リストをまとめてフレームを作る

作成と実行に関する制約はバンドルと直接コマンド リストとで異なるため、このトピックではそのような違いについて随時説明します。

コマンド リストの作成

ダイレクト コマンド リストとバンドルは、 ID3D12Device::CreateCommandList または ID3D12Device4::CreateCommandList1 を呼び出すことによって作成されます。

ID3D12Device4::CreateCommandList1 を使用して、新しいリストを作成してすぐに閉じるのではなく、閉じたコマンド リストを作成します。 これにより、アロケーターと PSO を使用してリストを作成する非効率を回避できますが、使用しません。

ID3D12Device::CreateCommandList は、次のパラメーターを入力として受け取ります。

D3D12_COMMAND_LIST_TYPE

D3D12_COMMAND_LIST_TYPE列挙は、作成されるコマンド リストの種類を示します。 種類には、直接コマンド リスト、バンドル、計算コマンド リスト、コピー コマンド リストがあります。

ID3D12CommandAllocator

コマンド アロケーターを利用すると、コマンド リストのために割り当てられるメモリをアプリで管理できます。 コマンド アロケーターを作成するには、CreateCommandAllocator を呼び出します。 コマンド リストを作成するときは、 D3D12_COMMAND_LIST_TYPEで指定されたアロケーターのコマンド リストの種類が、作成されるコマンド リストの種類と一致している必要があります。 1 つのアロケーターを同時に複数の "現在記録中" のコマンド リストに関連付けることはできませんが、1 つのコマンド アロケーターを使用して作成できる GraphicsCommandList オブジェクトの数に制限はありません。

コマンド アロケーターによって割り当てられたメモリを回収するには、アプリで ID3D12CommandAllocator::Reset を呼び出します。 これにより、アロケーターを新しいコマンドに再利用できますが、基になるサイズは小さくなります。 ただし、このことを行う前に、そのアロケーターに関連付けられているコマンド リストの GPU での実行がすべて終了していることをアプリが保証する必要があります。このとおりでない場合は、呼び出しに失敗します。 また、この API はフリースレッドではなく、したがって同じアロケーターに対して同時の複数のスレッドから呼び出すことはできません。

ID3D12PipelineState

コマンド リストの最初のパイプライン状態です。 Microsoft Direct3D 12 では、グラフィックス パイプラインの状態のほとんどはコマンド リストの中で ID3D12PipelineState オブジェクトを使用して設定されます。 アプリではこのオブジェクトを多数作成し (一般的にはアプリの初期化のときに)、それ以降の状態の更新には、その時点でバインドされている状態オブジェクトを ID3D12GraphicsCommandList::SetPipelineState を使用して変更します。 パイプライン状態オブジェクトの詳細については、「Direct3D 12 でグラフィックス パイプラインの状態を管理する」を参照してください。

なお、バンドルには、その親である直接コマンド リストの中のそれまでの呼び出しによって設定されたパイプライン状態は継承されないことに注意してください。

このパラメーターが NULL の場合は、既定の状態が使用されます。

コマンド リストの記録

作成直後のコマンド リストは記録中状態となっています。 ID3D12GraphicsCommandList::Reset を呼び出すことで、既存のコマンド リストを再利用することもできます。これにより、コマンド リストも記録状態になります。 ID3D12CommandAllocator::Reset とは異なり、Reset はそのコマンド リストの実行中でも呼び出すことができます。 一般的なパターンは、コマンド リストを提出し、その直後にリセットするというものです。これで、割り当て済みのメモリを他のコマンド リストに再利用できるようになります。 なお、各コマンド アロケーターに関連付けられているコマンド リストのうち、記録中状態になることができるのは一度に 1 つだけであることに注意してください。

アプリケーションでは、コマンド リストが記録中状態になったら、ID3D12GraphicsCommandList インターフェイスのメソッドを呼び出してコマンドをそのリストに追加します。 これらのメソッドの多くは、Microsoft Direct3D 11 開発者にとってなじみ深い共通の Direct3D 機能を利用するためのものです。それ以外の API は Direct3D 12 で新規追加されたものです。

アプリケーションでコマンドをコマンド リストに追加した後に、コマンド リストを記録中状態から遷移させるには Close を呼び出します。

コマンド アロケーターは拡張できますが、縮小することはできません。 アプリの効率を最大化するには、アロケーターのプールと再利用を検討する必要があります。 一度に 1 つのアロケーターに記録されているリストが 1 つだけの場合、リセット前に複数のリストを同じアロケーターに記録できます。 各リストは、実行する ID3D12CommandQueue::ExecuteCommandLists を示すアロケーターの一部を所有しているものとして視覚化できます。

単純なアロケーター プール戦略では、おおよそ numCommandLists * MaxFrameLatency のアロケーターを目指す必要があります。 たとえば、6 つのリストを記録し、最大 3 つの潜在的なフレームを許可する場合、18 から 20 個のアロケーターが合理的に期待できます。 同じスレッド上の複数のリストのアロケーターを再利用する、より高度なプール戦略では、アロケーターを numRecordingThreads * MaxFrameLatency 目指すことができます。 前の例を使用して、スレッド A に 2 つのリストが記録され、スレッド B に 2、スレッド C に 1、スレッド D に 1 が記録されている場合、実際には 12 から 14 個のアロケーターを目指す可能性があります。

フェンスを使用して、特定のアロケーターをいつ再利用できるかを判断します。

コマンド リストは実行後すぐにリセットできるため、簡単にプールでき、 ID3D12CommandQueue::ExecuteCommandLists を呼び出すたびにプールに追加し直すことができます。

次のコード スニペットは、コマンド リストの作成と記録の方法の例を示すものです。 この例には次の Direct3D 12 の機能が含まれています。

次のような例があります。

void D3D12HelloTriangle::LoadAssets()
{
    // Create an empty root signature.
    {
        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;
        ThrowIfFailed(D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error));
        ThrowIfFailed(m_device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&m_rootSignature)));
    }

    // Create the pipeline state, which includes compiling and loading shaders.
    {
        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 = { reinterpret_cast<UINT8*>(vertexShader->GetBufferPointer()), vertexShader->GetBufferSize() };
        psoDesc.PS = { reinterpret_cast<UINT8*>(pixelShader->GetBufferPointer()), pixelShader->GetBufferSize() };
        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)));
    }

    // Create the command list.
    ThrowIfFailed(m_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, m_commandAllocator.Get(), m_pipelineState.Get(), IID_PPV_ARGS(&m_commandList)));

    // Command lists are created in the recording state, but there is nothing
    // to record yet. The main loop expects it to be closed, so close it now.
    ThrowIfFailed(m_commandList->Close());

    // 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 } }
        };

        const UINT vertexBufferSize = sizeof(triangleVertices);

        // Note: using upload heaps to transfer static data like vert buffers is not 
        // recommended. Every time the GPU needs it, the upload heap will be marshalled 
        // over. Please read up on Default Heap usage. An upload heap is used here for 
        // code simplicity and because there are very few verts to actually transfer.
        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(&m_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(m_vertexBuffer->Map(0, &readRange, reinterpret_cast<void**>(&pVertexDataBegin)));
        memcpy(pVertexDataBegin, triangleVertices, sizeof(triangleVertices));
        m_vertexBuffer->Unmap(0, nullptr);

        // Initialize the vertex buffer view.
        m_vertexBufferView.BufferLocation = m_vertexBuffer->GetGPUVirtualAddress();
        m_vertexBufferView.StrideInBytes = sizeof(Vertex);
        m_vertexBufferView.SizeInBytes = vertexBufferSize;
    }

    // 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();
    }
}

コマンド リストが作成されて記録が完了したら、コマンド キューを使用してこのコマンド リストを実行できます。 詳細については、「コマンド リストの実行と同期」を参照してください。

参照カウント

ほとんどの D3D12 API では、次の COM 規則に従い、参照カウントが引き続き使用されます。 ただし、D3D12 グラフィックス コマンド リストの API は例外です。 ID3D12GraphicsCommandList に対するすべての API は、その API に渡されたオブジェクトへの参照を保持しません。 つまり、コマンド リストを実行のために提出するときに、破棄済みのリソースを参照していないことを保証するのはアプリケーションの責任となります。

コマンド リストのエラー

ID3D12GraphicsCommandList に対する API のほとんどは、エラーを返しません。 コマンド リスト作成中に発生したエラーは、ID3D12GraphicsCommandList::Close まで延期されます。 1 つの例外はDXGI_ERROR_DEVICE_REMOVEDであり、さらに延期されます。 これは、D3D11 とは異なることに注意してください。D3D11 では多数のパラメーター検証エラーが警告なしで削除されるため、呼び出し元に返されることはありません。

アプリケーションでは、次の API 呼び出しでDXGI_DEVICE_REMOVEDエラーが発生する可能性があります。

コマンド リスト API の制限事項

一部のコマンド リスト API は、特定の種類のコマンド リストでのみ呼び出すことができます。 次の表は、コマンド リストの各種類で呼び出すために有効なコマンド リスト API を示しています。 また、 D3D12 レンダー パスで呼び出すために有効な API も示します。

API 名 グラフィックス Compute コピー Bundle レンダー パス
AtomicCopyBufferUINT
AtomicCopyBufferUINT64
BeginQuery
BeginRenderPass
BuildRaytracingAccelerationStructure
ClearDepthStencilView
ClearRenderTargetView
ClearState
ClearUnorderedAccessViewFloat
ClearUnorderedAccessViewUint
CopyBufferRegion
CopyRaytracingAccelerationStructure
CopyResource
CopyTextureRegion
CopyTiles
DiscardResource
Dispatch
DispatchRays
DrawIndexedInstanced
DrawInstanced
EmitRaytracingAccelerationStructurePostbuildInfo
EndQuery
EndRenderPass
ExecuteBundle
ExecuteIndirect
ExecuteMetaCommand
IASetIndexBuffer
IASetPrimitiveTopology
IASetVertexBuffers
InitializeMetaCommand
OMSetBlendFactor
OMSetDepthBounds
OMSetRenderTargets
OMSetStencilRef
ResolveQueryData
ResolveSubresource
ResolveSubresourceRegion
ResourceBarrier
RSSetScissorRects
RSSetShadingRate
RSSetShadingRateImage
RSSetViewports
SetComputeRoot32BitConstant
SetComputeRoot32BitConstants
SetComputeRootConstantBufferView
SetComputeRootDescriptorTable
SetComputeRootShaderResourceView
SetComputeRootSignature
SetComputeRootUnorderedAccessView
SetDescriptorHeaps
SetGraphicsRoot32BitConstant
SetGraphicsRoot32BitConstants
SetGraphicsRootConstantBufferView
SetGraphicsRootDescriptorTable
SetGraphicsRootShaderResourceView
SetGraphicsRootSignature
SetGraphicsRootUnorderedAccessView
SetPipelineState
SetPipelineState1
SetPredication
SetProtectedResourceSession
SetSamplePositions
SetViewInstanceMask
SOSetTargets
WriteBufferImmediate

バンドルに関する制限

バンドルに関連付けられる作業のほとんどを Direct3D 12 ドライバーが記録時に実行できるようにするために、各種制限が設けられています。その結果として、ExecuteBundle API を実行するときのオーバーヘッドを抑えることができます。 1 つのバンドルで参照されるすべてのパイプライン状態オブジェクトのレンダー ターゲット形式、深度バッファー形式、およびサンプル記述が同一であることが必要です。

次のコマンド リスト API 呼び出しは、種類が D3D12_COMMAND_LIST_TYPE_BUNDLE で作成されたコマンド リストでは許可されません。

SetDescriptorHeaps をバンドルに対して呼び出すことはできますが、バンドル記述子ヒープは呼び出し元のコマンド リスト記述子ヒープと一致する必要があります。

これらの API をバンドルに対して呼び出すと、その呼び出しはランタイムによって破棄されます。 このことが起きたときは、デバッグ レイヤーによってエラーが発行されます。

Direct3D 12 での作業の送信