4.2 CPU / GPU交互


我们必须明白,在图形编程中,我们有两个处理器:CPU和GPU。 它们并行工作,有时需要同步。 为了获得最佳性能,目标是尽可能长时间保持忙碌状态并尽量减少同步。 同步是不可取的,因为这意味着一个处理单元在等待另一个处理单元完成一些工作时处于空闲状态; 换句话说,它破坏了并行性。

关于同步与异步本博主其他博客也有描述,地址:https://blog.csdn.net/yaotuzhi/article/details/80144898

4.2.1命令队列和命令列表
GPU有一个命令队列。 CPU通过命令列表通过Direct3D API向队列提交命令(参见图4.6)。 理解一旦一组命令已经被提交给命令队列很重要,它们不会立即由GPU执行。 他们坐在队列中,直到GPU准备好处理它们,因为GPU很可能忙于处理先前插入的命令。


图4.6。 命令队列。

如果命令队列变空,GPU将闲置,因为它没有任何工作要做; 另一方面,如果命令队列变得太满,那么GPU捕捉[Crawfis12],CPU必须闲置。 这两种情况都是不可取的。 对于像游戏这样的高性能应用,目标是保持CPU和GPU的繁忙,充分利用可用的硬件资源。
在Direct3D 12中,命令队列由ID3D12CommandQueue接口表示。 它是通过填充描述队列的D3D12_COMMAND_QUEUE_DESC结构,然后调用ID3D12Device :: CreateCommandQueue创建的。 我们在本书中创建命令队列的方式如下所示:

 Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;//定义ID3D12CommandQueue的地址

D3D12_COMMAND_QUEUE_DESC queueDesc = {};//定义CommandQueue的描述符及其一些参数
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助手宏被定义为:#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)

其中__uuof(**(ppType))计算为(**(ppType))的COM接口ID,在上面就是COMJ接口:ID3D12CommandQueue。 IID_PPV_ARGS_Helper函数基本上将ppType转换为void **。 在本书中,我们使用这个宏,因为许多Direct3D 12 API调用都有一个参数,它需要我们创建的接口的COM ID,并取一个void **。这接口的主要方法之一是ExecuteCommandLists方法,它将命令列表中的命令添加到队列中:

void ID3D12CommandQueue::ExecuteCommandLists(
// 数组中列出的命令数
UINT Count,
//指向命令列表数组中第一个元素的指针

ID3D12CommandList *const *ppCommandLists);

命令列表按照从第一个数组元素开始的顺序执行。

正如上面的方法声明所提示的,图形的命令列表由从ID3D12CommandList接口继承的ID3D12GraphicsCommandList接口表示。 ID3D12GraphicsCommandList接口提供了多种向命令列表添加命令的方法。 例如,下面的代码添加了设置窗口,清除渲染目标视图和发出绘制调用的命令:

// 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方法来指示我们已完成记录命令:

mCommandList->Close();//记录命令

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

与命令列表关联的是称为ID3D12CommandAllocator的内存支持类。 当命令被记录到命令列表中时,它们实际上将被存储在关联的命令分配器中。 当通过ID3D12CommandQueue :: ExecuteCommandLists执行命令列表时,命令队列将引用分配器中的命令。 命令分配器是从ID3D12Device创建的:

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

1. type:可以与此分配器关联的命令列表的类型。我们在本书中使用的两种常见类型是:
1. D3D12_COMMAND_LIST_TYPE_DIRECT:存储GPU直接执行的命令列表(我们迄今为止描述的命令列表类型)。

2. D3D12_COMMAND_LIST_TYPE_BUNDLE:指定命令列表表示一个包。在构建命令列表中有一些CPU开销,所以Direct3D 12提供了一种优化,使我们能够将一系列命令记录到所谓的捆绑包中。在记录包之后,驱动程序将预处理这些命令以在渲染过程中优化它们的执行。因此,应该在初始化时记录捆绑包。如果分析显示构建特定命令列表需要花费大量时间,则应该将捆绑的使用视为优化。 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。 否则,节点掩码将标识与该命令列表关联的物理GPU。 在本书中,我们假设单GPU系统。
2.键入:命令列表的类型:_COMMAND_LIST_TYPE_DIRECT或D3D12_COMMAND_LIST_TYPE_BUNDLE。
3. pCommandAllocator:与创建的命令列表关联的分配器。 命令分配器类型必须与命令列表类型匹配。
4. pInitialState:指定命令列表的初始管道状态。 这对bundle来说可以为null,在特殊情况下,为了初始化而执行命令列表并且不包含任何绘图命令。 我们在第6章讨论ID3D12PipelineState。
5. riid:我们要创建的ID3D12CommandList接口的COM ID。

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

您可以使用ID3D12Device :: GetNodeCount方法来查询系统上GPU适配器节点的数量。

您可以创建与同一分配器关联的多个命令列表,但不能同时进行记录。 也就是说,除了我们要记录的命令之外,所有命令列表都必须关闭。 因此,来自给定命令列表的所有命令将被连续添加到分配器中。 请注意,创建或重置命令列表时,它处于“打开”状态。 所以如果我们试图用同一个分配器在一行中创建两个命令列表,我们会得到一个错误:

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,它将vector重新调整为零,但保持当前容量不变。 但是,由于命令队列可能引用了分配器中的数据,因此只有在确定GPU已完成执行分配器中的所有命令后,才能重置命令分配器; 下一节将介绍如何做到这一点。

4.2.2 CPU / GPU同步
由于有两个处理器并行运行,会出现许多同步问题。
假设我们有一些资源R存储我们想绘制的几何几何的位置。 此外,假定CPU更新R的数据以存储位置p1,然后将用于引用R的绘图命令C添加到命令队列中,以便在位置p1绘制几何图形。 将命令添加到命令队列不会阻塞CPU,因此CPU将继续运行。 在GPU执行绘图之前,CPU继续并覆盖R的数据以存储新的位置p2是错误的

命令C(见图4.7)。


图4.7。 这是一个错误,因为C使用p2绘制几何图形,或者在R正在更新中间时绘制几何图形。 无论如何,这不是预期的行为。

解决这种情况的一个办法是强制CPU等待,直到GPU完成处理队列中所有的命令直到指定的围栏点。 我们称之为刷新命令队列。 我们可以使用围栏来做到这一点。 栅栏由ID3D12Fence接口表示,用于同步GPU和CPU。 可以使用以下方法创建栅栏对象:

HRESULT ID3D12Device::CreateFence(
UINT64 InitialValue,
D3D12_FENCE_FLAGS Flags,
REFIID riid,
void **ppFence);
// Example
ThrowIfFailed(md3dDevice->CreateFence(
0,
D3D12_FENCE_FLAG_NONE,

IID_PPV_ARGS(&mFence)));

一个fence对象维护一个UINT64值,这个值只是一个整数,用于标识一个围栏时间点。 我们从零开始,每次我们需要标记一个新的围栏点时,我们只是递增整数。 现在,下面的代码/注释显示了我们如何使用fence来刷新命令队列。

UINT64 mCurrentFence = 0;
void D3DApp::FlushCommandQueue()
{
// 提高围栏值以标记直至此围栏点的命令。
mCurrentFence++;
//向命令队列添加一条指令来设置一个新的围栏点。
//因为我们在GPU时间轴上,所以新的栅栏点不会
//直到GPU完成处理此Signal()前的所有命令为止。
ThrowIfFailed(mCommandQueue->Signal(mFence.Get(),mCurrentFence));
//等到GPU完成到这个围栏点的命令。
if(mFence->GetCompletedValue() < mCurrentFence)
{
HANDLE eventHandle = CreateEventEx(nullptr, false,
false, EVENT_ALL_ACCESS);
//当GPU击中当前栅栏时触发事件。
ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle));
//等待GPU击中当前的围栏事件。
WaitForSingleObject(eventHandle, INFINITE);
CloseHandle(eventHandle);
}
}

图4.8用图形解释了这个代码。


图4.8。 在这个快照中,GPU已经处理了高达xgpu的命令并且CPU刚才调用了ID3D12CommandQueue :: Signal(fence,n + 1)方法。 这基本上是在队列末尾添加一条指令,以将fence值更改为n + 1.但是,mFence-> GetCompletedValue()将继续返回n,直到GPU处理队列中添加的所有命令 信号(fence,n + 1)指令。

所以在前面的例子中,在CPU发出绘图命令C之后,它将在覆盖R的数据以存储新的位置p2之前刷新命令队列。 这个解决方案并不理想,因为这意味着CPU在等待GPU完成时处于空闲状态,但它提供了一个简单的解决方案,我们将在第7章之前使用它。您可以在几乎任何点上刷新命令队列(不一定每次只需刷新一次 帧); 如果您有一些初始化GPU命令,例如,可以在进入主渲染循环之前刷新命令队列以执行初始化。

  请注意,刷新命令队列也可以用来解决我们在上一节末尾提到的问题; 也就是说,我们可以刷新命令队列以确保在重置命令分配器之前所有的GPU命令都已经被执行。

4.2.3资源转换

为了实现常见的渲染效果,GPU通常在一步中写入资源R,然后在后面的步骤中从资源R中读取。但是,从资源读取资源会造成资源危险。 GPU尚未完成写入或未开始写入。为了解决这个问题,Direct3D将一个状态关联到资源。资源在创建时处于默认状态,并由应用程序告知Direct3D任何状态转换。这使GPU可以做任何需要做的工作来完成转换并防止资源危害。例如,如果我们正在写入资源,比如说纹理,我们会将纹理状态设置为渲染目标状态;当我们需要读取纹理时,我们会将其状态更改为着色器资源状态。通过向Direct3D通知转换,GPU可以采取措施避免危害,例如,在从资源读取之前等待所有写入操作完成。出于性能原因,资源转移的负担落在应用程序开发人员身上。应用程序开发人员知道这些转换何时发生。自动转换跟踪系统会带来额外的开销。

通过在命令列表上设置转换资源障碍数组来指定资源转换; 它是一个数组,以防您想通过一个API调用转换多个资源。 在代码中,资源障碍由表示

D3D12_RESOURCE_BARRIER_DESC结构。 以下帮助函数(在d3dx12.h中定义)为给定资源返回转换资源障碍描述,并指定之前和之后的状态:

struct CD3DX12_RESOURCE_BARRIER : public
D3D12_RESOURCE_BARRIER
{
// […] convenience methods
static inline CD3DX12_RESOURCE_BARRIER Transition(
_In_ ID3D12Resource* pResource,
D3D12_RESOURCE_STATES stateBefore,
D3D12_RESOURCE_STATES stateAfter,
UINT subresource =
D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES,
D3D12_RESOURCE_BARRIER_FLAGS flags =
D3D12_RESOURCE_BARRIER_FLAG_NONE)
{
CD3DX12_RESOURCE_BARRIER result;
ZeroMemory(&result, sizeof(result));
D3D12_RESOURCE_BARRIER &barrier = result;
result.Type =
D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
result.Flags = flags;
barrier.Transition.pResource = pResource;
barrier.Transition.StateBefore = stateBefore;
barrier.Transition.StateAfter = stateAfter;
barrier.Transition.Subresource = subresource;
return result;
}
// […] more convenience methods

};

注意到CD3DX12_RESOURCE_BARRIER扩展了D3D12_RESOURCE_BARRIER_DESC并添加了便利方法。 大多数Direct3D 12结构都有助手的变体,我们更喜欢这些变体以方便使用。 CD3DX12的变体全部在d3dx12.h中定义。 该文件不是核心DirectX 12 SDK的一部分,但可以从Microsoft下载。 为方便起见,本书源代码的Common目录中包含一个副本。

本章示例应用程序中的一个函数示例如下:

mCommandList->ResourceBarrier(1,&CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),

D3D12_RESOURCE_STATE_PRESENT,D3D12_RESOURCE_STATE_RENDER_TARGET));

此代码将表示我们在屏幕上显示的图像的纹理从呈现状态转换为呈现目标状态。 注意资源障碍已被添加到命令列表中。 您可以将资源障碍转换看作是指示GPU资源状态正在转换的命令,以便在执行后续命令时可以采取必要的步骤来防止资源危险。
除转换类型外,还有其他类型的资源障碍。 目前,我们只需要转换类型。 我们将在需要时介绍其他类型。

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值