设置
所有内容都汇集到这一章节了。我们编写一个drawFrame
函数来将三角形绘制到屏幕上。创建该函数并在mainLoop
中调用:
void mainLoop() {
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
drawFrame();
}
}
...
void drawFrame() {
}
同步
drawFrame
函数将执行如下操作:
- 从交换链中获取图像
- 将图像作为帧缓冲附件并执行命令缓冲区
- 将图像返还到交换链进行显示
这些操作都由一个函数调用来执行,但它们是异步执行的。函数会在操作完成先返回,操作执行顺序是未定义的。但每个操作都依赖前一步操作,这可不行。
有两种同步交换链时间的方法:栅栏和信号量。它们都是可用于协调操作的对象,操作等待栅栏或信号量变为有信号状态,然后继续执行。
栅栏和信号量的区别是栅栏可以通过vkWariForFences
从程序访问,而信号量不行。栅栏主要设计用来同步应用程序本身和渲染操作,而信号量用于同步命令队列内或命令队列间的操作。我们希望同步绘制命令和呈现操作,这里使用信号量最合适。
信号量(Semaphores)
我们需要一个信号量来表示图像已经被获取并准备好渲染,另一个信号量来表示渲染已经完成并且可以进行展示。创建两个类成员保存信号量对象:
VkSemaphore imageAvailableSemaphore;
VkSemaphore renderFinishedSemaphore;
创建函数createSemaphores
创建信号量:
void initVulkan() {
...
createCommandBuffers();
createSemaphores();
}
...
void createSemaphores() {
}
创建信号量的结构体VkSemaphoreCreateInfo
现在还没有任何参数:
void createSemaphores() {
VkSemaphoreCreateInfo semaphoreInfo{};
semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
}
未来该机构提可能会添加flags
或pHext
参数来扩展功能。创建信号量遵循Vulkan创建模式:
if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphore) != VK_SUCCESS ||
vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphore) != VK_SUCCESS) {
throw std::runtime_error("failed to create semaphores!");
}
在程序最后销毁信号量:
void cleanup() {
vkDestroySemaphore(device, renderFinishedSemaphore, nullptr);
vkDestroySemaphore(device, imageAvailableSemaphore, nullptr);
从交换链获取图像
drawFrame
函数要做的第一件事就是从交换链获取一个图像。交换链是一个扩展特性,所以我们需要使用具有vk*KHR
命名约定的函数:
void drawFrame() {
uint32_t imageIndex;
vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex);
}
vkAcquireNextImageKHR
的前两个参数是逻辑设备和交换链。第三个参数为超时时间,UINT64_MAX表示禁用超时。
接着两个参数表示呈现引擎使用过图像时发出的信号对象。有该信号就表示我们可以绘制该图像了。这里可以指定信号量和栅栏。我们使用前面创建的imageAvailableSemaphore
。
最后一个参数输出可用的交换链图像的索引。该索引指向我们swapChainImages
数组中的VkImage
。我们使用该索引选择正确的命令缓冲区。
提交命令缓冲区
队列提交和同步通过VkSubmitInfo
来配置。
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
数组对应的每个条目对应于pWaitSemaphores
中相同索引的信号量(?)。
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffers[imageIndex];
接下来两个参数指定要提交的命令缓冲区,选择交换链图像对应的那个。
VkSemaphore signalSemaphores[] = {renderFinishedSemaphore};
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;
signalSemaphoreCount
和pSignalSemaphores
参数指定命令缓冲区完成后要发出的信号量。我们这里使用renderFinishedSemaphore
。
if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) {
throw std::runtime_error("failed to submit draw command buffer!");
}
现在,我们可以使用vkQueueSubmit
来将命令缓冲区提交到图形队列。该函数接受VkSubmitInfo
数组,以提高效率。最后一个参数可以指定一个栅栏,来表明命令缓冲区执行完毕。我们这边用信号量来做同步,所以这里设置为VK_NULL_HANDLE
。
子通道依赖
我们要记得,渲染通道中的子通道会自动处理图像布局转换。这些转换被子通道依赖控制,子通道依赖指定了子通道间的内存和执行依赖关系。我们现在只有一个子通道,但其前后的操作也算作隐式“子通道”。