Vulkan教程 - 11 帧缓冲和命令缓冲

2024-08-21 19:58
文章标签 命令 教程 缓冲 vulkan

本文主要是介绍Vulkan教程 - 11 帧缓冲和命令缓冲,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

        帧缓冲我们前面的章节已经讨论很多了,而且我们已经建立了渲染通道,以便得到单个的帧缓冲,有着和交换链图像一样的格式,但是我们还没有真正创建什么东西呢。

        在渲染通道创建过程中指定的附件通过把它们包装成一个VkFramebuffer对象来绑定到一起。帧缓冲对象引用了所有表示附件的VkImageView对象。我们这里就一个附件,即颜色附件。但是,我们为了这个附件要用的图像依赖于当我们获取图像用于呈现的时候交换链返回的是哪个图像。也就是说我们要为交换链中所有的图像创建一个帧缓冲,然后使用一个和绘制时获取的图像对应的图像。

        创建一个std::vector类型的类成员,存储帧缓冲用:

std::vector<VkFramebuffer> swapChainFramebuffers;

        我们会在一个新的方法中为这个数组创建对象,这个方法是createFramebuffers,在initVulkan方法的创建图形管线之后调用。

        一开始要调整容器大小以容纳所有帧缓冲:

void createFramebuffers() {swapChainFramebuffers.resize(swapChainImageViews.size());
}

        我们接着会遍历图像视图并从中创建帧缓冲:

for (size_t i = 0; i < swapChainImageViews.size(); i++) {VkImageView attachments[] = {swapChainImageViews[i]};VkFramebufferCreateInfo framebufferInfo = {};framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;framebufferInfo.renderPass = renderPass;framebufferInfo.attachmentCount = 1;framebufferInfo.pAttachments = attachments;framebufferInfo.width = swapChainExtent.width;framebufferInfo.height = swapChainExtent.height;framebufferInfo.layers = 1;if (vkCreateFramebuffer(device, &framebufferInfo, nullptr, &swapChainFramebuffers[i]) != VK_SUCCESS) {throw std::runtime_error("failed to create framebuffer!");}
}

        正如你看到的,创建帧缓冲是比较直白的。首先需要指定帧缓冲和哪个renderPass兼容。只能用兼容的,也就是说它们使用相同个数和类型的附件。

        attachmentCount和pAttachments参数指定在渲染通道pAttachment数组中要绑定到各自附件描述的VkImageView对象。

        width和height参数不用解释,layers指的是图像数组中的层的个数。我们这里交换链图像是单图像的,所以层个数就是1。我们应该在图像视图和渲染通道之前删除帧缓冲,但是要在完成渲染之后:

for (auto framebuffer : swapChainFramebuffers) {vkDestroyFramebuffer(device, framebuffer, nullptr);
}

        现在已经完成了渲染所需的各项要求,下一章我们将写一个真正的绘制命令。

        Vulkan中的命令,比如绘制操作和内存转移,并不是直接用方法调用来执行的。你必须把所有操作记录到命令缓冲对象中。这么做的优势是建立绘制命令这种困难的工作能够提前做好,且是多线程做的。这样,你就能告诉Vulkan来执行主循环中的命令了。

        在我们创建命令缓冲之前,必须要创建一个命令池。命令池管理用于存储缓冲的内存,命令缓冲也是从它们中分配的。添加一个新的类成员来存储VkCommandPool:

VkCommandPool commandPool;

        然后创建一个新的方法createCommandPool,然后从initVulkan中调用,调用时机是在创建帧缓冲之后。命令池创建只需要两个参数:

QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice);VkCommandPoolCreateInfo poolInfo = {};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value();
poolInfo.flags = 0;

        命令缓冲通过提交到某个设备队列上执行,如我们获取到的图形和呈现队列。每个命令池只能分配单一类型队列中提交的命令缓冲。我们会记录命令来进行绘制,这也是为什么我们选择了图形队列族。

        命令池有两种可能的标记:

        VK_COMMAND_POOL_CREATE_TRANSIENT_BIT:表明命令缓冲经常用新的命令记录(可能改变内存分配行为);

        VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT:允许命令缓冲逐个记录,没有这个标记则它们会统一进行重置。

        我们仅仅在程序开始的时候记录命令缓冲,然后在主循环中把它们执行很多次,所以我们并不会用到上面的两种标记:

if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) {throw std::runtime_error("failed to create command pool!");
}

        使用vkCreateCommandPool方法完成命令池创建。程序整个生命周期都会用到命令,因此它们要在结束的时候销毁,就放在cleanup的第一行:

vkDestroyCommandPool(device, commandPool, nullptr);

        现在我们可以分配内存缓冲了,另外记录它们的绘制命令。因为有一个绘制命令涉及到绑定正确的VkFramebuffer,我们要为交换链中的每一个图像再次记录一个命令缓冲。为此创建一个VkCommandBuffer列表,作为类成员。命令缓冲会在命令池销毁的时候自动释放,所以不用在cleanup方法中进行显式处理。

std::vector<VkCommandBuffer> commandBuffers;

        现在开始创建一个createCommandBuffers方法,它负责分配和记录每个交换链图像的命令:

void createCommandBuffers() {commandBuffers.resize(swapChainFramebuffers.size());
}

        该方法就在initVulkan的最后调用。

        命令缓冲分配用的是vkAllocateCommandBuffers方法,接收一个VkCommandBufferAllocateInfo结构体作为参数,指定命令池和要分配的缓冲个数:

VkCommandBufferAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = (uint32_t)commandBuffers.size();if (vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()) != VK_SUCCESS) {throw std::runtime_error("failed to allocate command buffers!");
}

        level参数表明分配的命令缓冲是是主命令缓冲,还是次要命令缓冲:

        VK_COMMAND_BUFFER_LEVEL_PRIMARY:可以提交到队列执行,但是不能从其他命令缓冲中调用;

        VK_COMMAND_BUFFER_LEVEL_SECONDARY:不能直接提交,但是可以从主命令缓冲中调用。

        我们不会用次要命令缓冲,但是你可以想象下,从主命令缓冲中重用通用的操作是很有用的。

        我们用vkBeginCommandBuffer开始记录命令缓冲,传一个小结构体VkCommandBufferBeginInfo作为其参数,指定一些命令缓冲使用的细节信息:

for (size_t i = 0; i < commandBuffers.size(); i++) {VkCommandBufferBeginInfo beginInfo = {};beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;beginInfo.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;beginInfo.pInheritanceInfo = nullptr;  // optionalif (vkBeginCommandBuffer(commandBuffers[i], &beginInfo) != VK_SUCCESS) {throw std::runtime_error("failed to begin recording command buffer!");}
}

        flags标记参数表明了我们如何使用命令缓冲,有以下可选项:

        VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT:命令缓冲将一旦执行后就被记录;

        VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT:这是完全在一个渲染通道中的次命令缓冲;

         VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT:命令缓冲可以在挂起执行的情况下重新提交。

        我们用了最后一个标记,因为我们可能在最后一帧还没完成的时候已经为下一帧计划绘制命令了。pInheritanceInfo参数只和次命令缓冲有关。它指定了从调用的主命令缓冲的哪个状态继承。

        如果命令缓冲已经记录了一次,那么调用vkBeginCommandBuffer会隐式地重置它。后面就不可能将命令追加到缓冲中了。

        绘制就从用vkCmdBeginRenderPass开启渲染通道开始。渲染通道使用一些VkRenderPassBeginInfo结构体中的参数配置:

VkRenderPassBeginInfo renderPassInfo = {};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = renderPass;
renderPassInfo.framebuffer = swapChainFramebuffers[i];

        第一个参数是渲染通道自身和要绑定的附件。我们为每个交换链图像创建一个帧缓冲,把它作为颜色附件。

renderPassInfo.renderArea.offset = { 0, 0 };
renderPassInfo.renderArea.extent = swapChainExtent;

        接下来的这两个参数定义了渲染区域大小,渲染区域定义了着色器加载和存储的地点,在此之外的像素的值将会是未定义的。它应该和附件的大小一致,以便取得最佳性能。

VkClearValue clearColor = { 0.0f, 0.0f, 0.0f, 1.0f };
renderPassInfo.clearValueCount = 1;
renderPassInfo.pClearValues = &clearColor;

        最后的两个参数定义了VK_ATTACHMENT_LOAD_OP_CLEAR要用的清除值,我们用作颜色附件的加载操作。这里定义的清除颜色就是一个很简单的完全不透明的黑色。

vkCmdBeginRenderPass(commandBuffers[i], &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);

        现在渲染通道可以开始了,所有的记录命令的功能都有vkCmd前缀。它们都返回void,所以直到我们完成记录之前都不会有错误处理。

        每个命令的第一个参数一直都是要记录命令的命令缓冲。第二个参数明确了我们提供的渲染通道的细节信息。最终的参数控制渲染通道内的绘制命令如何提供。有以下两种值可选:

        VK_SUBPASS_CONTENTS_INLINE:渲染通道命令将会嵌入到主命令缓冲中,次命令缓冲不会执行;

        VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS:渲染通道命令将会从次命令缓冲执行。

        我们不用次命令缓冲,所以这里就用第一个选项。

        现在我们可以绑定图形管线了:

vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);

        第二个参数说明了该管线对象是否是一个图形或者计算管线。我们现在告诉了Vulkan在图形管线中执行哪个操作以及在片段着色器中使用哪个附件,所以现在剩下的就是告诉它绘制三角形:

vkCmdDraw(commandBuffers[i], 3, 1, 0, 0);

        实际的vkCmdDraw有些虎头蛇尾,但是它太简单了,因为所有的信息我们都提前说明了。除了命令缓冲外它还有以下参数:

        vertexCount:尽管我们没用顶点缓冲,但是严格来说还是有三个顶点要绘制的;

        instanceCount:用于实例渲染,如果没有这么做的话就设置为1;

        firstVertex:作为顶点缓冲的偏置,定义了gl_VertexIndex的最小值;

        firstInstance:作为实例渲染偏置,定义了gl_InstanceIndex的最小值。

        现在渲染通道可以结束了:

vkCmdEndRenderPass(commandBuffers[i]);

        现在已经完成了命令缓冲的记录:

if (vkEndCommandBuffer(commandBuffers[i]) != VK_SUCCESS) {throw std::runtime_error("failed to record command buffer!");
}

        下一章我们会写一些代码,放在主循环中,获取交换链图像,执行正确的命令缓冲并返回完成的图像到交换链中。

这篇关于Vulkan教程 - 11 帧缓冲和命令缓冲的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

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文件

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

30常用 Maven 命令

Maven 是一个强大的项目管理和构建工具,它广泛用于 Java 项目的依赖管理、构建流程和插件集成。Maven 的命令行工具提供了大量的命令来帮助开发人员管理项目的生命周期、依赖和插件。以下是 常用 Maven 命令的使用场景及其详细解释。 1. mvn clean 使用场景:清理项目的生成目录,通常用于删除项目中自动生成的文件(如 target/ 目录)。共性规律:清理操作

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 工程,引入依赖:

利用命令模式构建高效的手游后端架构

在现代手游开发中,后端架构的设计对于支持高并发、快速迭代和复杂游戏逻辑至关重要。命令模式作为一种行为设计模式,可以有效地解耦请求的发起者与接收者,提升系统的可维护性和扩展性。本文将深入探讨如何利用命令模式构建一个强大且灵活的手游后端架构。 1. 命令模式的概念与优势 命令模式通过将请求封装为对象,使得请求的发起者和接收者之间的耦合度降低。这种模式的主要优势包括: 解耦请求发起者与处理者

linux 判断某个命令是否安装

linux 判断某个命令是否安装 if ! [ -x "$(command -v git)" ]; thenecho 'Error: git is not installed.' >&2exit 1fi