子流程依赖:
渲染流程的子流程会自动进行图像布局变换。这一变换过程由子流程的依赖所决定。子流程的依赖包括子流程之间的内存和执行的依赖关系。虽然我们现在只使用了一个子流程,但子流程执行之前和子流程执行之后
的操作也被算作隐含的子流程。在渲染流程开始和结束时会自动进行图像布局变换,但在渲染流程开始时进行的自动变换的时机和我们的需求不符,变换发生在管线开始时,但那时我们可能还没有获取到交换链图像。有两种方式可以解决这个问题。
1.设置 imageAvailableSemaphore 信号量的 waitStages 为 VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,确保渲染流程在我们获取交换链图像之前不会开始。
2.设置渲染流程等待VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT 管线阶段。
在这里,为了让读者能够了解子流程依赖如何控制图像布局变换,我们使用第二种方式
部分示例Demo:
//可以同时并行处理的帧数--12
const int MAX_FRAMES_IN_FLIGHT = 2;
//为每一帧创建属于它们自己的信号量
//信号量发出图像已经被获取,可以开始渲染的信号--12
std::vector<VkSemaphore> imageAvailableSemaphores ;
//量发出渲染已经结果,可以开始呈现的信号--12
std::vector<VkSemaphore> renderFinishedSemaphores ;
//追踪当前渲染的是哪一帧--12
size_t currentFrame = 0;
/**
需要使用栅栏(fence) 来进行 CPU 和 GPU 之间的同步,
来防止有超过 MAX_FRAMES_IN_FLIGHT帧的指令同时被提交执行。
栅栏 (fence) 和信号量 (semaphore) 类似,可以用来发出信号和等待信号
*/
//每一帧创建一个VkFence 栅栏对象--12
std::vector<VkFence> inFlightFences ;
//创建信号量和VkFence--12
void createSyncObjects(){
//创建每一帧需要的信号量对象
imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT) ;
renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT) ;
inFlightFences.resize(MAX_FRAMES_IN_FLIGHT) ;
VkSemaphoreCreateInfo semaphoreInfo = {};
semaphoreInfo.sType =
VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
VkFenceCreateInfo fenceInfo = {};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
/**
默认情况下,栅栏 (fence) 对象在创建后是未发出信号的状态。
这就意味着如果我们没有在 vkWaitForFences 函数调用
之前发出栅栏 (fence) 信号,vkWaitForFences 函数调用将会一直处于等待状态。
这里设置初始状态为已发出信号
*/
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
for( size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++){
//创建信号量和VkFence 对象
if(vkCreateSemaphore(device,&semaphoreInfo,nullptr,
&imageAvailableSemaphores[i]) != VK_SUCCESS ||
vkCreateSemaphore(device,&semaphoreInfo,nullptr,
&renderFinishedSemaphores[i]) != VK_SUCCESS ||
vkCreateFence(device,&fenceInfo,nullptr,
&inFlightFences[i]) != VK_SUCCESS){
throw std::runtime_error(
"failed to create synchronization objects for a frame!");
}
}
}
/**
* @brief drawFrame
* 1.从交换链获取一张图像
* 2.对帧缓冲附着执行指令缓冲中的渲染指令
* 3.返回渲染后的图像到交换链进行呈现操作
上面这些操作每一个都是通过一个函数调用设置的, 但每个操作的实际
执行却是异步进行的。函数调用会在操作实际结束前返回,并且操作的实
际执行顺序也是不确定的。而我们需要操作的执行能按照一定的顺序,所
以就需要进行同步操作。
有两种用于同步交换链事件的方式:栅栏 (fence) 和信号量 (semaphore)。
它们都可以完成同步操作。
栅栏 (fence) 和信号量 (semaphore) 的不同之处是,我们可以通过调
用 vkWaitForFences 函数查询栅栏 (fence) 的状态,但不能查询信号量
(semaphore) 的状态。通常,我们使用栅栏 (fence) 来对应用程序本身和渲
染操作进行同步。使用信号量 (semaphore) 来对一个指令队列内的操作或
多个不同指令队列的操作进行同步。这里,我们想要通过指令队列中的绘
制操作和呈现操作,显然,使用信号量 (semaphore) 更加合适。
*/
//绘制图像--12
void drawFrame(){
/**
vkWaitForFences 函数可以用来等待一组栅栏 (fence) 中的一个或
全部栅栏 (fence) 发出信号。上面代码中我们对它使用的 VK_TRUE
参数用来指定它等待所有在数组中指定的栅栏 (fence)。我们现在只有
一个栅栏 (fence) 需要等待,所以不使用 VK_TRUE 也是可以的。和
vkAcquireNextImageKHR 函数一样,vkWaitForFences 函数也有一个超时
参数。和信号量不同,等待栅栏发出信号后,我们需要调用 vkResetFences
函数手动将栅栏 (fence) 重置为未发出信号的状态。
*/
//等待我们当前帧所使用的指令缓冲结束执行
vkWaitForFences(device,1,&inFlightFences[currentFrame],
VK_TRUE,std::numeric_limits<uint64_t>::max());
vkResetFences(device , 1 , &inFlightFences[currentFrame]);
uint32_t imageIndex;
/**
vkAcquireNextImageKHR参数:
1.使用的逻辑设备对象
2.我们要获取图像的交换链,
3.图像获取的超时时间,我们可以通过使用无符号 64 位整型所能表示的
最大整数来禁用图像获取超时
4,5.指定图像可用后通知的同步对象.可以指定一个信号量对象或栅栏对象,
或是同时指定信号量和栅栏对象进行同步操作。
在这里,我们指定了一个叫做 imageAvailableSemaphore 的信号量对象
6.用于输出可用的交换链图像的索引,我们使用这个索引来引用我们的
swapChainImages数组中的VkImage对象,并使用这一索引来提交对应的指令缓冲
*/
//从交换链获取一张图像
//交换链是一个扩展特性,所以与它相关的操作都会有 KHR 这一扩展后缀
vkAcquireNextImageKHR(device,swapChain,
std::numeric_limits<uint64_t>::max(),
imageAvailableSemaphores[currentFrame],
VK_NULL_HANDLE,&imageIndex);
//提交信息给指令队列
VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
VkSemaphore waitSemaphores[] = {imageAvailableSemaphores[currentFrame]};
//这里需要写入颜色数据到图像,所以我们指定等待图像管线到达可以写入颜色附着的管线阶段
VkPipelineStageFlags waitStages[] = {
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
/**
waitSemaphoreCount、pWaitSemaphores 和pWaitDstStageMask 成员变量用于指定
队列开始执行前需要等待的信号量,以及需要等待的管线阶段
*/
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = waitSemaphores;
//waitStages 数组中的条目和 pWaitSemaphores 中相同索引的信号量相对应。
submitInfo.pWaitDstStageMask = waitStages;
//指定实际被提交执行的指令缓冲对象
//我们应该提交和我们刚刚获取的交换链图像相对应的指令缓冲对象
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffers[imageIndex];
VkSemaphore signalSemaphores [ ] = {
renderFinishedSemaphores[currentFrame]};
//指定在指令缓冲执行结束后发出信号的信号量对象
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;
/**
vkQueueSubmit 函数使用vkQueueSubmit结构体数组作为参数,可以同时大批量提交数.。
vkQueueSubmit 函数的最后一个参数是一个可选的栅栏对象,
可以用它同步提交的指令缓冲执行结束后要进行的操作。
在这里,我们使用信号量进行同步,没有使用它,将其设置为VK_NULL_HANDLE
*/
//提交指令缓冲给图形指令队列
if(vkQueueSubmit(graphicsQueue,1,&submitInfo,
inFlightFences[currentFrame])!= VK_SUCCESS){
throw std::runtime_error("failed to submit draw command buffer!");
}
/**
* 将渲染的图像返回给交换链进行呈现操作
*/
//配置呈现信息
VkPresentInfoKHR presentInfo = {};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
//指定开始呈现操作需要等待的信号量
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;
//指定了用于呈现图像的交换链,以及需要呈现的图像在交换链中的索引
VkSwapchainKHR swapChains [ ] = {swapChain };
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = swapChains;
presentInfo.pImageIndices = &imageIndex;
/**
我们可以通过 pResults 成员变量获取每个交换链的呈现操作是否成功
的信息。在这里,由于我们只使用了一个交换链,可以直接使用呈现函数
的返回值来判断呈现操作是否成功
*/
presentInfo.pResults = nullptr;
//请求交换链进行图像呈现操作
vkQueuePresentKHR( presentQueue , &presentInfo ) ;
/**
如果开启校验层后运行程序,观察应用程序的内存使用情况,
可以发现我们的应用程序的内存使用量一直在慢慢增加。这是由于我
们的 drawFrame 函数以很快地速度提交指令,但却没有在下一次指令
提交时检查上一次提交的指令是否已经执行结束。也就是说 CPU 提交
指令快过 GPU 对指令的处理速度,造成 GPU 需要处理的指令大量堆
积。更糟糕的是这种情况下,我们实际上对多个帧同时使用了相同的
imageAvailableSemaphore 和 renderFinishedSemaphore 信号量。
最简单的解决上面这一问题的方法是使用 vkQueueWaitIdle 函数来等
待上一次提交的指令结束执行,再提交下一帧的指令:
但这样做,是对 GPU 计算资源的大大浪费。图形管线可能大部分时
间都处于空闲状态.
*/
//等待一个特定指令队列结束执行
vkQueueWaitIdle ( presentQueue ) ;
//更新currentFrame
currentFrame = (currentFrame+1) %MAX_FRAMES_IN_FLIGHT;
}
//清除为每一帧创建的信号量和VkFence 对象--12
for(size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++){
//所有它所同步的指令执行结束后,对它进行清除
vkDestroySemaphore(device,renderFinishedSemaphores[i],nullptr);
vkDestroySemaphore(device,imageAvailableSemaphores[i],nullptr);
vkDestroyFence(device ,inFlightFences[i], nullptr);
}