Vulkan之Fence

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
2
3
4
5
typedef struct VkFenceCreateInfo {
VkStructureType sType;
const void* pNext;
VkFenceCreateFlags flags;
} VkFenceCreateInfo;

sType要设置为VK_STRUCTURE_TYPE_FENCE_CREATE_INFO
flags在初始时一般要设置为VK_FENCE_CREATE_SIGNALED_BIT,这样我们新创建的fence就是signaled了。

1
2
3
4
5
VKAPI_ATTR VkResult VKAPI_CALL vkCreateFence(
VkDevice device,
const VkFenceCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkFence* pFence);

当调用vkCreateFence成功后,会把一个新的fence对象的句柄放置到pFence指向的变量中。

5)fence的销毁

fence是我们创建的对象,当程序结束时,我们需要手动将其销毁

1
2
3
4
VKAPI_ATTR void VKAPI_CALL vkDestroyFence(
VkDevice device,
VkFence fence,
const VkAllocationCallbacks* pAllocator);

6)vkWaitForFences

vkWaitForFences会让CPU在当前位置被阻塞掉,然后一直等待到它接受的Fence变为signaled的状态,这样就可以实现在某个渲染队列内的所有任务被完成后,CPU再执行某些操作的同步情景。

1
2
3
4
5
6
VKAPI_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
4
VKAPI_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
3
VKAPI_ATTR VkResult VKAPI_CALL vkGetFenceStatus(
VkDevice device,
VkFence fence);

10) fence的实际使用

我们从实际的代码分析

1
2
3
4
5
6
7
8
9
10
11
12
std::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
2
3
4
5
6
7
8
void drawFrame() {
vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX);
......
vkResetFences(device, 1, &inFlightFences[currentFrame]);
if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) {
throw std::runtime_error("failed to submit draw command buffer!");
}
}

我们首先需要等待fence的状态,如果时signaled,则将其reset为unsignaled,然后再次提交新的命令vkQueueSubmit。这里我们用了并行渲染的模式,所以我们看到的是数组里的某一个fence。

上面的inFlightFences解决了CPU命令提交过快问题,下面的imagesInFlight解决了GPU和CPU对同一资源一个读,一个改的问题。

1
2
3
4
if (imagesInFlight[imageIndex] != VK_NULL_HANDLE) {
vkWaitForFences(device, 1, &imagesInFlight[imageIndex], VK_TRUE, UINT64_MAX);
}
imagesInFlight[imageIndex] = inFlightFences[currentFrame];

显示 Gitment 评论