Vulkan 同步
前言
在前面的文章中,我们讲解了 Vulkan 的多线程设计理念,分析了其底层的机制。我们知道在 Vulkan 的设计中,尽量避免资源的同步竞争,但是在某些复杂场景和多线程优化过程中难免会遇到资源竞争的问题,这时候就需要同步机制来保证线程访问数据的安全性和一致性。
然而 Vulkan 认为资源读写所需要做的同步是应用程序的职责,其内部只提供了很少的隐式同步机制,其余的都需要在程序中显式地使用 Vulkan 中的同步机制来实现。
Vulkan 同步基础
在 Vulkan 代码实现中,使用的同步命令会涉及如下代码:
VkPipelineStageFlags
Vulkan 所有的命令都会在 Pipeline 上执行,只是不同类型的命令,它们的执行阶段是不同的。当我们在 Vulkan 中使用同步机制时,都是以流水线阶段为单位,即某个流水线阶段上执行的所有命令,会在当前阶段暂停,等待另一个流水线阶段上的所有命令在相应的阶段执行完全后,再开始执行。VkPipelineStageFlags 就代表流水线阶段。
VkAccessFlags
Vulkan 中的同步不仅控制操作执行的顺序,还要控制缓存的写回,即内存数据的同步,VkAccessFlags 是为了控制流水线阶段对于内存的读写操作。
因为在 CPU 等存储器件为了读写性能都会分为多层缓存。多级缓存就会导致多线程读写数据的一致性问题,就需要有机制保证某一层缓存数据更新了需要同步到其他缓存上,比如 MemoryBarrier,而所有的 MemoryBarrier 中也都会包含 VkAccessFlags 参数。
Vulkan 所有的同步在全局上都应该认为是对一个 Queue 中的所有命令有效果。
Vulkan 同步原语
Vulkan 中主要有四种同步原语(synchronization primitives):
Fences
最粗粒度的同步原语,用于同步跨队列或跨粗粒度提交到单个队列的工作,目的是给 CPU 端提供一种方法,可以知道 GPU 或者其他 Vulkan Device 什么时候把提交的工作全部做完。类似 Android 的显示机制。
Semaphores
颗粒度比 Fences 更小一点,通常用于不同 Queue 之间的数据同步操作
Events
颗粒度更小,可以用于 Command Buffer 之间的同步工作。
Barriers
Vulkan 流水线(Pipeline)阶段内用于内存访问管理和资源状态移动的同步机制。
Fence
如上介绍 Fence 是粗粒度的同步,它有两种状态——signaled 和 unsignaled。
在调用 vkQueueSubmit 时可以关联一个 Fence,这样当 Queue 中的所有命令都被完成以后,Fence 就会被设置成 signaled 的状态;
通过调用 vKResetFences 可以让一个 Fence 恢复成 unsignaled 的状态;
vkWaitForFences 会让 CPU 原地阻塞,需要等待直到它关联的 Fence 变为 signaled 的状态,这样就可以实现在某个渲染队列内的所有任务被完成后,CPU 再执行某些操作的同步场景。
Fence 也具备内存数据同步的功能,但不需要开发者手动调用。在使用 Fence 时,如果它一旦被设置成 signaled 状态,那么使用这个 Fence 的 Queue 中的所有的命令如果涉及到了对内存的修改,后续的内存访问就一定会在 signaled 之前在 Device 上更新(注意只是在 Device 上更新,如果确保 CPU 也能够获取最新的值的话,就需要再用上其他的同步原语)。
Semaphore
Semaphore 用于 queue 每次提交的一批命令之间的同步和 Fence 一样,它也有两种状态:signaled 和 unsignaled。
调用 vkQueueSubmit 提交命令时,会填充 VkSubmitInfo 结构,而这个结构体中需要设置 pWaitSemaphores、pSignalSemaphores、pWaitDstStageMask。程序在执行到 pWaitDstStageMask 时要阻塞,直到等 pWaitSemaphores 所指向的所有 Semaphore 的状态变成 signaled 时才可以继续执行。本次提交的 Command buffer 执行结束后,pSignalSemaphores 所指向的所有 Semaphore 的状态都会被设置成 signaled。
注:vkQueueSubmit 函数本身也隐含了一个内存数据的同步机制:就是 CPU 上所有的内存修改操作,都会在 GPU 读写之前,对 GPU 而言变成 available 的,并且对于所有后续在 GPU 上的 MemoryAccess,它们都是 visible 的。
部分相关代码示例:
vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]);
Event
Event 同样也具有两种状态——signaled 和 unsignaled,与 Fence 不同的是,它的状态改变既可以在 CPU 上完成,也可以在 GPU 上完成,并且它是一种细粒度的同步机制。注意:Event 只能用在同队列的 Command buffer 间的同步。
在 CPU 侧可以调用 vkSetEvent 来使一个 Event 变成 Signaled 的状态;调用 vkResetEvent 来使一个 Event 恢复成 Unsignaled 的状态;调用 vkGetEventStatus 获取一个 Event 的当前状态,根据 Event 状态阻塞 CPU 运行。
GPU 侧:通过 vkCmdSetEvent 命令来使得一个 Event 变成 Signaled 状态,此时该命令附加了一个操作执行同步:根据提交顺序,在该命令之前的所有命令都必须在 Event 设置 Signaled 状态之前完成。
通过 vkCmdResetEvent 命令来使得一个 Event 变成 Unsignaled 状态,此时该命令同样附加了一个操作执行同步:根据提交顺序,在该命令之前的所有命令都必须在 Event 设置 Unsignaled 状态之前完成。
相关代码:
Barrier
所有的同步原语中,Barrier 的使用成本最高。Barrier 用于显式地控制 buffer 或者 image 的访问范围,避免 hazards(RaW,WaR,and WaW),保证数据一致性。
Barrier 又分为 pipeline barrier 和 memory barrier。
pipeline barrier
要开发者了解渲染管线的各个阶段,能清晰地把握管线中每个步骤对资源的读写顺序。
Vulkan 中 Pipline 各阶段的定义:
TOP_OF_PIPE_BIT
DRAW_INDIRECT_BIT
VERTEX_INPUT_BIT
VERTEX_SHADER_BIT
TESSELLATION_CONTROL_SHADER_BIT
TESSELLATION_EVALUATION_SHADER_BIT
GEOMETRY_SHADER_BIT
FRAGMENT_SHADER_BIT
EARLY_FRAGMENT_TESTS_BIT
LATE_FRAGMENT_TESTS_BIT
COLOR_ATTACHMENT_OUTPUT_BIT
TRANSFER_BIT
COMPUTE_SHADER_BIT
BOTTOM_OF_PIPE_BIT
举一个简单的例子:
场景中有两个渲染管线 P1 和 P2,P1 会通过 Vertex Shader 往 Command buffer 写入顶点数据,P2 需要在 Compute Shader 中使用这些数据。如果使用 fence,P1 的 command 提交后,P2 通过 fence 确保 P1 的操作已经被全部执行完,再开始工作。但理论上 P2 只需要在 Compute Shader 阶段等待 P1 的顶点数据即可。
该场景优化可以用 Barrier,只需要告诉 Vulkan,我们在 P2 的 Compute Shader 阶段才需要等待 P1 Vertex Shader 里面的数据,其他阶段可以并行。
Memory Barrier
内存数据的同步需要使用 Memory Barrier 完成,Vulkan 中有三种 MemoryBarrier。
所有的 MemoryBarrier 都需要搭配 PipelineBarrier 或者 Event 使用。
隐藏的执行顺序
Vulkan 是显式的,号称“没有秘密的 API”。但是在多线程同步时,还是存在一些潜规则。在提隐式执行前,先来了解下提交顺序,它是 Vulkan 的隐式同步及用户的显式同步的前提。
在 CPU 上通过多次 vkQueueSubmit 提交了一系列命令,这些命令的提交顺序为先提交的先行。
同一个 Queue 中,一起提交的多个 Command Buffer,按照下标顺序提交。
同一个 Command Buffer 的操作,先提交的先行
了解了提交顺序后,我们来看下 Vulkan 中隐式的执行顺序,
Command Buffer 中的 Command,严格遵循提交顺序的,先记录的先执行。
ImageLayout 的转移是通过 ImageMemoryBarrier 实现的,严格按照提交顺序执行。
同一个 Queue 中,一起提交的多个 Command Buffer,严格按照提交顺序执行
先提交的 Command Buffer 先执行
总结
本篇文章讲了 Vulkan 中的同步机制,包含显示和隐藏的同步控制,只能感叹 Vulkan 的操作是真的繁琐,希望大家能在了解同步机制的情况下,实现出性能更极致的程序。
参考
版权声明: 本文为 InfoQ 作者【江湖修行】的原创文章。
原文链接:【http://xie.infoq.cn/article/4b05b9a4bb7ad698102d8309a】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论