如何解决DirectX12 上传同步 D3D12_HEAP_TYPE_UPLOAD
我想确保我的 D3D12_HEAP_TYPE_UPLOAD
资源在使用前已上传。
显然要做到这一点,您先调用 ID3D12Resource::Unmap
、ID3D12CommandList::Close
、ID3D12CommandQueue::ExecuteCommandList
,然后调用 ID3D12CommandQueue::Signal
。
然而,这让我感到困惑。调用 ID3D12Resource::Unmap
完全没有连接到命令列表和队列,除了创建资源的设备。但是我每个设备有多个命令队列。那么它是如何选择将资源上传到哪个命令队列的呢?
这在任何地方都有记录吗?我能找到的唯一帮助是示例中的注释。
解决方法
根据 Microsoft Docs,Map
和 Unmap
所做的只是处理 CPU 上的虚拟内存地址映射。与 Direct3D 11 不同,您可以安全地将资源映射(即保持映射到虚拟内存)很长时间,而 Direct3D 11 则必须Unmap
。
几乎所有示例都在 D3DX12.H 实用程序标头中使用 UpdateSubresources
助手。这有一些重载,但它们都做相同的基本事情:
- 创建/映射“中间”资源(即上传堆上的内容)。
- 从 CPU 获取数据并将其复制到“中间”资源中(完成后取消映射,因为无需保留虚拟内存地址分配)。
- 然后在命令列表(可以是图形队列命令列表、复制队列命令列表或计算队列命令列表)上调用
CopyBufferRegion
或CopyTextureRegion
。
您可以根据需要将任意数量的这些发布到命令列表中,但“中间”资源在完成之前必须保持有效。
与 Direct3D 12 中的大多数内容一样,您可以使用围栏来执行此操作。当该围栏完成时,您知道可以释放“中间”资源。此外,在您关闭并提交命令列表以供执行之前,没有 副本实际上会启动。
您还需要将最终资源从复制状态转换为可用于渲染的状态。通常,您将这些发布到同一个命令列表中,但如果您使用复制队列或计算队列命令列表,则存在一些限制。
有关此的完整实现,请参阅 DirectX Tool Kit for DX12
请注意,可以直接从上传堆渲染纹理或使用顶点/索引缓冲区。它不如将其复制到默认堆中那样有效,但类似于 Direct3D 11 USAGE_DYNAMIC
。在这种情况下,保持上传堆“映射”并重新使用相同的地址一旦你知道它不再被使用是有意义的。否则,可能会发生腐败或其他坏事。
一旦您将数据复制到映射指针,它就可以立即被命令使用,在上传资源的情况下,在这种情况下无需取消映射资源(您可以在发布或应用程序关闭时取消映射) .
但是,重要的是要注意(特别是阅读您的评论),该命令稍后将在 gpu 上执行,因此如果您打算重用该内存,则需要有一些同步机制。
让我们做一个简单的伪代码示例: 您有一个名为 buffer1 的缓冲区(您已经创建并映射了该缓冲区),现在您可以通过 mappingPtr1 访问其内存。
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
在这种情况下,由于您在与 data1 相同的位置复制了 data2, 第一次计算着色器调用将使用 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 是一个非阻塞函数,当它返回时仅表示您的命令已准备好执行,而不是命令已被处理。
为了保证您在正确的时间使用正确的数据,在这种情况下您有多种选择:
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 和主队列中的等待以确保复制队列具有完成处理并且数据可用(根据另一个答案,在这种情况下,您还需要在默认堆资源中设置资源障碍)
希望它是有道理的。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。