Calendar's Blog
技术学习
交换链的多重缓冲机制
图像降采样
GPU通用计算
Vulkan compute shader
integral Image计算
模糊阴影视效研究
2D矢量图元绘制技术研究
3D SDF
1.预备知识和基于3D SDF的康奈尔盒子
2.RSM阴影和一次间接光照
3.优化VPL采样
EasyVulkan
Builder类
CommandBuffer
DescriptorSet
RenderPass
ResourceManager中的内存泄露
VMA
Vulkan初始化
Vulkan同步机制
VulkanDebug
Home
noBug
色彩空间问题
std140数据对齐
vector.back()引发的指针错误
Contact
Copyright © 2025 |
Calendar
Home
>
EasyVulkan
> Builder类
Now Loading ...
Builder类
Vulkan渲染通道
引言 在Vulkan图形API的渲染管线中,渲染通道(Render Pass)是构建高效渲染流程的核心组件,它不仅描述了一次渲染操作中的渲染目标(attachments)的使用方式,还决定了多个渲染阶段(subpass)之间的执行顺序和数据依赖关系。本文将深入探讨Vulkan渲染通道的工作原理及其关键要素的实现细节。 渲染通道的本质 渲染通道(VkRenderPass)定义了渲染操作期间使用的帧缓冲附件集合及其使用方式。它通过明确指定附件的生命周期和依赖关系,允许驱动进行深层次优化。相较于传统图形API的隐式状态管理,Vulkan的显式声明机制可降低内存带宽消耗达30%以上。 渲染通道主要负责描述: 渲染目标(Attachments) 的格式、加载/存储操作、采样数等属性; 子通道(Subpasses) 中每个阶段如何使用这些渲染目标; 子通道间的依赖关系(Pipeline Dependencies),用于保证数据正确性和同步。 设计哲学 显式控制:开发者必须明确指定所有附件和子流程 执行优化:提前声明渲染流程使驱动能优化资源布局 依赖管理:精确控制子流程间的内存和执行顺序 Attachment简介与创建方法 Attachment 通常指的是帧缓冲区中的渲染目标,比如颜色缓冲、深度缓冲或模板缓冲。每个attachment都需要在创建渲染通道时进行详细的描述,主要包括以下几个方面: 格式(Format):如 VK_FORMAT_B8G8R8A8_UNORM、VK_FORMAT_D32_SFLOAT 等。 采样数(Samples):多重采样时使用的采样数。 加载/存储操作(Load/Store Operations):如在渲染开始时是清除还是保留已有数据,在渲染结束时是存储还是丢弃数据。 初始与最终布局(InitialLayout/FinalLayout):表明attachment在渲染开始前和结束后的内存布局状态,便于Vulkan内部进行布局转换。 在创建渲染通道时,需要通过一个 VkAttachmentDescription 数组来描述所有的attachment。例如: VkAttachmentDescription colorAttachment = {}; colorAttachment.format = swapchainImageFormat; // 指定Format colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT; // 指定采样数 colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; // 指定加载操作 colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; // 指定存储操作 // For depth/stencil attachments colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; // 指定模板加载操作 colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; // 指定模板存储操作 // End for depth/stencil attachments colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; // 指定初始布局 colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; // 指定最终布局 Subpass简介与创建方法 Subpass 是渲染通道中的一个阶段,每个subpass描述了在该阶段中如何使用和依赖attachment。 在创建渲染通道时,需要使用 VkSubpassDescription 结构体来描述每个subpass。一个subpass通常至少需要描述以下信息: Pipeline Bind Point:通常为 VK_PIPELINE_BIND_POINT_GRAPHICS,指明当前subpass将用于图形管线。 颜色附件引用(Color Attachments):指明渲染阶段中将写入颜色数据的attachment。 输入附件引用(Input Attachments):在一个subpass中可以读取之前subpass生成的数据。 深度/模板附件引用(Depth/Stencil Attachment):如果需要使用深度或模板测试,则需要指定对应的attachment。 如下代码创建了一个简单的subpass: VkAttachmentReference colorAttachmentRef = {}; colorAttachmentRef.attachment = 0; // 引用上面定义的第一个attachment colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; VkSubpassDescription subpass = {}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &colorAttachmentRef; 在这个例子中,我们创建了一个渲染阶段,该阶段会把渲染结果写入第0号attachment,并且要求该attachment处于 VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL 布局状态。 为什么需要subpass? Vulkan采用subpass的设计模式,除了在API层面给出了更明确的渲染流程以外,还带来了以下好处: 利用On-chip memory,减少内存带宽消耗。 在移动端等功耗敏感型设备上,GPU普遍采用了TBR设计,减少对内存带宽的占用,其主要思想是在小块(tile)区域内完成大部分渲染操作,然后统一写回到内存。subpass 非常适合这种架构:在同一个 render pass 内的多个 subpass 可以在同一tile 的生命周期内连续处理(数据传输发生在On-chip memory上),不必频繁地将数据在片上和内存之间来回传输。 避免全局同步。 传统渲染流水线中,可能需要使用全局的内存屏障来确保数据一致性,而在 subpass 内部,由于数据依赖关系已被明确定义,驱动和硬件就可以局部地处理同步,减少不必要的等待。 Subpass Dependency简介与创建方法 在一个渲染通道中,多个subpass之间或subpass与外部操作之间往往存在数据依赖关系。为了确保数据的正确性和避免竞态条件,需要在渲染通道中明确声明这些依赖关系。这就是管线Dependency(Pipeline Dependency)的作用。 管线Dependency 允许开发者在subpass之间定义内存屏障和执行屏障,确保: 某个subpass的写操作完成后,下一个subpass读取数据时能够获得最新的结果; 在执行特定渲染操作前,所有前置的操作已经完成并且内存访问已经同步。 在Vulkan中,这种依赖关系通过 VkSubpassDependency 结构体进行描述。 如何使用管线依赖 假设有两个subpass:subpass0写入颜色数据,而subpass1需要读取这些数据作为输入attachment。在这种场景下,我们需要确保subpass0的写操作在subpass1开始读取之前已经完成。可以通过如下方式定义一个依赖: VkSubpassDependency dependency = {}; dependency.srcSubpass = 0; // 依赖源subpass dependency.dstSubpass = 1; // 依赖目标subpass // 指定依赖的阶段与访问类型 dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dependency.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; dependency.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; dependency.dstAccessMask = VK_ACCESS_INPUT_ATTACHMENT_READ_BIT; // 对于跨subpass依赖,通常设置dependency.flags为0或VK_DEPENDENCY_BY_REGION_BIT dependency.dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT; 在上述例子中: srcSubpass 指定了依赖的来源,即subpass0; dstSubpass 指定了依赖的目标,即subpass1; srcStageMask 和 dstStageMask 指明了涉及的管线阶段; srcAccessMask 和 dstAccessMask 则描述了内存访问的类型。 此外,对于一些特殊情况(如初始状态与最终状态的同步),也可以将 srcSubpass 或 dstSubpass 设置为 VK_SUBPASS_EXTERNAL,以描述与渲染通道外部的依赖关系。 例如,定义一个计算预处理 -> 图形渲染的依赖: VkSubpassDependency compToGraphic = { .srcSubpass = VK_SUBPASS_EXTERNAL, // 表示计算阶段 .dstSubpass = 0, // 图形子流程索引 .srcStageMask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, .dstStageMask = VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT, .dstAccessMask = VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_SHADER_READ_BIT, .dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT }; 又或者定义一个图形渲染 -> 计算后处理的依赖: VkSubpassDependency graphicToComp = { .srcSubpass = 1, // 最后一个图形子流程 .dstSubpass = VK_SUBPASS_EXTERNAL, // 后续计算阶段 .srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, .dstStageMask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, .dstAccessMask = VK_ACCESS_SHADER_READ_BIT, .dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT }; EasyVulkan中的RenderPass 构建一个含有颜色和深度附件的 Renderpass: // 创建一个简单的 Renderpass,其中包含颜色和深度附件 auto renderPass = renderPassBuilder .addColorAttachment(swapchainFormat) // 添加颜色附件 .addDepthStencilAttachment(depthFormat) // 添加深度/模板附件 .beginSubpass() // 开始一个子通道 .addColorReference(0) // 子通道引用第 0 个附件作为颜色附件 .setDepthStencilReference(1) // 子通道引用第 1 个附件作为深度/模板附件 .endSubpass() // 结束子通道 .build("mainRenderPass"); // 构建 Renderpass,并命名为 "mainRenderPass" 对于复杂的渲染流程,经常需要设置多个子通道以及它们之间的依赖关系。以下示例展示了如何配置多个子通道,并在它们之间添加依赖: // 构建一个拥有多个子通道的 Renderpass auto renderPass = renderPassBuilder .addColorAttachment(colorFormat) // 添加颜色附件 .addDepthStencilAttachment(depthFormat) // 添加深度/模板附件 // 第一个子通道配置:渲染到颜色附件和深度附件 .beginSubpass() .addColorReference(0) .setDepthStencilReference(1) .endSubpass() // 第二个子通道配置:使用第一个子通道的颜色输出作为输入附件,同时写入到另一个颜色附件(例如后续处理) .beginSubpass() .addInputReference(0) .addColorReference(2) .endSubpass() // 添加子通道间依赖:确保第一个子通道的写入操作完成后,第二个子通道才能读取 .addDependency(0, 1, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT) .build("multiPassRender"); // 构建 Renderpass
EasyVulkan
· 2025-01-29
Vulkan描述符集
Vulkan描述符集的简化之道:探索EasyVulkan的实现 在使用Vulkan时,需要使用描述符集来管理资源。描述符集是Vulkan中的一种资源管理机制,用于管理资源(如纹理、缓冲区等)的绑定和使用。然而,描述符集的创建和使用需要大量的代码操作,包括创建描述符池、创建layout binding、创建描述符池、创建和更新descriptorSet等。并且,增加新的资源时,也需要修改大量的代码,这无疑增加了开发者的负担。 为了简化这个过程,EasyVulkan提供了DescriptorSetBuilder类,它采用了构建器模式,大大简化了描述符集的创建和管理过程。让我们一起深入了解这个实现。 DescriptorSetBuilder的核心设计 1. 构建器模式的应用 EasyVulkan的DescriptorSetBuilder采用了构建器模式,这使得描述符集的创建过程变得更加流畅和直观。主要体现在: DescriptorSetBuilder builder(device, context); VkDescriptorSet descriptorSet = builder .addBinding(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_VERTEX_BIT) .addBufferDescriptor(0, buffer, 0, bufferSize, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER) .buildWithLayout("myDescriptorSet"); 2. 资源绑定的简化 DescriptorSetBuilder提供了多个直观的方法来添加不同类型的资源,这些方法中封装了原本复杂的Vulkan API调用操作: addBinding: 添加描述符布局绑定 addBufferDescriptor: 添加缓冲区描述符 addImageDescriptor: 添加图像描述符 addStorageImageDescriptor: 添加存储图像描述符 3. 自动化的资源管理 DescriptorSetBuilder还提供了自动的资源管理功能: 自动创建和管理描述符池 自动验证绑定的正确性 自动注册资源到资源管理器 自动处理错误情况 实现细节解析 1. 描述符池的创建 描述符池的创建在build方法中调用createPool方法完成。该方法的实现如下: VkDescriptorPool DescriptorSetBuilder::createPool() const { // 统计每种描述符类型的数量 std::unordered_map<VkDescriptorType, uint32_t> typeCount; for (const auto &binding : m_layoutBindings) { typeCount[binding.descriptorType] += binding.descriptorCount; } // 创建池大小信息 std::vector<VkDescriptorPoolSize> poolSizes; for (const auto &[type, count] : typeCount) { poolSizes.push_back({type, count}); } // ... 创建描述符池 } 2. 绑定验证机制 为了确保描述符集的正确性,DescriptorSetBuilder实现了完善的验证机制: void DescriptorSetBuilder::validateBindings() const { // 检查是否存在绑定 if (m_layoutBindings.empty()) { throw std::runtime_error("No descriptor set bindings specified"); } // 检查重复绑定 std::unordered_map<uint32_t, VkDescriptorType> bindingTypes; for (const auto &binding : m_layoutBindings) { auto [it, inserted] = bindingTypes.insert({binding.binding, binding.descriptorType}); if (!inserted) { throw std::runtime_error("Duplicate binding number in descriptor set layout"); } } // 验证写入描述符与绑定的匹配性 // ... } 3. 资源更新机制 描述符集的更新过程也被简化: void DescriptorSetBuilder::updateDescriptorSet(VkDescriptorSet descriptorSet) const { std::vector<VkWriteDescriptorSet> writes = m_writes; for (auto &write : writes) { write.dstSet = descriptorSet; } vkUpdateDescriptorSets(m_device->getLogicalDevice(), static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr); } EasyVulkan中的DescriptorSet // 创建一个包含uniform buffer和纹理的描述符集 auto descriptorSet = builder // 添加uniform buffer绑定 .addBinding(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_VERTEX_BIT) // 添加纹理绑定 .addBinding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_FRAGMENT_BIT) // 添加uniform buffer描述符 .addBufferDescriptor(0, uniformBuffer, 0, sizeof(UniformBufferObject), VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER) // 添加纹理描述符(Sampler可以通过SamplerBuilder创建) .addImageDescriptor(1, textureImageView, textureSampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER) // 构建描述符集(name用于资源追踪) .buildWithLayout("myMaterialDescriptorSet");
EasyVulkan
· 2025-01-27
Vulkan命令缓冲区
命令池与命令缓冲区 在Vulkan的渲染架构中,命令池(Command Pool)和命令缓冲区(Command Buffer)构成了GPU指令管理的核心机制。 命令池 (Command Pool): 命令池是命令缓冲区的内存分配器和管理器。你可以把它想象成一个命令缓冲区的“工厂”。 每个命令池都与一个特定的队列族索引 (Queue Family Index) 关联。这意味着从该命令池分配的命令缓冲区只能提交到与该队列族索引对应的队列中。 内存分配: 命令池负责分配命令缓冲区所需的内存。Vulkan 允许驱动程序在命令池级别进行内存管理优化,例如预分配内存,从而提高命令缓冲区分配和释放的效率。 生命周期管理: 命令池管理着它所分配的命令缓冲区的生命周期。你可以重置整个命令池,一次性释放所有命令缓冲区,也可以单独重置和重新使用命令缓冲区。 命令缓冲区 (Command Buffer): 命令缓冲区是实际存储 GPU 指令的容器。它记录了一系列图形或计算操作,例如: 渲染指令: 设置渲染状态、绑定描述符集、绑定顶点缓冲区和索引缓冲区、绘制调用等。 计算指令: 分发计算着色器、绑定计算描述符集等。 传输指令: 缓冲区和图像的拷贝、填充、更新等。 同步指令: 设置事件、栅栏、管线屏障等。 可以将命令缓冲区类比为一条“指令流水线”,GPU 会按照命令缓冲区中指令的顺序逐条执行。 特性 命令池 命令缓冲区 生命周期管理 手动创建/销毁 由命令池分配/回收 线程关联性 绑定到特定队列族 继承所属命令池的队列族属性 重置行为 可批量重置所有关联命令缓冲区 支持单独或批量重置 内存管理 控制底层内存分配策略 使用预分配的内存空间 图 1:CommandPool 、CommandBuffer 、QueueFamily 、 Queue 的关系。 回顾在Vulkan初始化中,我们提到命令池创建时需要指定队列族,由该命令池创建的命令缓冲区也只能使用该队列族的队列来执行。 创建和使用 命令池创建 VkCommandPoolCreateInfo poolInfo{}; poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; poolInfo.queueFamilyIndex = queueFamilyIndex; // 指定队列族索引 (例如图形队列族) poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool); 标志位解析: VK_COMMAND_POOL_CREATE_TRANSIENT_BIT: 适用于高频更新的短期命令。提示驱动程序命令缓冲区是短暂的,可能可以进行一些优化,但实际效果取决于驱动程序。 VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT: 允许单独重置命令缓冲区。强烈建议设置此标志位。它允许你单独重置命令池中分配的命令缓冲区,以便重复使用,而无需重新分配。 命令缓冲区分配 VkCommandBufferAllocateInfo allocInfo{}; allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; allocInfo.commandPool = commandPool; allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; // 或者 VK_COMMAND_BUFFER_LEVEL_SECONDARY allocInfo.commandBufferCount = 1; // 分配的命令缓冲区数量 vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer); commandPool: 指定命令缓冲区从哪个命令池分配。 level: 指定命令缓冲区的级别,可以是 VK_COMMAND_BUFFER_LEVEL_PRIMARY 或 VK_COMMAND_BUFFER_LEVEL_SECONDARY。 commandBufferCount: 指定要分配的命令缓冲区数量。可以一次性分配多个命令缓冲区。 开始和结束记录 VkCommandBufferBeginInfo beginInfo{}; beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; // 提示缓冲区将被提交一次并立即重置 vkBeginCommandBuffer(commandBuffer, &beginInfo); // ... 在这里记录你的 Vulkan 指令 (例如 vkCmdBindPipeline, vkCmdDraw 等) ... vkEndCommandBuffer(commandBuffer); beginInfo.flags: 可以设置一些标志位来提示驱动程序命令缓冲区的用途,常用的标志位包括: VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT: 提示缓冲区将被提交一次,然后立即重置或释放。 VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT: 指示后续渲染通道的状态将继承自这个命令缓冲区之前的渲染通道。 VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT: 指示命令缓冲区可以多次提交,直到被显式重置。 提交 记录完成的命令缓冲区需要提交到队列才能被 GPU 执行。 VkSubmitInfo submitInfo{}; submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &commandBuffer; if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) { throw std::runtime_error("failed to submit command buffer!"); } // 可选: 等待队列完成执行 (同步操作) vkQueueWaitIdle(graphicsQueue); submitInfo.pCommandBuffers: 指向要提交的命令缓冲区数组。 vkQueueSubmit: 将命令缓冲区提交到指定的队列 (graphicsQueue 在这里是图形队列)。 vkQueueWaitIdle: 等待队列中的所有命令缓冲区执行完成。通常用于同步操作,例如等待渲染完成才能进行后续操作。 释放和重置命令缓冲区 使用完命令缓冲区后,你可以选择释放或重置它。 释放命令缓冲区: 将命令缓冲区返回给命令池,可以再次分配新的命令缓冲区。 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer); 重置命令缓冲区: 清除命令缓冲区中的所有指令,使其可以重新记录。重置操作比重新分配更高效。 vkResetCommandBuffer(commandBuffer, 0); // 或者 VK_COMMAND_BUFFER_RESET_RELEASE_RESOURCES_BIT VK_COMMAND_BUFFER_RESET_RELEASE_RESOURCES_BIT: 提示驱动程序释放命令缓冲区内部使用的资源,可以节省内存,但可能会降低性能。 销毁命令池 当不再需要命令池时,需要销毁它,释放其占用的资源。 vkDestroyCommandPool(device, commandPool, nullptr); 高级用法-二级缓冲区被主命令缓冲区调用 Vulkan 将命令缓冲区分为两种级别: 主命令缓冲区 (Primary Command Buffer, VK_COMMAND_BUFFER_LEVEL_PRIMARY): 主命令缓冲区可以提交到队列执行,并且可以调用二级命令缓冲区。它通常用于组织应用程序的主要渲染或计算流程。 二级命令缓冲区 (Secondary Command Buffer, VK_COMMAND_BUFFER_LEVEL_SECONDARY): 二级命令缓冲区不能直接提交到队列执行, 必须由主命令缓冲区调用才能被执行。二级命令缓冲区常用于: 组织复杂的渲染流程: 将渲染流程分解成多个逻辑模块,每个模块用一个二级命令缓冲区表示,提高代码可读性和可维护性。 并行命令缓冲区记录: 多个线程可以并行记录二级命令缓冲区,然后由主命令缓冲区按顺序调用,利用多核 CPU 提升命令缓冲区记录效率。 命令复用: 对于一些重复使用的命令序列,可以将其记录到二级命令缓冲区中,然后在多个主命令缓冲区中复用,减少重复记录的工作。 调用二级命令缓冲区 步骤: 创建二级命令缓冲区: 按照之前的方法,创建一个 VK_COMMAND_BUFFER_LEVEL_SECONDARY 级别的命令缓冲区。 记录二级命令缓冲区: 在二级命令缓冲区中记录你希望复用或并行记录的命令序列。 在主命令缓冲区中调用二级命令缓冲区: 在主命令缓冲区的记录过程中,使用 vkCmdExecuteCommands 命令来调用二级命令缓冲区。 // 假设 primaryCmdBuffer 是主命令缓冲区,secondaryCmdBuffer 是二级命令缓冲区 vkBeginCommandBuffer(primaryCmdBuffer, &primaryBeginInfo); // ... 主命令缓冲区中的其他指令 ... // 调用二级命令缓冲区 vkCmdExecuteCommands(primaryCmdBuffer, 1, &secondaryCmdBuffer); // ... 主命令缓冲区中的其他指令 ... vkEndCommandBuffer(primaryCmdBuffer); 二级命令缓冲区的继承 (Inheritance) 当二级命令缓冲区在渲染通道内执行时,需要设置继承信息,例如渲染通道 (Render Pass) 和帧缓冲区 (Framebuffer)。这通过 VkCommandBufferInheritanceInfo 结构体在分配二级命令缓冲区时指定。 VkCommandBufferInheritanceInfo inheritanceInfo{}; inheritanceInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_INFO; inheritanceInfo.renderPass = renderPass; // 继承的渲染通道 inheritanceInfo.framebuffer = framebuffer; // 继承的帧缓冲区 VkCommandBufferAllocateInfo allocInfo{}; allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; allocInfo.commandPool = commandPool; allocInfo.level = VK_COMMAND_BUFFER_LEVEL_SECONDARY; allocInfo.commandBufferCount = 1; allocInfo.pInheritanceInfo = &inheritanceInfo; // 设置继承信息 vkAllocateCommandBuffers(device, &allocInfo, &secondaryCmdBuffer); 当前 Render Pass:确保次级缓冲区的操作与主缓冲区的渲染流程兼容。 当前 Framebuffer:明确操作的目标附件(如颜色/深度附件)。 子通道(Subpass):若次级缓冲区在某个子通道内执行,需指定子通道索引。 Vulkan 会基于这些信息验证次级缓冲区的操作是否合法。如果未正确配置,可能导致验证层错误或运行时崩溃: 如果未正确配置,Vulkan 会抛出以下错误: VUID-VkCommandBufferBeginInfo-flags-00053(Render Pass 未匹配) VUID-vkCmdExecuteCommands-pCommandBuffers-00088(Framebuffer 不兼容) 主命令缓冲会在记录时显式的在VkRenderPassBeginInfo中指定VkRenderPass和VkFramebuffer。 // 主缓冲区记录 Render Pass VkRenderPassBeginInfo renderPassInfo{}; renderPassInfo.renderPass = myRenderPass; // 在此处指定 Render Pass renderPassInfo.framebuffer = myFramebuffer; vkBeginCommandBuffer(primaryCmdBuffer, ...); vkCmdBeginRenderPass(primaryCmdBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE); // 调用次级缓冲区或记录绘制命令 vkCmdEndRenderPass(primaryCmdBuffer); vkEndCommandBuffer(primaryCmdBuffer); 高级用法-条件执行模式 代码逻辑 // 1. 定义可能执行的命令缓冲区(此处为两个候选) VkCommandBuffer conditionalBuffer = ...; // 2. 配置条件渲染信息 VkConditionalRenderingBeginInfoEXT condInfo{}; condInfo.sType = VK_STRUCTURE_TYPE_CONDITIONAL_RENDERING_BEGIN_INFO_EXT; condInfo.buffer = conditionBuffer; // 存储条件值的缓冲区 condInfo.offset = 0; // 条件值在缓冲区中的偏移量 // 3. 开启条件渲染范围 vkCmdBeginConditionalRenderingEXT(primaryBuffer, &condInfo); // 4. 在条件范围内执行命令 vkCmdExecuteCommands(primaryBuffer, 1, conditionalBuffer); // 5. 结束条件渲染范围 vkCmdEndConditionalRenderingEXT(primaryBuffer); 解析 条件值的判定规则 GPU 会从 conditionBuffer 的指定 offset 处读取一个 32位无符号整数值。 判定逻辑: 若值 ≠ 0 → 执行条件范围内的命令 若值 = 0 → 跳过所有条件范围内的命令 执行范围的作用域 条件渲染的影响范围严格限定在 vkCmdBeginConditionalRenderingEXT 和 vkCmdEndConditionalRenderingEXT 之间的命令。 嵌套支持:Vulkan 允许条件渲染的嵌套使用,内层条件可以覆盖外层条件。 次级命令缓冲区的特殊性 示例中通过 vkCmdExecuteCommands 调用的次级命令缓冲区会整体受条件值控制。 若条件不满足,次级缓冲区的所有命令将被跳过,如同未被调用。 场景 动态遮挡剔除(Occlusion Culling) // 步骤: // 1. 第一帧:执行遮挡查询,将结果写入 conditionBuffer // 2. 后续帧:根据查询结果决定是否绘制物体 vkCmdBeginConditionalRenderingEXT(cmdBuffer, &condInfo); vkCmdDrawIndexed(cmdBuffer, ...); // 仅当物体可见时执行绘制 vkCmdEndConditionalRenderingEXT(cmdBuffer); 多方案动态切换 uint32_t conditionValue = useTechniqueA ? 1 : 0; CopyDataToBuffer(conditionBuffer, &conditionValue); // 更新条件值 vkCmdBeginConditionalRenderingEXT(cmdBuffer, &condInfo); if (useTechniqueA) { vkCmdExecuteCommands(cmdBuffer, 1, &techACmdBuffer); } else { vkCmdExecuteCommands(cmdBuffer, 1, &techBCmdBuffer); } vkCmdEndConditionalRenderingEXT(cmdBuffer); GPU-Driven 渲染决策 // 通过计算着色器生成条件值 vkCmdDispatch(computeCmdBuffer, ...); // 在渲染流程中根据计算结果决策 vkCmdBeginConditionalRenderingEXT(renderCmdBuffer, &condInfo); vkCmdDraw(renderCmdBuffer, ...); // 由 GPU 计算的结果控制是否绘制 vkCmdEndConditionalRenderingEXT(renderCmdBuffer); 何时使用二级命令缓冲区? 1. 复杂场景分解 将复杂的渲染流程分解成多个二级命令缓冲区,例如将不同的物体或渲染阶段分别用不同的二级命令缓冲区表示,可以提高代码组织性。 2. 并行记录 如果你的应用程序有复杂的场景,命令缓冲区记录成为瓶颈,可以考虑使用多线程并行记录二级命令缓冲区,然后在一个主命令缓冲区中按顺序调用这些二级命令缓冲区。这可以有效利用多核 CPU 的性能。 3. 命令复用 对于重复使用的渲染或计算序列,将其记录到二级命令缓冲区中,在多个主命令缓冲区中复用,可以减少重复记录的工作量。 打包提交CommandBuffer 在Vulkan中,提交多个不同的command buffer到同一个队列与使用单个command buffer相比,性能差异主要受以下因素影响: 1. CPU开销 多次提交多个command buffer: 若每次提交均调用vkQueueSubmit(尤其是分散的多次调用),会增加CPU负担。驱动需要为每次提交处理验证、同步资源及命令传输,频繁的小批次提交可能导致CPU成为瓶颈。 单次提交单个command buffer: 减少vkQueueSubmit调用次数可降低CPU开销。驱动优化空间更大,可能合并内部操作,提升效率。 2. GPU执行效率 状态切换与批处理: 多个command buffer可能导致频繁的状态切换(如管线绑定、资源更新)。若这些command buffer未优化,GPU可能在执行时产生空闲。而单个command buffer可通过连续记录减少状态切换,提升吞吐量。 提交批次的影响: GPU通常以提交批次为单位调度任务。多次提交可能分割任务,导致GPU无法充分并行;而单次提交(或一次提交多个command buffer)可能形成更大的批次,利于硬件优化。 3. 同步与依赖 显式同步需求: 多次提交常需依赖信号量或栅栏确保执行顺序,可能引入GPU等待。单次提交内部命令天然有序,减少同步需求,降低延迟。 4. 驱动与硬件的优化 驱动处理差异: 部分驱动可能优化多command buffer的合并执行(尤其在单次vkQueueSubmit提交多个时),性能接近单个command buffer。但多次分散提交可能无法享受此类优化。 硬件特性: 某些GPU架构更擅长处理大命令流,而小批次可能导致调度开销。 实践建议 优先减少提交次数:通过单次vkQueueSubmit提交多个command buffer(而非多次调用),可平衡CPU/GPU效率,接近单一大command buffer的性能。 合并录制需权衡:若多个command buffer内容固定且需重用,分开录制可能更灵活;若内容动态变化,合并录制可能减少状态切换,但需评估CPU录制开销。 场景依赖:对实时渲染等高吞吐场景,倾向于减少提交次数与状态切换;对复杂依赖或并行录制需求,可接受适度性能损失以换取灵活性。 EasyVulkan中的CommandBuffer 在EasyVulkan中,使用CommandBufferBuilder来创建和记录命令缓冲区,在注册到ResourceManager中时,会绑定对应的CommandPool。即name-> (CommandBuffer,CommandPool) 例如创建单个CommandBuffer: // 假设 graphicsPool 已经正确创建并初始化 auto cmdBuffer = commandBufferBuilder ->setCommandPool(graphicsPool) ->setLevel(VK_COMMAND_BUFFER_LEVEL_PRIMARY) ->build("mainCommandBuffer"); 创建多个CommandBuffer: // swapchainImageCount 为交换链图像数量 auto cmdBuffers = commandBufferBuilder ->setCommandPool(graphicsPool) ->setCount(swapchainImageCount) ->buildMultiple({"frame0", "frame1", "frame2"}); 创建多个二级CommandBuffer: // 假设 threadCount 是线程数量 auto secondaryCmdBuffers = commandBufferBuilder ->setCommandPool(graphicsPool) ->setLevel(VK_COMMAND_BUFFER_LEVEL_SECONDARY) ->setCount(threadCount) ->buildMultiple();
EasyVulkan
· 2025-01-27
<
>
Touch background to close