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

相关文章

Ubuntu固定虚拟机ip地址的方法教程

《Ubuntu固定虚拟机ip地址的方法教程》本文详细介绍了如何在Ubuntu虚拟机中固定IP地址,包括检查和编辑`/etc/apt/sources.list`文件、更新网络配置文件以及使用Networ... 1、由于虚拟机网络是桥接,所以ip地址会不停地变化,接下来我们就讲述ip如何固定 2、如果apt安

PyCharm 接入 DeepSeek最新完整教程

《PyCharm接入DeepSeek最新完整教程》文章介绍了DeepSeek-V3模型的性能提升以及如何在PyCharm中接入和使用DeepSeek进行代码开发,本文通过图文并茂的形式给大家介绍的... 目录DeepSeek-V3效果演示创建API Key在PyCharm中下载Continue插件配置Con

Deepseek R1模型本地化部署+API接口调用详细教程(释放AI生产力)

《DeepseekR1模型本地化部署+API接口调用详细教程(释放AI生产力)》本文介绍了本地部署DeepSeekR1模型和通过API调用将其集成到VSCode中的过程,作者详细步骤展示了如何下载和... 目录前言一、deepseek R1模型与chatGPT o1系列模型对比二、本地部署步骤1.安装oll

在不同系统间迁移Python程序的方法与教程

《在不同系统间迁移Python程序的方法与教程》本文介绍了几种将Windows上编写的Python程序迁移到Linux服务器上的方法,包括使用虚拟环境和依赖冻结、容器化技术(如Docker)、使用An... 目录使用虚拟环境和依赖冻结1. 创建虚拟环境2. 冻结依赖使用容器化技术(如 docker)1. 创

Spring Boot整合log4j2日志配置的详细教程

《SpringBoot整合log4j2日志配置的详细教程》:本文主要介绍SpringBoot项目中整合Log4j2日志框架的步骤和配置,包括常用日志框架的比较、配置参数介绍、Log4j2配置详解... 目录前言一、常用日志框架二、配置参数介绍1. 日志级别2. 输出形式3. 日志格式3.1 PatternL

MySQL8.2.0安装教程分享

《MySQL8.2.0安装教程分享》这篇文章详细介绍了如何在Windows系统上安装MySQL数据库软件,包括下载、安装、配置和设置环境变量的步骤... 目录mysql的安装图文1.python访问网址2javascript.点击3.进入Downloads向下滑动4.选择Community Server5.

CentOS系统Maven安装教程分享

《CentOS系统Maven安装教程分享》本文介绍了如何在CentOS系统中安装Maven,并提供了一个简单的实际应用案例,安装Maven需要先安装Java和设置环境变量,Maven可以自动管理项目的... 目录准备工作下载并安装Maven常见问题及解决方法实际应用案例总结Maven是一个流行的项目管理工具

本地私有化部署DeepSeek模型的详细教程

《本地私有化部署DeepSeek模型的详细教程》DeepSeek模型是一种强大的语言模型,本地私有化部署可以让用户在自己的环境中安全、高效地使用该模型,避免数据传输到外部带来的安全风险,同时也能根据自... 目录一、引言二、环境准备(一)硬件要求(二)软件要求(三)创建虚拟环境三、安装依赖库四、获取 Dee

MySql9.1.0安装详细教程(最新推荐)

《MySql9.1.0安装详细教程(最新推荐)》MySQL是一个流行的关系型数据库管理系统,支持多线程和多种数据库连接途径,能够处理上千万条记录的大型数据库,本文介绍MySql9.1.0安装详细教程,... 目录mysql介绍:一、下载 Mysql 安装文件二、Mysql 安装教程三、环境配置1.右击此电脑

在idea中使用mysql数据库超详细教程

《在idea中使用mysql数据库超详细教程》:本文主要介绍如何在IntelliJIDEA中连接MySQL数据库,并使用控制台执行SQL语句,还详细讲解了如何使用MyBatisGenerator快... 目录一、连接mysql二、使用mysql三、快速生成实体、接口、sql文件总结一、连接mysql在ID