原文地址 : vulkan-tutorial
接下来我们将使用drawFrame()
函数将三角形显示到屏幕上。
drawFrame()要做如下几件事:
从Swap Chain 请求一个image。
执行带有这个image的command buffer ,这个image曾被当做attachment存储在framebuffer中(Execute the command buffer with that image as attachment in the framebuffer)。
虽然所有的操作都在一个函数里运行,但是它们的执行却是异步的,这个函数调用在它真正完成任务前就会返回,并且函数内部各个操作的执行顺序也不是确定的。这就产生了一个问题,因为我们希望后一个操作在前一个操作完成后才进行。
在Vulkan中可以使用两种方法进行同步:fences
和 semaphore
,他们都能够使一个操作发送信号(signal),另一个操作等待(wait) fence
或者semaphore
, 最终使的fence
或semaphore
从unginaled
状态变为signaled
状态。所不同的是fence
可以在程序中使用vkWaitForFences()
来获取状态,而semaphore
则不可以。Fence
主要在渲染操作时同步应用自身(synchronize your application itself with rendering operation),而semaphore
被设计为同步一个或跨多个命令队列工作。而我们想要同步绘画命令的队列操作和显示命令的队列操作,所以semaphore
更为适合。
// image 已经得到,可以被渲染了
VDeleter imageAvailableSemaphore {device, vkDestroySemaphore};
// image 渲染完毕可以被提交显示了
VDeleter renderFinishedSemaphore {device, vkDestroySemaphore};
添加创建semaphore的函数:
void createSemaphores() {
VkSemaphoreCreateInfo semaphoreInfo = {};
semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphore) != VK_SUCCESS ||
vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphore) != VK_SUCCESS) {
throw std::runtime_error("failed to create semaphores!");
}
}
drawFrame
函数的第一步是要获取swap chain里的image,因为swap chain是一个扩展功能,所以需要使用vk***KHR
命名约定:
void drawFrame() {
uint32_t imageIndex;
vkAcquireNextImageKHR(device, swapChain,
std::numeric_limits ::max(),
imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex);
}
让我们来看看 vkAcquireNextImageKHR(…)
:
VkResult vkAcquireNextImageKHR(
VkDevice device,
VkSwapchainKHR swapchain,
uint64_t timeout,
VkSemaphore semaphore,
VkFence fence,
uint32_t* pImageIndex);
timeout
表示等待时间,单位纳秒,如果timeout=0,
函数不被阻塞,立刻返回:成功或者VK_NOT_READY
。使用64位整数的最大值表示一直等待,直到获得数据。同步既可以使用semaphore
也可以使用fence
,或者两者一起。 pImageIndex
表示可使用的Image索引,对应于swapChainImages
数组,我们将用这个索引来选择合适的Command buffer 。
通过VkSubmitInfo
来配置Command buffer 提交到队列和同步控制:
VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
VkSemaphore waitSemaphores[] = {imageAvailableSemaphore};
VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = waitSemaphores;
submitInfo.pWaitDstStageMask = waitStages;
waitStages
表示pipeline将在何处等待,我希望当image 可以访问时,将颜色写入image,所以定义stage为pipeline的写color atatchement.
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffers[imageIndex];
提交哪一个command buffer 运行。
VkSemaphore signalSemaphores[] = {renderFinishedSemaphore};
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;
当Command buffer 执行完成后,哪一个semaphore发送信号(signal).
提交Command buffer 到队列的函数:
VkResult vkQueueSubmit(
VkQueue queue, //队列类型
uint32_t submitCount, //submitInfo数目
const VkSubmitInfo* pSubmits,
VkFence fence); //执行完时发送信号
现在提交我们的Command buffer 到图形队列:
if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) {
throw std::runtime_error("failed to submit draw command buffer!");
}
不知你是否记得,Render Pass 中的 subpass
自动处理image (attachment)的layout转换,这些转换被subpass 依赖(subpass dependencies) 所控制,它指定了subpass
间的内存和执行依赖,虽然我们只有一个subpass,但是在执行这个subpass的前后操作也被隐式的当做subpass了。
有两个内置的依赖(built-in dependencies)控制render pass前和render pass后的转换,前者出现的时机并不正确,因为它假定render pass前的转换发生在pipeline开始的时候,但是这个时候我们还没有获得image呢!因为image是在VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT
阶段(stage)才获得的。所以我们必须重写/覆盖这个依赖。
现在回到createRenderPass
中。
Vulkan 使用VkSubpassDependency
来描述这些依赖:
VkSubpassDependency dependency = {};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;
srcSubpass
和dstSubpass
分别表示依赖的索引和从属的subpass(即生产者与消费者的索引). VK_SUBPASS_EXTERNAL
代表Render pass 前或后的隐含的subpass,这取决于VK_S**_EX**L
是被定义在src还是dst。索引0指向我们定义的第一个同时也是唯一的一个subpass。dst
必须大于src
以防止循环依赖。
dependency.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT;
dependency.srcStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
这两个字段分别定义了我们将在什么操作上等待以及这个操作在何种阶段发生。我们必须等待Swap Chain从image读完之后才能访问它,这个操作发生在pipeline的最后阶段。
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT |
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
读和写 color attachment 操作必须在 _COLOR_ATTACHMENT_
阶段进行等待,这些设置保证:除非必要(如:当我们真的想写颜色(color)的时候)否则转换将不会发生。
现在让我们回到创建Render Pass阶段,修改 renderPassInfo
:
renderPassInfo.dependencyCount = 1;
renderPassInfo.pDependencies = &dependency;
绘画帧(drawing a frame)的最后一步就是把绘画结果返回到Swap Chain里,以致最终显示到屏幕上。显示(Presentation) 通过VkPresentationKHR
来配置:
VkPresentInfoKHR presentInfo = {};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;
pWaitSemaphores
表示显示前需要等待的信号量(semaphore),如同VkSubmitInfo
。
VkSwapchainKHR swapChains[] = {swapChain};
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = swapChains;
presentInfo.pImageIndices = &imageIndex;
pSwapchains
表示将要把image 提交到的Swap Chain的数组。 pImageIndices
表示要提交到Swap Chain的image索引数组,总是一个数据(pImageIndices
中的每一个元素对应pSwapchains
中的每一个元素)。
presentInfo.pResults = nullptr; // Optional
这是最后一个字段,表示每个Swap Chain的显示(Presentation)结果是否正确,它是一个VkResult
数组,因为我们只有一个Swap Chain和一个image,所以通过函数返回值就能判断了。
vkQueuePresentKHR(presentQueue, &presentInfo);
vkQueuePresentKHR
提交显示请求。
之后我们要为vkAcquireNextImageKHR
和 vkQueuePresentKHR
添加错误控制方法,因为他们的失败并不意味着程序必须终止。
运行程序你将看到 :
如果你的Validation Layers 可用的话,当关闭窗口时就会在控制台收到从debugCallback
返回的debug信息:
因为drawFrame
里的操作是异步的,这就意味着当我们关掉主循环(mainLoop()
函数)时,绘图操作和显示操作可能还在进行,这个时候剥夺它们的资源是不明智的。
为了修正这个错误,可以在主mainLoop()
退出前先等待Logical Device 结束:
void mainLoop() {
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
drawFrame();
}
vkDeviceWaitIdle(device);
}
同样也可以使用vkQueueWaitIdle
来等待具体某个命令队列里的的某个操作的结束。
我们用了800多行的代码才完成了使一个简单的三角形显示到屏幕上的功能,这确实是一个巨大的工作,但也从侧面反映了一个事实:Vulakn 给你提供了更多显示控制硬件的权利。 我建议你花费一些时间重读这些代码,在脑中构想出一幅模型图画,想一想每个Vulkan对象的作用以及它们之间的联系。
源码 : source code