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

相关文章

Linux join命令的使用及说明

《Linuxjoin命令的使用及说明》`join`命令用于在Linux中按字段将两个文件进行连接,类似于SQL的JOIN,它需要两个文件按用于匹配的字段排序,并且第一个文件的换行符必须是LF,`jo... 目录一. 基本语法二. 数据准备三. 指定文件的连接key四.-a输出指定文件的所有行五.-o指定输出

Linux jq命令的使用解读

《Linuxjq命令的使用解读》jq是一个强大的命令行工具,用于处理JSON数据,它可以用来查看、过滤、修改、格式化JSON数据,通过使用各种选项和过滤器,可以实现复杂的JSON处理任务... 目录一. 简介二. 选项2.1.2.2-c2.3-r2.4-R三. 字段提取3.1 普通字段3.2 数组字段四.

java中ssh2执行多条命令的四种方法

《java中ssh2执行多条命令的四种方法》本文主要介绍了java中ssh2执行多条命令的四种方法,包括分号分隔、管道分隔、EOF块、脚本调用,可确保环境配置生效,提升操作效率,具有一定的参考价值,感... 目录1 使用分号隔开2 使用管道符号隔开3 使用写EOF的方式4 使用脚本的方式大家平时有没有遇到自

全网最全Tomcat完全卸载重装教程小结

《全网最全Tomcat完全卸载重装教程小结》windows系统卸载Tomcat重新通过ZIP方式安装Tomcat,优点是灵活可控,适合开发者自定义配置,手动配置环境变量后,可通过命令行快速启动和管理... 目录一、完全卸载Tomcat1. 停止Tomcat服务2. 通过控制面板卸载3. 手动删除残留文件4.

Linux命令rm如何删除名字以“-”开头的文件

《Linux命令rm如何删除名字以“-”开头的文件》Linux中,命令的解析机制非常灵活,它会根据命令的开头字符来判断是否需要执行命令选项,对于文件操作命令(如rm、ls等),系统默认会将命令开头的某... 目录先搞懂:为啥“-”开头的文件删不掉?两种超简单的删除方法(小白也能学会)方法1:用“--”分隔命

Python的pandas库基础知识超详细教程

《Python的pandas库基础知识超详细教程》Pandas是Python数据处理核心库,提供Series和DataFrame结构,支持CSV/Excel/SQL等数据源导入及清洗、合并、统计等功能... 目录一、配置环境二、序列和数据表2.1 初始化2.2  获取数值2.3 获取索引2.4 索引取内容2

python依赖管理工具UV的安装和使用教程

《python依赖管理工具UV的安装和使用教程》UV是一个用Rust编写的Python包安装和依赖管理工具,比传统工具(如pip)有着更快、更高效的体验,:本文主要介绍python依赖管理工具UV... 目录前言一、命令安装uv二、手动编译安装2.1在archlinux安装uv的依赖工具2.2从github

C#实现SHP文件读取与地图显示的完整教程

《C#实现SHP文件读取与地图显示的完整教程》在地理信息系统(GIS)开发中,SHP文件是一种常见的矢量数据格式,本文将详细介绍如何使用C#读取SHP文件并实现地图显示功能,包括坐标转换、图形渲染、平... 目录概述功能特点核心代码解析1. 文件读取与初始化2. 坐标转换3. 图形绘制4. 地图交互功能缩放

SpringBoot集成redisson实现延时队列教程

《SpringBoot集成redisson实现延时队列教程》文章介绍了使用Redisson实现延迟队列的完整步骤,包括依赖导入、Redis配置、工具类封装、业务枚举定义、执行器实现、Bean创建、消费... 目录1、先给项目导入Redisson依赖2、配置redis3、创建 RedissonConfig 配

Redis 的 SUBSCRIBE命令详解

《Redis的SUBSCRIBE命令详解》Redis的SUBSCRIBE命令用于订阅一个或多个频道,以便接收发送到这些频道的消息,本文给大家介绍Redis的SUBSCRIBE命令,感兴趣的朋友跟随... 目录基本语法工作原理示例消息格式相关命令python 示例Redis 的 SUBSCRIBE 命令用于订