统一管理D3D12和Vulkan的资源状态
说明
- 个人水平较菜, 此处仅作为个人学习笔记记录. 欢迎指出错误和不足.
- 原文链接: http://diligentgraphics.com/2018/12/09/resource-state-management/
- 没有翻译Vulkan同步方法和作者自己的引擎如何管理同步.
正文
现代图形应用程序可以很好的被看作是客户端-服务器系统, 其中CPU作为客户端记录渲染命令并将命令放置在一个队列中, GPU作为服务器异步地从队列中取得并处理命令. 因此, 当CPU发起一个命令时并不会直接被执行, 当指令数达到一定规模, 到达队列相应点, GPU才会执行(通常1~2帧). GPU被设计为处理特定类型的计算, 因此在结构上与CPU有很大不同. CPU善于处理具有很多流控制指令的算法(分支/循环等), 例如, 处理应用程序的输入事件; GPU可以高效并行地执行数千甚至数百万次相同的计算. CPU也具有wide SIMD单元, 可以有效的执行计算, 但是GPU还是要快几个数量级.
CPU和GPU都需要解决的主要问题是内存延迟. CPU是无序机器, 具有强大的核心和大块缓存的, 它使用复杂的预取和分支预测机制以确保core需要的数据是可以取到的. GPU是有序的, 具有小缓存, 数千个微小核心以及非常深的管线. GPU不使用任何分支预测或预取, 它在运行期间维护数万个线程, 并且可以即时的在线程间进行切换. 当一组线程等待内存请求的结果时, 只要有足够的工作, GPU就可以简单的切换到另一组线程.
在CPU下编程时 (这里谈论的CPU都是指x86CPU, ARM可能会涉及更多内容), 硬件做了很多我们认为是理所当然的事情. 例如, 当一个core向某个内存地址写入内容后, 另一个core可以直接读取相同内存地址上的内容. 包含数据的cache line需要通过CPU做一点额外操作, 但最终另一个core会获取正确的内容且无需应用程序做额外的工作. GPU则几乎没有明确的保证. 大多数情况下, 除非应用程序非常小心, 否则没法指望一次写入结果对后续的读取操作可见. 此外, 数据可能在下一步使用前从一种形式转换为另一种形式.
下面是一些可能需要显示的进行同步的例子:
- 通过UAV(UAV in D3D)或image(in Vulkan/OpenGL terminology) 将数据写入到一个texture或buffer中后, 当该texture或buffer被其他shader读取前, GPU需要等待所有的写入操作完成, 缓存刷新到内存中.
- 当执行完shadow map的绘制命令, 在shadow map可以在light shader中使用前, GPU可能需要等待rasterization和所有的写入操作完成, 刷新缓存并将texture layout变更为采样优化的格式,
- 如果CPU需要读取之前由GPU写入的数据, 则可能需要使该内存区域无效以确保cache获取的是更新后数据.
只有极少的同步依赖GPU需要解决. 通常来说, 所有的同步都是由API/driver控制, 并对开发者屏蔽这部分. 老一点的API如D3D11, OpenGl/GLES就是按这样的方案来. 对开发者来说, 这种方案限制了应用程序无法达到最理想的性能. Driver或API不知道开发者的意图, 因此必须始终假设最坏的情况并确保正确性. 例如, 一个shader向UAV的某块区域进行了写入操作, 下一个shader从另一块区域读取操作, driver必须插入一个barrier以确保写入操作完成并且可见, 因为driver并不知道写入和读取的区域是否存在重叠, 因此做了一个没有必要的barrier.
这种方法最大的问题是使得parallel command recording几乎没什么用. 思考这样一个方案: 一个线程记录绘制shadow map的命令, 另一个线程记录一次forward rendering pass中使用该shadow map的命令. 第一个线程需要shadow map处于depth-stencil writable state, 第二个线程需要shadow map处于shader readable state. 但问题是第二个线程不知道原始的shadow map的state, 因此当应用程序提交第二个命令缓冲区执行时, API需要找到shader map texture的实际state, 并使用正确的state transition修补命令缓冲区. API实际不仅需要这样处理shadow map texture, 还要处理其他所有command list用到resource. 这是个重要的序列化瓶颈, 旧的API没有办法解决这个问题.
D3D12和Vulkan通过显示要求所有resource进行transition解决了上述问题. 应用程序现在可以追踪所有resource的state并确保所有需要的barrier/transition都会执行. 在上面的例子中, 应用程序将会知道什么时候shadow map被用在forward pass, 什么时候处于depth-stencil writable state, 因此可以无需等待第一个command buffer被记录或提交, 直接插入一个barrier. 与此同时, 造成的缺点就是应用程序需要承担自己追踪所有的resource state的重大负担.
Vulkan中的同步
D3D12中的同步
D3D12的同步工具不向Vulkan那样expressive和复杂. 除了下面讲述的UAV barrier之外, D3D12没有定义execution barrier和memory barrier之间的区别, 并通过操作resource state进行barrier.
D3D12 resource参考下表以及https://docs.microsoft.com/en-us/windows/desktop/api/d3d12/ne-d3d12-d3d12_resource_states
D3D12定义了三种resource barrier type, 参考链接: https://docs.microsoft.com/en-us/windows/desktop/api/d3d12/nf-d3d12-id3d12graphicscommandlist-resourcebarrier#remarks