DirectX12 上传同步 D3D12_HEAP_TYPE_UPLOAD

DirectX12 Upload Synchronization D3D12_HEAP_TYPE_UPLOAD

我想确保我的 D3D12_HEAP_TYPE_UPLOAD 资源在我使用之前已经上传。

显然要做到这一点,您可以调用 ID3D12Resource::UnmapID3D12CommandList::CloseID3D12CommandQueue::ExecuteCommandList,然后调用 ID3D12CommandQueue::Signal

然而,这让我很困惑。调用 ID3D12Resource::Unmap 完全没有连接到命令列表和队列,除了创建资源的设备。但是我每个设备都有多个命令队列。那么它是如何选择将资源上传到哪个命令队列的呢?

这在任何地方都有记录吗?我能找到的唯一帮助是示例中的评论。

对于Microsoft DocsMapUnmap所做的只是处理CPU上的虚拟内存地址映射。您可以安全地长时间保留资源映射(即,将其映射到虚拟内存),这与 Direct3D 11 不同,在 Direct3D 11 中,您必须 Unmap 它。

几乎所有示例都使用 D3DX12.H 实用程序 header 中的 UpdateSubresources 助手。有一些重载,但它们都做同样的基本事情:

  • Create/Map 一个 'intermediate' 资源(即上传堆上的东西)。
  • 从 CPU 中获取数据并将其复制到 'intermediate' 资源中(完成后取消映射,因为不需要保留虚拟内存地址分配)。
  • 然后在command-list上调用CopyBufferRegionCopyTextureRegion(可以是图形queuecommand-list,副本queuecommand-list, 或 compute-queue command-list).

您可以 post 将任意数量的资源放入 command-list 中,但 'intermediate' 资源在完成之前必须保持有效。

与 Direct3D 12 中的大多数内容一样,您可以使用栅栏来执行此操作。当围栏完成时,您知道可以释放 'intermediate' 资源。此外,none 的副本将实际开始,直到您关闭并提交 command-list 以供执行。

需要将最终资源从复制状态转换为可用于呈现的状态。通常你 post 这些在同一个 command-list 上,尽管如果你使用 copy-queue 或 compute-queue command-lists.

会有限制

有关此的完整实施,请参阅 DirectX Tool Kit for DX12

Note that it is possible to render a texture or use vertex/index buffers directly from the upload heap. It's not as efficient as copying it into a default heap, but is akin to the Direct3D 11 USAGE_DYNAMIC. In this case, it would make sense to keep the upload heap "mapped" and re-use the same address once you know it's no longer in use. Otherwise, corruption or other bad things can happen.

一旦您将数据复制到映射指针,它立即可供命令使用,在上传资源的情况下,在这种情况下无需取消映射资源(您可以在发布或应用程序中取消映射关机)。

然而,重要的是要注意(特别是阅读您的评论),该命令稍后将在 gpu 上执行,因此如果您打算重用该内存,您需要有一些同步机制。

让我们做一个简单的伪代码示例: 您有一个名为 buffer1 的缓冲区(您已经创建并映射),现在您可以通过 mappedPtr1 访问其内存。

copy data1 to mappedPtr1
call compute shader in commandList
execute CommandList

现在一切都会正常执行(假设您有同步,对于一帧)

现在,如果您执行以下操作:

copy data1 to mappedPtr1
call compute shader in commandList (1)
copy data2 to mappedPtr1
call compute shader in commandList (1)
execute CommandList

在那种情况下,由于您将 data2 复制到与 data1 相同的位置, 第一次计算着色器调用将使用 data2(调用 execute CommandList 时它是最新的可用数据)

现在让我们举一个稍微不同的例子:

copy data1 to mappedPtr1
call compute shader in commandList1
execute CommandList1
copy data2 to mappedPtr1
call compute shader in commandList2
execute CommandList2

现在会发生什么是不确定的,因为你不知道什么时候 CommandList1 和 CommandList2 会被有效处理。

如果 CommandList1 在之前处理(足够快):

copy data2 to mappedPtr1

那么data1就是当前内存并被使用

但是,如果您的 commandList 有点重,并且在您完成对

的调用时 CommandList1 尚未处理
copy data2 to mappedPtr1

这很可能会发生,然后两个计算在被 gpu 使用时将再次使用 data2。

这是因为executeCommandList是一个非阻塞函数,当它returns时它只意味着你的命令已经准备好执行,而不是命令已经被处理。

为了保证您在正确的时间使用正确的数据,在这种情况下您有几种选择:

1/使用栅栏等待完成

copy data1 to mappedPtr1
call compute shader in commandList1
execute CommandList1 on commandQueue
attachSignal (1) to commandQueue 
add a waitevent for value (1)  
copy data2 to mappedPtr1
call compute shader in commandList2
execute CommandList2 on commandQueue
attachSignal (2) to commandQueue 
add a waitevent for value (2)

这很简单,但效率非常低,因为现在您要等待 gpu 完成所有 commandList 的执行,然后才能继续任何 cpu 工作。

2/使用不同的资源:

既然你现在复制到 2 个不同的位置,你当然会保证你的数据在两个调用中是不同的。

3/使用带偏移量的单一资源。

您还可以创建一个更大的资源,可以保存所有调用的数据,然后复制一次。

这里我假设你的数据是 64 字节(所以你会创建一个 128 字节的缓冲区)

copy data1 to mappedPtr1 (offset 0)
bind address from mappedPtr1 (offset 0) to compute
call compute shader in commandList1
execute CommandList1 on commandQueue 
copy data2 to mappedPtr1 (offset 64)
bind address from mappedPtr1 (offset 64) to compute
call compute shader in commandList2
execute CommandList2 on commandQueue

请注意,您仍然应该有围栏来指示帧何时完成处理,这是保证上传部分最终可以重用的唯一方法。

如果您想将数据复制到默认堆(特别是如果您在单独的复制队列上执行此操作),您还需要复制队列上的 Fence 和主队列中的等待以确保复制队列已完成处理并且数据可用(根据其他答案,在这种情况下,您还需要在默认堆资源中设置资源屏障)

希望它有意义。