【D3D12学习手记】The Command Queue and Command Lists

GPU有一个命令队列,CPU通过Direct3D API将命令提交到队列里来使用命令列表(command lists),如下图。当一套命令(a set of commands)已经被提交到命令队列,他们不会被GPU立刻执行,理解这一点非常重要。由于GPU很可能忙着处理之前插入的命令,所以它们会待在队列里直到GPU准备好处理它们。

如果命令队列空了,没有任何工作可做,GPU就会处于空闲状态;另一方面,如果命令队列太满,CPU在某个时刻必须停下来等着GPU追上来。这两种情况都不是我们希望看到的;对于高性能要求的应用,比如游戏,目标是同时保持CPU和GPU的处于繁忙状态以使得能够充分利用硬件资源的优势。

在Direct3D12中,命令队列由接口ID3D12CommandQueue来表示。它是通过填充D3D12_COMMAND_QUEUE_DESC结构来描述队列,然后调用ID3D12Device::CreateCommandQueue来创建的。在本书中,我们通过以下方式来创建我们的命令队列:

Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
ThrowIfFailed(md3dDevice->CreateCommandQueue(
  &queueDesc, IID_PPV_ARGS(&mCommandQueue)));

其中IID_PPV_ARGS这个帮助宏(helper macro)的定义如下

#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)

其中__uuidof(**(ppType))求值为(**(ppType))的COM接口ID,在上面的代码中是ID3D12CommandQueue。IID_PPV_ARGS_Helper函数实质上将ppType强制转换为void **。 我们在本书中使用了这个宏,这是因为许多Direct3D 12 API调用都要求有一个参数,即我们正在创建的接口的COM ID,并使用void **类型。

这个接口中的一个主要函数是ExecuteCommandLists方法,它将命令列表(command lists)中的命令(commands)添加到到命令队列(command queue):

void ID3D12CommandQueue::ExecuteCommandLists( 
  // Number of commands lists in the array
  UINT Count, 
  // Pointer to the first element in an array of command lists
  ID3D12CommandList *const *ppCommandLists);

命令列表(command lists)将会从ppCommandLists的第一个数组元素开始顺序执行

正如上面的方法声明所暗示的,图形的命令列表(a command list for graphics)由ID3D12GraphicsCommandList接口表示,该接口继承自ID3D12CommandList接口。 ID3D12GraphicsCommandList接口有许多方法可以将命令添加到命令列表中。 例如,以下代码添加了设置视口(set the viewport),清除渲染目标视图(clear the render target view)和发出绘制调用(issue a draw call)的命令:

// mCommandList pointer to ID3D12CommandList
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->ClearRenderTargetView(mBackBufferView, 
  Colors::LightSteelBlue, 0, nullptr);
mCommandList->DrawIndexedInstanced(36, 1, 0, 0, 0);

这些方法的名称暗示命令是立即执行的,但实际并不是。上面的代码只是将命令添加到命令列表中。 ExecuteCommandLists方法将命令添加到命令队列,GPU处理来自队列的命令。 在我们阅读本书的过程中,我们将了解ID3D12GraphicsCommandList支持的各种命令。 当我们完成向命令列表添加命令时,我们必须通过调用ID3D12GraphicsCommandList :: Close方法来表明我们已完成命令录制(finished recording commands)。

// Done recording commands.
mCommandList->Close();

命令列表在被传递给ID3D12CommandQueue :: ExecuteCommandLists之前,必须先被关闭。

与命令列表相关联的是一个名为ID3D12CommandAllocator的内存支持类。 当命令被记录到命令列表中时,它们实际上将存储在相关的命令分配器(command allocator)中。 当通过ID3D12CommandQueue :: ExecuteCommandLists执行命令列表时,命令队列将引用分配器中的命令(commands)。 从ID3D12Device创建命令分配器的代码如下:

HRESULT ID3D12Device::CreateCommandAllocator( 
  D3D12_COMMAND_LIST_TYPE type,
  REFIID riid,
  void **ppCommandAllocator);

参数解释如下

1.type:可与此分配器关联的命令列表类型。我们在本书中使用的两种常见类型是:
  第一种:D3D12_COMMAND_LIST_TYPE_DIRECT:存储会被GPU直接执行的命令列表(到目前为止我们已经描述的命令列表的类型)。
  第二种:D3D12_COMMAND_LIST_TYPE_BUNDLE:指定命令列表的捆绑包。构建(building)命令列表时会产生一些CPU开销,因此Direct3D 12提供了一种优化,允许我们将一系列命令记录到所谓的bundle中。记录捆绑后,驱动程序将预处理命令以优化其在渲染过程中的执行。因此,应在初始化时记录捆绑。如果分析显示构建特定命令列表需要花费大量时间,则应将bundle的使用视为必要的优化。 Direct3D 12绘图API已经非常高效,因此您不需要经常使用捆绑包,只有在您可以通过它们取得立竿见影的性能时才应该使用它们;也就是说,默认情况下不要使用它们。我们在本书中不使用bundle;有关更多详细信息,请参阅DirectX 12文档。

2.riid:我们要创建的ID3D12CommandAllocator接口的COM ID。

3.ppCommandAllocator:输出的指向被创建的命令分配器的指针。

命令列表也是用ID3D12Device中的方法来创建的

HRESULT ID3D12Device::CreateCommandList( 
  UINT nodeMask,
  D3D12_COMMAND_LIST_TYPE type,
  ID3D12CommandAllocator *pCommandAllocator,
  ID3D12PipelineState *pInitialState,
  REFIID riid,
  void **ppCommandList);

参数解释如下

1.nodeMask:单GPU系统设置为0。 否则,nodeMask 标识与该命令列表相关联的物理GPU。 在本书中,我们假设单GPU系统。

2.命令列表的类型:_COMMAND_LIST_TYPE_DIRECT或D3D12_COMMAND_LIST_TYPE_BUNDLE。

3.pCommandAllocator:与创建的命令列表关联的分配器。 命令分配器类型必须与命令列表类型匹配。

4.pInitialState:指定命令列表的初始管道状态(pipeline state)。 对于bundle而言,这可以为null,并且在特殊情况下,执行命令列表以进行初始化并且不包含任何绘制命令。 我们将在第6章讨论ID3D12PipelineState。

5.riid:我们想要创建的ID3D12CommandList接口的COM ID。

6.ppCommandList:输出指向被创建的命令列表的指针。

你可以创建多个命令列表,并关联到同一个分配器上,但不能同时为这些命令列表录制命令。 也就是说,除了我们将要录制命令的列表之外,其他命令列表必须被关闭。 因此,来自给定命令列表的所有命令将连续地添加到分配器。 请注意,创建或重置命令列表时,它处于“打开”(open)状态。 因此,如果我们尝试使用相同的分配器在一行(a row)中创建两个命令列表,我们将收到错误:

D3D12 ERROR: ID3D12CommandList::{Create,Reset}CommandList: The command allocator is currently in-use by another command list.

在我们调用了ID3D12CommandQueue :: ExecuteCommandList(C)之后,通过调用ID3D12CommandList :: Reset方法重用C的内部存储器来记录一组新命令是安全的。 此方法的参数与ID3D12Device :: CreateCommandList中的相应参数相同:

HRESULT ID3D12CommandList::Reset( 
  ID3D12CommandAllocator *pAllocator,
  ID3D12PipelineState *pInitialState);

此方法将命令列表设置为和刚刚创建时相同的状态,但允许我们重用内部内存并避免取消分配旧命令列表并分配新命令列表。 请注意,重置命令列表不会影响命令队列中的命令,因为关联的命令分配器仍然具有命令队列引用的内存中的命令。

在我们将完整帧的渲染命令提交给GPU之后,我们希望重用命令分配器中的内存来录制下一帧的渲染命令。 ID3D12CommandAllocator :: Reset方法可用于此目的:

HRESULT ID3D12CommandAllocator::Reset(void);

这个想法类似于调用std :: vector :: clear,它将向量的大小调整为零,但保持当前容量相同。 但是,因为命令队列可能在分配器中引用数据,所以在我们确定GPU已完成执行分配器中的所有命令之前,不得重置命令分配器。 如何执行此操作将在下一节中介绍。

原文地址:https://www.cnblogs.com/heben/p/11324758.html