创建和记录命令列表和捆绑包

本主题描述在 Direct3D 12 应用中记录命令列表和捆绑包。 命令列表和捆绑都允许应用记录绘图或状态更改调用,以便稍后在图形处理单元 (GPU) 上执行。

在命令列表的外部,API 通过添加另一个级别的命令列表(称为“捆绑”)来利用 GPU 硬件中提供的功能。 捆绑旨在让应用将少量 API 命令组合在一起,供以后执行。 创建捆绑时,驱动程序会执行尽量多的预处理,以降低以后的执行开销。 捆绑可以使用和重用任意次, 而命令列表通常仅执行一次。 但是,只要应用程序在提交新执行之前确保先前的执行完成,则可以多次执行命令列表。

不过,通常,将 API 调用构建到捆绑包中,API 调用和捆绑到命令列表,以及命令列表到单个框架中,如下图所示,指出在命令列表 1 和命令列表 2 中重复使用捆绑包 1,并且关系图中的 API 方法名称只是示例。 可以使用许多不同的 API 调用。

命令、捆绑和命令列表构成帧

在创建和执行捆绑与直接命令列表方面存在不同的限制,本主题将通篇介绍这些差异。

创建命令列表

直接命令列表和捆绑包是通过调用 ID3D12Device::CreateCommandListID3D12Device4::CreateCommandList1 创建的。

使用 ID3D12Device4::CreateCommandList1 创建已关闭的命令列表,而不是创建一个新列表并立即将其关闭。 这避免了使用分配器和 PSO 创建列表但不使用它们时效率低下的问题。

ID3D12Device::CreateCommandList 采用以下参数作为输入:

D3D12_COMMAND_LIST_TYPE

D3D12_COMMAND_LIST_TYPE枚举指示正在创建的命令列表的类型。 它可以是直接命令列表、捆绑、计算命令列表或复制命令列表。

ID3D12CommandAllocator

命令分配器可让应用管理分配给命令列表的内存。 通过调用CreateCommandAllocator 创建命令分配器。 创建命令列表时,分配器的命令列表类型(由 D3D12_COMMAND_LIST_TYPE 指定)必须与所创建的命令列表类型匹配。 一个给定的分配器可同时与多个“当前正在记录”命令列表相关联,不过,可以使用一个命令分配器来创建任意数量的 GraphicsCommandList 对象。

若要回收命令分配器分配的内存,应用需调用 ID3D12CommandAllocator::Reset。 这允许分配器重新用于新命令,但不会减小其基础大小。 但是,在这样做之前,应用必须确保 GPU 不再执行与分配器关联的任何命令列表;否则调用将会失败。 另请注意,此 API 不是自由线程的,因此,不能同时从多个线程针对同一分配器调用此 API。

ID3D12PipelineState

命令列表的初始管道状态。 在 Microsoft Direct3D 12 中,大多数图形管道状态是使用 ID3D12PipelineState 对象在命令列表中设置的。 应用通常在应用初始化期间创建大量此类状态,然后通过使用 ID3D12GraphicsCommandList::SetPipelineState 更改当前绑定的状态对象来更新状态。 有关管道状态对象的详细信息,请参阅在 Direct3D 12 中管理图形管道状态

请注意,捆绑不会继承以前的调用在直接命令列表(捆绑的父级)中设置的管道状态。

如果此参数为 NULL,则使用默认状态。

记录命令列表

创建后,命令列表会立即处于记录状态。 还可以通过调用 ID3D12GraphicsCommandList::Reset 来重复使用现有命令列表,这也会使命令列表处于录制状态。 与 ID3D12CommandAllocator::Reset 不同,可以在仍在调用命令列表时调用 Reset。 典型的模式是提交一个命令列表,然后立即将其重置为对另一个命令列表重复使用分配的内存。 请注意,每次只能有一个与每个命令分配器关联的命令列表处于记录状态。

命令列表进入记录状态后,只需调用 ID3D12GraphicsCommandList 接口的方法即可将命令添加到列表。 其中的许多方法可以实现 Microsoft Direct3D 11 开发人员熟悉的常用 Direct3D 功能;其他 API 是 Direct3D 12 的新功能。

将命令添加到命令列表后,调用 Close 使命令列表退出记录状态。

命令分配器可以增长但不会收缩 - 应考虑池和重用分配器,以最大限度地提高应用的效率。 可以在重置前将多个列表记录到同一分配器,前提是一次只有一个列表正在记录到给定分配器。 可以将每个列表可视化为拥有分配器的一部分,该分配器指示将执行哪些 ID3D12CommandQueue::ExecuteCommandLists

简单的分配器池策略应针对大约 numCommandLists * MaxFrameLatency 分配器。 例如,如果记录 6 个列表并允许最多 3 个潜在帧,则可以合理地预期 18-20 个分配器。 更高级的池策略可以针对 numRecordingThreads * MaxFrameLatency 同一线程上的多个列表重用分配器。 使用前面的示例,如果在线程 A 上记录了 2 个列表,在线程 B 上记录了 2 个,在线程 C 上记录了 1 个列表,在线程 D 上记录了 1 个列表,则实际上可以针对 12-14 个分配器。

使用围栏确定何时可以重用给定的分配器。

由于命令列表可以在执行后立即重置,因此它们可以平常地共用,在每次调用 ID3D12CommandQueue::ExecuteCommandLists 后,将它们添加回池。

示例

以下代码片段演示了命令列表的创建和记录。 请注意,此示例包括以下 Direct3D 12 功能:

  • 管道状态对象 - 这些对象用于从命令列表内部设置渲染器管道的大多数状态参数。 有关详细信息,请参阅在 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。 一个例外是DXGI_ERROR_DEVICE_REMOVED,这将进一步推迟。 请注意,这不同于 D3D11,其中的许多参数验证错误将以无提示方式丢弃,而永远不会返回给调用方。

应用程序预期会在以下 API 调用中看到DXGI_DEVICE_REMOVED错误:

命令列表 API 限制

某些命令列表 API 只能在某些类型的命令列表上调用。 下表显示了在每种类型的命令列表上调用的命令列表 API 有效。 它还显示哪些 API 可用于在 D3D12 呈现通道中调用。

API 名称 显卡 计算 复制 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。 捆绑引用的所有管道状态对象必须具有相同的渲染器目标格式、深度缓冲区格式和样本说明。

在以 类型创建的命令列表中,不允许对以下命令列表 API 调用:D3D12_COMMAND_LIST_TYPE_BUNDLE:

可以针对捆绑调用 SetDescriptorHeaps,但捆绑描述符堆必须与调用方命令列表描述符堆相匹配。

如果针对捆绑调用其中的任一 API,则运行时会丢弃该调用。 每当发生这种情况,调试层就会发出错误。

Direct3D 12 中的工作提交