Vulkan教程 - 12 栅栏和信号量

2024-08-21 19:58
文章标签 教程 信号量 vulkan 栅栏

本文主要是介绍Vulkan教程 - 12 栅栏和信号量,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

        这一章所有东西都会整合到一起了。我们将会写一个drawFrame方法,它会被主循环调用,将三角形呈现到屏幕上。创建drawFrame方法在mainLoop的while内处理事件后调用:

void mainLoop() {while (!glfwWindowShouldClose(window)) {glfwPollEvents();drawFrame();}
}

        该方法会执行下面的操作:

        从交换链获取图像;

        用该图像作为帧缓冲中的附件来执行命令缓冲;

        将图像返回给交换链以便呈现。

        这些事件每一个都是使用单个方法调用来启动的,但是它们都是异步的。方法调用会在实际操作结束之前返回,且执行顺序也是不一定的。这就很不幸了,因为每个操作都依赖于之前的操作完成才行。

        有两种方式来同步交换链事件:栅栏和信号量。它们俩对象都能用于协调操作,方式就是设置一个操作信号,另一个操作等到一个栅栏或者信号量,然后从一个未标记的状态变成标记的状态。

        不同之处是栅栏状态可以从你的程序中通过类似vkWaitForFences的调用来访问,而信号量却不行。栅栏主要是设计用于同步你的应用和渲染操作的,然而信号量用于同步命令队列操作。我们想要同步绘制命令和呈现的序列操作,选用信号量最合适。

        我们需要一个信号量标记一个图像已经获取到且准备渲染就绪,另一个信号量用于标记渲染已经完成且可以呈现了。创建两个类成员来存储:

VkSemaphore imageAvailableSemaphore;
VkSemaphore renderFinishedSemaphore;

        为了创建信号量,我们还需要添加最后一个create方法,也就是createSemaphores,放在initVulkan最后。创建信号量需要填写VkSemaphoreCreateInfo,但是当前版本的API就只要求sType为必填项:

void createSemaphores() {VkSemaphoreCreateInfo semaphoreInfo = {};semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
}

        未来版本的Vulkan或者扩展可能为flags和pNext参数添加功能,就和别的结构体那样。创建信号量如下:

if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphore) != VK_SUCCESS ||vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphore) != VK_SUCCESS) {throw std::runtime_error("failed to create semaphores!");
}

        信号量要在程序结束的地方清理,这时所有命令都完成了且没有更多必要的同步要做:

vkDestroySemaphore(device, renderFinishedSemaphore, nullptr);
vkDestroySemaphore(device, imageAvailableSemaphore, nullptr);

        放在cleanup开头处。

        像前面提到的,drawFrame中第一个要做的事就是从交换链获取图像。回想下,交换链是扩展特性,所以我们必须用vk*KHR命名习惯:

void drawFrame() {uint32_t imageIndex;vkAcquireNextImageKHR(device, swapChain, std::numeric_limits<uint64_t>::max(),imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex);
}

        vkAcquireNextImageKHR前两个参数是逻辑设备和我们想要获取图像的交换链,第三个参数指定以纳秒为单位的图像可用性超时。使用64位无符号整数的最大值来禁用超时。

        接着两个参数指定当呈现引擎使用该图像完成任务时标记的同步对象。就是这个时间点,我们可以开始向它绘制东西。可以指定一个信号量,栅栏或者两者都有。我们这里使用imageAvailableSemaphore。

        最后一个参数指定一个变量输出可用的交换链图像索引。索引引用swapChainImages数组中的VkImage,我们会使用该索引来取得正确的命令缓冲。

        队列提交和同步通过VkSubmitInfo配置:

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即可。

        记住,渲染通道中的子通道自动处理图像布局转移问题。这些转移是子通道依赖控制的,指定了子通道的内存和执行依赖。我们只有一个子通道,但是该子通道之前或之后紧挨着的操作也是隐形的子通道。有两个内置的依赖负责在渲染通道的开头和结尾处转移,但是前者不会在正确的时间发生。它假设转移发生在管线的起始处,但是我们那时候还没有获取图像呢。有两个办法来处理该问题,我们可以改变imageAvailableSemaphore的waitStages为VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT来确保渲染通道不会开始,直到图像可用;或者使渲染通道等待VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT阶段。我决定用第二种,因为可以更好观察子通道依赖和它们如何工作的:

        子通道依赖是VkSubpassDependency中设定的,在createRenderPass方法中添加一个:

VkSubpassDependency dependency = {};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;

        开始的两个字段指定了依赖和被依赖子通道的索引。特殊值VK_SUBPASS_EXTERNAL指的是渲染通道之前或之后根据是否在srcSubpass或dstSubpass中要依赖的隐含子通道。索引0指的是我们的子通道,也是第一个和仅有的一个。dstSubpass一定要一直比srcSubpass高,以防止依赖图循环问题。

dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.srcAccessMask = 0;

        接着两个字段指定了要等待的操作和这些操作在什么阶段产生。我们要等到交换链完成从图像的读取才能访问图像。这可以通过等待颜色附件输出阶段来做:

dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT |VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;

        应该等待这个的操作是在颜色附件阶段,且涉及到读取和写入颜色附件。这些设置将会阻止转移的发生,直到有必要或者允许的时候,也就是我们想要开始向它写入颜色的时候。

renderPassInfo.dependencyCount = 1;
renderPassInfo.pDependencies = &dependency;

        VkRenderPassCreateInfo有两个字段指定一组依赖。

        绘制一帧的最后一步是提交结果到交换链,让它最终显示到屏幕上。呈现通过VkPresentInfoKHR配置,就在drawFrame方法的末尾处:

VkPresentInfoKHR presentInfo = {};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;

        前面两个参数指定了呈现开始之前等待哪个信号量,就和VkSubmitInfo一样。

VkSwapchainKHR swapChains[] = {swapChain};
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = swapChains;
presentInfo.pImageIndices = &imageIndex;

        后面两个参数指定了图像呈现的目的交换链,以及每个交换链图像索引。这基本上都是一个。

presentInfo.pResults = nullptr;  // optional

        还有最后一个可选参数pResults。它能让你指定一个VkResult数组值来检查每个交换链是否成功呈现。如果只用了一个交换链就不必检查,因为可以直接用呈现函数的返回值。

vkQueuePresentKHR(presentQueue, &presentInfo);

        vkQueuePresentKHR方法提交请求呈现图像给交换链。我们给vkAcquireNextImageKHR和vkQueuePresentKHR添加错误处理,因为它们的问题不一定要让程序停止,这并不像我们之前看到的那些方法。

        如果你到目前为止把所有事情都做好了,那么运行的时候应该能看到类似这样的东西:

        对!不幸的是,启用了验证层,当你关闭程序的时候他就崩溃了。终端中debugCallback给出的信息如下:

        drawFrame中所有的操作都是异步的,这意味着当我们退出mainLoop的时候,绘制和呈现操作可能还在进行中,这时候就清理资源就不是个好办法。

        为了解决这个问题,我们要等逻辑设备结束操作的时候,且在退出mainLoop之前,销毁该窗口:

void mainLoop() {while (!glfwWindowShouldClose(window)) {glfwPollEvents();drawFrame();}vkDeviceWaitIdle(device);
}

        也可以用vkQueueWaitIdle在一个特定的命令队列中等待操作完成,这些方法可以用于原始的同步方式。此时可以看到退出的时候就不报错了:

        最终我们成功显示了一个三角形:

        文档已经看到145页了,才绘制出一个三角形,Vulkan太高效了呀!

        如果你开启了验证层后运行程序,且你监视内存使用的话,你可能会注意到它会慢慢增加,原因是该程序在drawFrame方法中快速地提交工作,但是实际上却不检查这些工作是否完成了。如果CPU提交的工作比GPU能处理的快,那么队列会慢慢被工作填充满。更糟糕的是,我们同时对多个帧重用imageAvailableSemaphore和renderFinishedSemaphore信号量。

        解决该问题的简单方法是在drawFrame末尾提交后就等待工作完成:

vkQueuePresentKHR(presentQueue, &presentInfo);
vkQueueWaitIdle(presentQueue);

        但是这样就不是高效使用GPU的方式了,因为一个时间点整个图形管线就只能被一个帧使用。当前帧已经处理过的阶段空闲着,可能已经被下一帧用了。我们现在扩展下以允许多个帧在绑定的一定工作量增长的时候还能继续准备。

        在程序开始的时候添加一个常数,以定义帧并发量:

const int MAX_FRAMES_IN_FLIGHT = 2;

        每一帧应该有它自己的一套信号量:

std::vector<VkSemaphore> imageAvailableSemaphores;
std::vector<VkSemaphore> renderFinishedSemaphores;

        createSemaphores方法也要改成创建所有这些信号量:

void createSemaphores() {imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT);renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT);VkSemaphoreCreateInfo semaphoreInfo = {};semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {if (vkCreateSemaphore(device, &semaphoreInfo, nullptr,&imageAvailableSemaphores[i]) != VK_SUCCESS ||vkCreateSemaphore(device, &semaphoreInfo, nullptr,&renderFinishedSemaphores[i]) != VK_SUCCESS) {throw std::runtime_error("failed to create semaphores!");}}
}

        类似的,在cleanup中清理:

for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++)
{vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr);vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr);
}

        为了每次使用正确配对的信号量;我们要跟踪当前帧,这里设置一个帧索引:

size_t currentFrame = 0;

        drawFrame方法现在就修改成这个样子以使用正确的对象:

vkAcquireNextImageKHR(device, swapChain, std::numeric_limits<uint64_t>::max(),imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);
...
VkSemaphore waitSemaphores[] = { imageAvailableSemaphores[currentFrame] };
...
VkSemaphore signalSemaphores[] = { renderFinishedSemaphores[currentFrame] };

        当然,不要忘记每次都将其推到下一帧:

currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;

        通过使用取模运算符,我们确保了帧索引每当MAX_FRAMES_IN_FLIGHT个帧入队时循环一次。

        尽管现在我们设置了需要的对象来促进同时使用多帧,我们还是没有防止有多余MAX_FRAMES_IN_FLIGHT的工作提交。当前只有GPU-GPU同步而没有CPU-GPU同步以跟踪任务如何进行。我们可能在0号帧还未准备好的时候用0号帧的对象。

        为了进行CPU-GPU同步,Vulkan提供了第二种同步原语叫做栅栏。栅栏和信号量类似,它们可以被标记和等待,但是这次我们实际上在自己的代码中等待。我们先为每一个帧创建一个栅栏:

std::vector<VkSemaphore> imageAvailableSemaphores;
std::vector<VkSemaphore> renderFinishedSemaphores;
std::vector<VkFence> inFlightFences;
size_t currentFrame = 0;

        我决定和创建信号量和栅栏放在一起,然后把createSemaphores命名为createSyncObjects:

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;for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {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!");}}
}

        栅栏创建和信号量类似,也要在cleanup中清理:

vkDestroyFence(device, inFlightFences[i], nullptr);

        现在改一下drawFrame来用栅栏进行同步。vkQueueSubmit调用包括了一个可选参数来传递当命令缓冲完成执行的时候应该标记的栅栏。我们可以用这个来标记一个帧已经完成了:

if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) {throw std::runtime_error("failed to submit draw command buffer!");
}

        现在唯一剩下的事情就是修改drawFrame开头来等待帧完成:

vkWaitForFences(device, 1, &inFlightFences[currentFrame],VK_TRUE, std::numeric_limits<uint64_t>::max());
vkResetFences(device, 1, &inFlightFences[currentFrame]);

        vkWaitForFences方法接收一个栅栏数组变量,返回之前等待它们中有一些或全部都标记好了。VK_TRUE表示我们想要等待所有的栅栏,但是单个的情况下自然就没什么影响了。和vkAcquireNextImageKHR一样,本方法也接收一个超时参数。但是不像信号量那样,我们需要手动存储栅栏以通过调用vkResetFences重置来解除标记。

        如果现在运行程序会发现没有东西渲染出来,验证层给出的信息如下:

        意思是我们在等待一个还未提交的栅栏。这里的问题是,栅栏默认创建的时候是未标记状态。也就是说vkWaitForFences将会一直等待,如果我们都还没使用过该栅栏的话。为了解决该问题,我们可以修改栅栏的创建信息,初始化为signaled状态,如同我们渲染了一个已完成的初始帧:

VkFenceCreateInfo fenceInfo = {};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;

        现在程序应该正常工作了,内存泄露问题也没了。我们已经实现了所有需要的同步操作以保证不会有多余两个工作帧入队。

        至此,将内容展现到屏幕上的所有阶段我们都经历过了,九百多行代码画出了一个三角形。Vulkan给你很多的控制能力,各项设置都要明确给出,现在最好回想下这些代码都是干什么的,下章还要做一些额外的工作来让该程序成为以后开发构建的更好的根基。

这篇关于Vulkan教程 - 12 栅栏和信号量的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1094135

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Makefile简明使用教程

文章目录 规则makefile文件的基本语法:加在命令前的特殊符号:.PHONY伪目标: Makefilev1 直观写法v2 加上中间过程v3 伪目标v4 变量 make 选项-f-n-C Make 是一种流行的构建工具,常用于将源代码转换成可执行文件或者其他形式的输出文件(如库文件、文档等)。Make 可以自动化地执行编译、链接等一系列操作。 规则 makefile文件

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

沁恒CH32在MounRiver Studio上环境配置以及使用详细教程

目录 1.  RISC-V简介 2.  CPU架构现状 3.  MounRiver Studio软件下载 4.  MounRiver Studio软件安装 5.  MounRiver Studio软件介绍 6.  创建工程 7.  编译代码 1.  RISC-V简介         RISC就是精简指令集计算机(Reduced Instruction SetCom

前端技术(七)——less 教程

一、less简介 1. less是什么? less是一种动态样式语言,属于css预处理器的范畴,它扩展了CSS语言,增加了变量、Mixin、函数等特性,使CSS 更易维护和扩展LESS 既可以在 客户端 上运行 ,也可以借助Node.js在服务端运行。 less的中文官网:https://lesscss.cn/ 2. less编译工具 koala 官网 http://koala-app.

【Shiro】Shiro 的学习教程(三)之 SpringBoot 集成 Shiro

目录 1、环境准备2、引入 Shiro3、实现认证、退出3.1、使用死数据实现3.2、引入数据库,添加注册功能后端代码前端代码 3.3、MD5、Salt 的认证流程 4.、实现授权4.1、基于角色授权4.2、基于资源授权 5、引入缓存5.1、EhCache 实现缓存5.2、集成 Redis 实现 Shiro 缓存 1、环境准备 新建一个 SpringBoot 工程,引入依赖:

Windows环境利用VS2022编译 libvpx 源码教程

libvpx libvpx 是一个开源的视频编码库,由 WebM 项目开发和维护,专门用于 VP8 和 VP9 视频编码格式的编解码处理。它支持高质量的视频压缩,广泛应用于视频会议、在线教育、视频直播服务等多种场景中。libvpx 的特点包括跨平台兼容性、硬件加速支持以及灵活的接口设计,使其可以轻松集成到各种应用程序中。 libvpx 的安装和配置过程相对简单,用户可以从官方网站下载源代码

PHP APC缓存函数使用教程

APC,全称是Alternative PHP Cache,官方翻译叫”可选PHP缓存”。它为我们提供了缓存和优化PHP的中间代码的框架。 APC的缓存分两部分:系统缓存和用户数据缓存。(Linux APC扩展安装) 系统缓存 它是指APC把PHP文件源码的编译结果缓存起来,然后在每次调用时先对比时间标记。如果未过期,则使用缓存的中间代码运行。默认缓存 3600s(一小时)。但是这样仍会浪费大量C

Qt多语种开发教程

Qt作为跨平台的开发工具,早已应用到各行各业的软件开发中。 今天讲讲,Qt开发的正序怎么做多语言开发。就是说,你设置中文,就中文显示;设置英语就英文显示,设置繁体就繁体显示,设置发育就显示法语等。 开发环境(其实多语种这块根环境没太大关系):win10,Qt.5.12.10 一.先用QtCreator创建一个简单的桌面程序 1.工程就随便命名“LanguageTest”,其他默认。 2.在设计师

如何打造个性化大学生线上聊天交友系统?Java SpringBoot Vue教程,2025最新设计思路

✍✍计算机编程指导师 ⭐⭐个人介绍:自己非常喜欢研究技术问题!专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目:有源码或者技术上的问题欢迎在评论区一起讨论交流! ⚡⚡ Java实战 | SpringBoot/SSM Python实战项目 | Django 微信小程序/安卓实战项目 大数据实战项目 ⚡⚡文末获取源码 文章目录