Vulkan学习笔记5—帧缓冲和命令缓冲

一、什么是帧缓冲?

帧缓冲(Framebuffer)是图形渲染管线的最终输出目标,用于存储渲染后的图像数据。它本质上是一个由颜色附件、深度附件、模板附件等组成的集合,这些附件对应 GPU 内存中的特定区域,用于记录像素的颜色值、深度值等信息。在渲染过程中,GPU 将图元处理结果写入帧缓冲,最终通过显示设备输出图像。

Vulkan 与 OpenGL 的帧缓冲差异对比

1. 资源管理方式
VulkanOpenGL
默认帧缓冲无默认帧缓冲,必须手动创建所有帧缓冲存在默认帧缓冲(由系统自动创建)
附件绑定帧缓冲需显式绑定到渲染通道(Render Pass),附件通过 “图片视图(ImageView)” 关联帧缓冲可直接附加颜色缓冲、深度缓冲等对象
资源生命周期开发者需显式管理帧缓冲的创建、销毁和同步系统隐式管理部分资源(如默认帧缓冲)

2. 渲染通道与帧缓冲的关系

Vulkan:
帧缓冲必须与渲染通道严格绑定,渲染通道定义了附件的使用方式(如输入、输出、解析操作)。每个帧缓冲的附件类型、数量和布局需与渲染通道完全匹配,例如颜色附件必须指定存储格式(如 RGBA8)和加载 / 存储行为。
示例:创建帧缓冲前需先定义渲染通道,再通过vkCreateFramebuffer将图片视图与渲染通道关联。
OpenGL:
没有 “渲染通道” 的概念,帧缓冲的附件绑定更灵活。开发者可直接通过glFramebufferTexture2D等接口将纹理或渲染缓冲对象(RBO)附加到帧缓冲,附件的使用逻辑由管线状态隐式控制。

3. 附件抽象与组织形式

Vulkan:
附件通过图片(Image) 和图片视图抽象:
图片:底层 GPU 内存对象,存储原始像素数据。
图片视图:定义图片的访问方式(如格式、子资源范围),帧缓冲通过图片视图引用附件。
支持更复杂的附件布局转换(如从 “着色器读” 到 “呈现” 状态的显式转换)。
OpenGL:
附件直接由纹理对象(Texture) 或渲染缓冲对象(RBO) 充当,例如:
颜色附件可附加纹理(用于离屏渲染)或 RBO(用于存储临时数据)。
附件布局转换由管线自动处理,开发者无需显式管理。

4. 多帧缓冲与同步机制 

Vulkan:
为支持多帧渲染(如三重缓冲),需创建多个帧缓冲,并通过同步原语(如信号量、栅栏) 管理帧缓冲的使用顺序,避免资源竞争。
OpenGL:
多帧缓冲管理相对简单,默认帧缓冲支持直接交换链操作(如glXSwapBuffers),同步逻辑由驱动隐式处理。

二、命令缓冲

1. 命令缓冲区概述
  • 间接执行模型:Vulkan 中的命令(如绘制、内存传输)不直接通过函数调用执行,而是先记录到命令缓冲区对象中
  • 性能优势:批量提交命令允许 Vulkan 更高效地优化和调度执行
  • 多线程支持:不同线程可以并行记录命令到不同的命令缓冲区
2. 命令池
  • 作用:管理命令缓冲区的内存分配
  • 创建流程
    • 需要指定队列族(如图形队列族)
    • 支持两种标志:
      • VK_COMMAND_POOL_CREATE_TRANSIENT_BIT:频繁重新记录
      • VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT:允许单独重置命令缓冲区
  • 生命周期:整个应用程序生命周期中通常只创建一次,在程序结束时销毁
3. 命令缓冲区分配
  • 类型
    • 主命令缓冲区(PRIMARY):可直接提交到队列执行
    • 辅助命令缓冲区(SECONDARY):只能从主命令缓冲区调用
  • 分配方式:从命令池中分配,通过 vkAllocateCommandBuffers 函数
4. 命令缓冲区记录流程
  1. 开始记录:调用 vkBeginCommandBuffer
    • 可选标志:ONE_TIME_SUBMITRENDER_PASS_CONTINUESIMULTANEOUS_USE
  2. 启动渲染通道
    • 指定渲染通道对象和帧缓冲
    • 设置渲染区域和清除值
  3. 绑定图形管线:指定使用的图形管线
  4. 设置动态状态
    • 视口(Viewport):定义渲染区域的坐标映射
    • 裁剪矩形(Scissor):定义实际渲染的像素区域
  5. 执行绘制命令vkCmdDraw 函数
    • 参数:顶点数、实例数、顶点偏移、实例偏移
  6. 结束渲染通道vkCmdEndRenderPass
  7. 完成记录vkEndCommandBuffer
5. 关键函数总结
  • vkCreateCommandPool:创建命令池
  • vkAllocateCommandBuffers:从命令池分配命令缓冲区
  • vkBeginCommandBuffer:开始记录命令
  • vkCmdBeginRenderPass:开始渲染通道
  • vkCmdBindPipeline:绑定图形管线
  • vkCmdSetViewport/vkCmdSetScissor:设置动态状态
  • vkCmdDraw:执行基本绘制
  • vkCmdEndRenderPass:结束渲染通道
  • vkEndCommandBuffer:完成命令记录
6. 性能优化建议
  • 使用辅助命令缓冲区重用常见渲染操作
  • 对一次性使用的命令缓冲区设置 ONE_TIME_SUBMIT 标志
  • 确保渲染区域与视口匹配以获得最佳性能
  • 批量提交相关命令以减少驱动程序开销

三、新增成员变量和成员函数

新增成员变量

std::vector<VkFramebuffer> swapChainFramebuffers;
VkCommandPool commandPool;
VkCommandBuffer commandBuffer;

新增成员函数

void HelloTriangle::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;
        
        // 设置图层数 - 对于2D渲染通常为1
        // 对于立体渲染或数组纹理,可能需要更多图层
        framebufferInfo.layers = 1;

        // 创建帧缓冲对象
        // 注意:Vulkan 要求帧缓冲的所有附件格式必须与渲染通道中声明的格式兼容
        if (vkCreateFramebuffer(device, &framebufferInfo, nullptr, &swapChainFramebuffers[i]) != VK_SUCCESS) {
            throw std::runtime_error("Failed to create framebuffer!");
        }
    }
}

// 创建命令池函数
void HelloTriangle::createCommandPool() {
    // 初始化命令池创建信息结构体
    VkCommandPoolCreateInfo poolInfo{};
    poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; // 指定结构体类型
    // 设置命令池标志:允许单独重置命令缓冲区(每帧重新记录需求)
    poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
    // 指定命令池关联的队列族索引(使用之前获取的图形队列族)
    poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value();

    // 调用Vulkan API创建命令池,失败时抛出运行时错误
    if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) {
        throw std::runtime_error("Failed to create command pool!");
    }
}

// 分配命令缓冲区函数
void HelloTriangle::createCommandBuffer() {
    // 初始化命令缓冲区分配信息结构体
    VkCommandBufferAllocateInfo allocInfo{};
    allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; // 指定结构体类型
    allocInfo.commandPool = commandPool; // 指定从哪个命令池分配
    // 设置命令缓冲区级别:主命令缓冲区(可直接提交到队列执行)
    allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
    allocInfo.commandBufferCount = 1; // 分配1个命令缓冲区

    // 调用Vulkan API分配命令缓冲区,失败时抛出运行时错误
    if (vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer) != VK_SUCCESS) {
        throw std::runtime_error("Failed to allocate command buffers!");
    }
}

// 记录命令缓冲区函数(参数:目标命令缓冲区、当前交换链图像索引)
void HelloTriangle::recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex) {
    // 初始化命令缓冲区开始信息结构体
    VkCommandBufferBeginInfo beginInfo{};
    beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; // 指定结构体类型

    // 开始记录命令缓冲区,失败时抛出运行时错误
    if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) {
        throw std::runtime_error("Failed to begin recording command buffer!");
    }

    // 初始化渲染通道开始信息结构体
    VkRenderPassBeginInfo renderPassInfo{};
    renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; // 指定结构体类型
    renderPassInfo.renderPass = renderPass; // 指定使用的渲染通道对象
    // 指定当前交换链图像对应的帧缓冲(通过图像索引获取)
    renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex];
    // 设置渲染区域偏移量(从左上角开始)
    renderPassInfo.renderArea.offset = {0, 0};
    // 设置渲染区域大小(与交换链尺寸一致)
    renderPassInfo.renderArea.extent = swapChainExtent;

    // 设置清除颜色(黑色,不透明度100%)
    VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}};
    renderPassInfo.clearValueCount = 1; // 清除值数量
    renderPassInfo.pClearValues = &clearColor; // 指向清除值指针

    // 开始渲染通道(使用内联命令模式,不使用辅助命令缓冲区)
    vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
    // 绑定图形管线(指定图形管线类型和具体管线对象)
    vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);

    // 初始化视口结构体(定义坐标到屏幕的映射关系)
    VkViewport viewport{};
    viewport.x = 0.0f; viewport.y = 0.0f; // 视口左上角坐标
    // 视口宽度和高度(转换为浮点数,与交换链尺寸一致)
    viewport.width = (float)swapChainExtent.width;
    viewport.height = (float)swapChainExtent.height;
    viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; // 深度范围
    // 设置视口(应用到命令缓冲区,索引0,数量1)
    vkCmdSetViewport(commandBuffer, 0, 1, &viewport);

    // 初始化裁剪矩形结构体(定义实际渲染的像素区域)
    VkRect2D scissor{};
    scissor.offset = {0, 0}; // 裁剪区域左上角偏移
    scissor.extent = swapChainExtent; // 裁剪区域大小
    // 设置裁剪矩形(应用到命令缓冲区,索引0,数量1)
    vkCmdSetScissor(commandBuffer, 0, 1, &scissor);            
    // 执行绘制命令(绘制3个顶点,1个实例,无偏移)
    vkCmdDraw(commandBuffer, 3, 1, 0, 0);
    // 结束当前渲染通道
    vkCmdEndRenderPass(commandBuffer);
    // 完成命令缓冲区记录,失败时抛出运行时错误
    if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {
        throw std::runtime_error("Failed to record command buffer!");
    }
}

销毁资源

void HelloTriangle::cleanup() {
    vkDestroyCommandPool(device, commandPool, nullptr);
    for (auto framebuffer : swapChainFramebuffers) {
        vkDestroyFramebuffer(device, framebuffer, nullptr);
    }
    vkDestroyPipeline(device, graphicsPipeline, nullptr);
    ...
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值