vulkan以并行的方式异步的运行,这样通过设备上的多个队列和主机一起使物理资源保持繁忙,这也就使得vulkan变得异常复杂,因此需要引入各种同步原语,使得主机和设备保持同步,不至于混乱执行,这里我们首先分析下fence。
1)fence是做什么的
Fence提供了一种粗粒度的,从Device向Host单向传递信息的机制,可以理解为 CPU 与 GPU 端的同步信号,实际上也就是queue到CPU的一种同步信号。
对于一个fence对象,Device会将其从unsignaled转到signaled状态,告诉Host一些工作已经完成。所以fence使用在Host/Device之间的,且是一种比较粗粒度的同步机制。
2)需要引入fence的原因
CPU端任务提交过快,而GPU端(实际上是queue)处理渲染慢,GPU端已经满载了(这实际上是我们希望的,因为要提升帧率就必然会让GPU满载工作),进而会导致queue端需要处理的指令积压严重,这会造成应用程序的内存使用量暴涨(也就是说实际上fence的目的是解决应用程序内存暴涨的问题,也就是不希望CPU端提交指令过多)
同时fence还起到了阻塞CPU与GPU对同一资源的共同操作问题。在draw中,(由于swapchain的缓冲有限)在绘制同一个索引的framebuffer中,会修改uniformbuffer以及顶点数据等等(这在游戏或者视频中很常见)。如果没有fence,会导致GPU正在渲染的commandBuffer以及其中的资源被CPU修改了,进而导致渲染错乱。因此实质上fence也是阻断了CPU和GPU对同一帧资源的同时修改问题。
3)fence的状态
fence一共有两个状态signaled and unsignaled
初始状态下,fence会被设置为signaled的状态;
在调用vkQueueSubmit时,可以传入一个Fence,这样当Queue中的所有命令都被完成以后,Fence就会被设置成signaled的状态;
vkWaitForFences会让CPU在当前位置被阻塞掉,然后一直等待到它接受的Fence变为signaled的状态,这样就可以实现在某个渲染队列内的所有任务被完成后,CPU再执行某些操作的同步情景。
(fence被触发到signaled状态,必须存在一种方法,将之转回到unsignaled状态,这个功能由vkResetFences完成)
4)fence的创建
1 | typedef struct VkFenceCreateInfo { |
sType要设置为VK_STRUCTURE_TYPE_FENCE_CREATE_INFO
flags在初始时一般要设置为VK_FENCE_CREATE_SIGNALED_BIT,这样我们新创建的fence就是signaled了。1
2
3
4
5VKAPI_ATTR VkResult VKAPI_CALL vkCreateFence(
VkDevice device,
const VkFenceCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkFence* pFence);
当调用vkCreateFence成功后,会把一个新的fence对象的句柄放置到pFence指向的变量中。
5)fence的销毁
fence是我们创建的对象,当程序结束时,我们需要手动将其销毁1
2
3
4VKAPI_ATTR void VKAPI_CALL vkDestroyFence(
VkDevice device,
VkFence fence,
const VkAllocationCallbacks* pAllocator);
6)vkWaitForFences
vkWaitForFences会让CPU在当前位置被阻塞掉,然后一直等待到它接受的Fence变为signaled的状态,这样就可以实现在某个渲染队列内的所有任务被完成后,CPU再执行某些操作的同步情景。1
2
3
4
5
6VKAPI_ATTR VkResult VKAPI_CALL vkWaitForFences(
VkDevice device,
uint32_t fenceCount, // 需要等待的fence的数量
const VkFence* pFences, // 指向VkFence句柄的数组,这些句柄表示需要等待的fence
VkBool32 waitAll, // VK_TRUE则表示要等待所有的fence变为有信号,VK_FALSE为任何一个有信号就返回
uint64_t timeout); // 超时时间
7)vkResetFences
很简单。就是将fence由signaled状态转回到unsignaled状态1
2
3
4VKAPI_ATTR VkResult VKAPI_CALL vkResetFences(
VkDevice device,
uint32_t fenceCount, // 待重置的fence的数目
const VkFence* pFences); // 指向VkFence句柄的数组,这些句柄表示待重置的fence
8)vkQueueSubmit
vkQueueSubmit的作用时提交渲染命令,在其他章节有介绍。
在调用vkQueueSubmit时,可以传入一个Fence(这个fence必须时unsignaled,这也就是为啥要在vkQueueSubmit之前调用vkResetFences的原因),这样当Queue中的所有命令都被完成以后,Fence就会被设置成signaled的状态;
9) vkGetFenceStatus
应用程序可以直接调用此函数来查询fence的状态,不过这个好像使用的比较少。1
2
3VKAPI_ATTR VkResult VKAPI_CALL vkGetFenceStatus(
VkDevice device,
VkFence fence);
10) fence的实际使用
我们从实际的代码分析1
2
3
4
5
6
7
8
9
10
11
12std::vector<VkFence> inFlightFences;
std::vector<VkFence> imagesInFlight;
inFlightFences.resize(MAX_FRAMES_IN_FLIGHT);
imagesInFlight.resize(swapChainImages.size(), VK_NULL_HANDLE);
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i])
}
1 | void drawFrame() { |
我们首先需要等待fence的状态,如果时signaled,则将其reset为unsignaled,然后再次提交新的命令vkQueueSubmit。这里我们用了并行渲染的模式,所以我们看到的是数组里的某一个fence。
上面的inFlightFences解决了CPU命令提交过快问题,下面的imagesInFlight解决了GPU和CPU对同一资源一个读,一个改的问题。1
2
3
4if (imagesInFlight[imageIndex] != VK_NULL_HANDLE) {
vkWaitForFences(device, 1, &imagesInFlight[imageIndex], VK_TRUE, UINT64_MAX);
}
imagesInFlight[imageIndex] = inFlightFences[currentFrame];