教程 13 - Vulkan 渲染通道(Renderpass)

上一篇:Vulkan 逻辑设备和交换链 | 下一篇:待定 | 返回目录


📚 快速导航

目录 (点击展开/折叠)

🎯 本章目标

通过本教程,你将学会:

🎯 目标📝 描述✅ 成果
Renderpass 概念理解 Vulkan 渲染通道的作用和意义掌握 Renderpass 在图形管线中的角色
Attachment 系统学习颜色、深度附件的配置方法配置完整的附件描述和引用
Subpass 机制了解子通道及其依赖关系正确设置 Subpass 依赖和同步
Image Layout掌握图像布局转换的时机和方法实现高效的布局转换
代码实现编写 Renderpass 创建和管理代码完成 vulkan_renderpass.c 模块

📖 教程概述

🔍 我们要做什么?

教程 12 中,我们创建了逻辑设备和交换链,拥有了渲染目标。现在我们要创建 Vulkan Renderpass(渲染通道)

graph LR
    A[📋 Renderpass] --> B[🌈 颜色附件]
    A --> C[🔍 深度附件]
    A --> D[🔗 Subpass]
    A --> E[🔄 依赖关系]

    B --> F[🎨 渲染输出]
    C --> F
    D --> F
    E --> F

    F --> G[🖼️ 最终图像]

    style A fill:#e3f2fd
    style B fill:#fff3e0
    style C fill:#fff3e0
    style D fill:#e8f5e9
    style E fill:#fce4ec
    style G fill:#c8e6c9

核心任务

  1. ✅ 定义 Renderpass 数据结构
  2. ✅ 配置颜色和深度附件
  3. ✅ 创建 Subpass 描述
  4. ✅ 设置 Subpass 依赖关系
  5. ✅ 实现 Renderpass 开始/结束逻辑

❓ 为什么这样做?

🎮 Renderpass 的核心价值
原因说明
🎯 优化驱动提前告知 GPU 渲染流程,驱动可以优化内存布局和操作
🔄 自动布局转换Vulkan 自动处理图像布局转换,避免手动同步
🚀 TBR 优化移动 GPU 的 Tile-Based Rendering 可以极大优化
📊 多通道渲染支持多个 Subpass,实现延迟渲染等高级技术
💾 内存优化驱动知道哪些附件可以保留在 tile memory
🔥 为什么需要 Attachment?
╔══════════════════════════════════════════════════════════╗
║  🎯 Attachment(附件)的角色                              ║
╠══════════════════════════════════════════════════════════╣
║  🌈 颜色附件      │  存储渲染的颜色数据                    ║
║  🔍 深度附件      │  存储深度信息,实现 3D 深度测试         ║
║  🎭 模板附件      │  存储模板值,实现特殊效果              ║
║  🔗 输入附件      │  从前一个 Subpass 读取数据             ║
║  📊 Resolve 附件  │  MSAA 多重采样解析目标                 ║
╚══════════════════════════════════════════════════════════╝

🛠️ 如何实现?

我们的实现路径:

┌─────────────────────────────────────────────────────────┐
│  第一步:定义数据结构                                     │
│  ├─ vulkan_renderpass 结构体                             │
│  ├─ vulkan_render_pass_state 枚举                        │
│  └─ vulkan_command_buffer_state 枚举                     │
├─────────────────────────────────────────────────────────┤
│  第二步:实现创建函数                                     │
│  ├─ 配置颜色附件描述                                     │
│  ├─ 配置深度附件描述                                     │
│  ├─ 设置 Subpass 和依赖                                  │
│  └─ 调用 vkCreateRenderPass                              │
├─────────────────────────────────────────────────────────┤
│  第三步:实现控制函数                                     │
│  ├─ vulkan_renderpass_begin (开始渲染)                   │
│  ├─ vulkan_renderpass_end (结束渲染)                     │
│  └─ vulkan_renderpass_destroy (销毁资源)                 │
├─────────────────────────────────────────────────────────┤
│  第四步:集成到后端                                       │
│  ├─ 在 vulkan_backend_initialize 中创建                  │
│  └─ 在 vulkan_backend_shutdown 中销毁                    │
└─────────────────────────────────────────────────────────┘

🏗️ Renderpass 核心概念

📚 什么是 Renderpass?

Renderpass(渲染通道) 是 Vulkan 中描述渲染操作的核心对象:

// Renderpass 定义了:
// 1. 需要哪些附件(颜色、深度等)
// 2. 附件的格式和用途
// 3. 如何加载/存储附件数据
// 4. 渲染分为几个子通道(Subpass)
// 5. 子通道之间的依赖关系

与传统 API 的对比

🎨 OpenGL🌋 Vulkan
// OpenGL:隐式渲染通道
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glClear(GL_COLOR_BUFFER_BIT |
        GL_DEPTH_BUFFER_BIT);

// 渲染命令...
drawCalls();

// 自动完成

特点

  • ❌ 驱动不知道整体流程
  • ❌ 无法预先优化
  • ✅ 简单易用
// Vulkan:显式渲染通道
vkCmdBeginRenderPass(cmd, &beginInfo, ...);

// 渲染命令...
vkCmdDraw(...);

vkCmdEndRenderPass(cmd);

特点

  • ✅ 驱动预知渲染流程
  • ✅ 可以优化内存布局
  • ✅ TBR GPU 性能提升巨大
  • ⚠️ 需要显式管理

🔄 Renderpass 生命周期

vkCreateRenderPass
vkCmdBeginRenderPass
开始记录绘制命令
vkCmdEndRenderPass
提交命令缓冲
GPU 执行完成
vkDestroyRenderPass
READY
RECORDING
IN_RENDER_PASS
RECORDING_ENDED
SUBMITTED

🎯 Attachment 附件系统

Attachment 是 Renderpass 操作的图像资源:

typedef struct VkAttachmentDescription {
    VkAttachmentDescriptionFlags flags;
    VkFormat format;                    // 图像格式(如 BGRA8)
    VkSampleCountFlagBits samples;      // 采样数(MSAA)
    VkAttachmentLoadOp loadOp;          // 加载操作(CLEAR/LOAD/DONT_CARE)
    VkAttachmentStoreOp storeOp;        // 存储操作(STORE/DONT_CARE)
    VkAttachmentLoadOp stencilLoadOp;   // 模板加载
    VkAttachmentStoreOp stencilStoreOp; // 模板存储
    VkImageLayout initialLayout;        // 渲染前布局
    VkImageLayout finalLayout;          // 渲染后布局
} VkAttachmentDescription;

Load/Store 操作对比

操作LOADCLEARDONT_CARESTOREDONT_CARE (存储)
含义保留之前的内容清空为指定值不关心(可能是垃圾)保存结果不需要保存
性能慢(需要读取)中等快(跳过读取)慢(需要写回)快(跳过写回)
使用场景增量渲染每帧清屏首次使用需要结果临时缓冲

🔗 Subpass 子通道

Subpass 是 Renderpass 内的一个渲染阶段:

typedef struct VkSubpassDescription {
    VkPipelineBindPoint pipelineBindPoint;      // GRAPHICS/COMPUTE
    uint32_t inputAttachmentCount;              // 输入附件数量
    const VkAttachmentReference* pInputAttachments;
    uint32_t colorAttachmentCount;              // 颜色附件数量
    const VkAttachmentReference* pColorAttachments;
    const VkAttachmentReference* pResolveAttachments;
    const VkAttachmentReference* pDepthStencilAttachment;
    uint32_t preserveAttachmentCount;           // 保留附件
    const uint32_t* pPreserveAttachments;
} VkSubpassDescription;

多 Subpass 的应用场景

graph TD
    A[🎬 Subpass 0: G-Buffer 生成] --> B[📊 位置、法线、颜色]
    B --> C[🎬 Subpass 1: 光照计算]
    C --> D[💡 读取 G-Buffer]
    D --> E[🌟 最终光照结果]

    style A fill:#e3f2fd
    style C fill:#fff3e0
    style E fill:#c8e6c9

📁 项目结构分析

本次提交新增了 Renderpass 模块:

engine/src/renderer/vulkan/
├── vulkan_backend.c          # 🔄 修改:集成 Renderpass
├── vulkan_renderpass.c       # ✨ 新增:Renderpass 实现
├── vulkan_renderpass.h       # ✨ 新增:Renderpass 接口
└── vulkan_types.inl          # 🔄 修改:新增类型定义

文件依赖关系

vulkan_backend.c
vulkan_renderpass.h
vulkan_renderpass.c
vulkan_types.inl
core/kmemory.h

🔍 核心文件详解

🎨 vulkan_renderpass.h - 接口定义

📄 完整源码
#pragma once

#include "vulkan_types.inl"

void vulkan_renderpass_create(
    vulkan_context* context,
    vulkan_renderpass* out_renderpass,
    f32 x, f32 y, f32 w, f32 h,
    f32 r, f32 g, f32 b, f32 a,
    f32 depth,
    u32 stencil);

void vulkan_renderpass_destroy(vulkan_context* context, vulkan_renderpass* renderpass);

void vulkan_renderpass_begin(
    vulkan_command_buffer* command_buffer,
    vulkan_renderpass* renderpass,
    VkFramebuffer frame_buffer);

void vulkan_renderpass_end(vulkan_command_buffer* command_buffer, vulkan_renderpass* renderpass);
🔬 接口分析
函数参数作用
vulkan_renderpass_create位置、大小、清除颜色、深度、模板创建 Renderpass 对象
vulkan_renderpass_destroycontext, renderpass销毁 Renderpass
vulkan_renderpass_begin命令缓冲、Renderpass、Framebuffer开始渲染
vulkan_renderpass_end命令缓冲、Renderpass结束渲染

设计亮点

  • ✅ 清除值在创建时指定,避免每帧传递
  • ✅ 使用 out_renderpass 模式,清晰的输出语义
  • ✅ Begin/End 对称设计,易于理解

🎨 vulkan_renderpass.c - 实现详解

1️⃣ 创建 Renderpass
📄 vulkan_renderpass_create 函数
void vulkan_renderpass_create(
    vulkan_context* context,
    vulkan_renderpass* out_renderpass,
    f32 x, f32 y, f32 w, f32 h,
    f32 r, f32 g, f32 b, f32 a,
    f32 depth,
    u32 stencil) {

    // 1. 配置 Subpass
    VkSubpassDescription subpass = {};
    subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;

    // 2. 配置 Attachments
    u32 attachment_description_count = 2;
    VkAttachmentDescription attachment_descriptions[attachment_description_count];

    // 3. 颜色附件配置
    VkAttachmentDescription color_attachment;
    color_attachment.format = context->swapchain.image_format.format;
    color_attachment.samples = VK_SAMPLE_COUNT_1_BIT;
    color_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
    color_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
    color_attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
    color_attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
    color_attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    color_attachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
    color_attachment.flags = 0;

    attachment_descriptions[0] = color_attachment;

    // 4. 颜色附件引用
    VkAttachmentReference color_attachment_reference;
    color_attachment_reference.attachment = 0;  // 索引到 attachment_descriptions[0]
    color_attachment_reference.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

    subpass.colorAttachmentCount = 1;
    subpass.pColorAttachments = &color_attachment_reference;

    // 5. 深度附件配置
    VkAttachmentDescription depth_attachment = {};
    depth_attachment.format = context->device.depth_format;
    depth_attachment.samples = VK_SAMPLE_COUNT_1_BIT;
    depth_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
    depth_attachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
    depth_attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
    depth_attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
    depth_attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    depth_attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;

    attachment_descriptions[1] = depth_attachment;

    // 6. 深度附件引用
    VkAttachmentReference depth_attachment_reference;
    depth_attachment_reference.attachment = 1;
    depth_attachment_reference.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;

    subpass.pDepthStencilAttachment = &depth_attachment_reference;

    // 7. 其他 Subpass 配置
    subpass.inputAttachmentCount = 0;
    subpass.pInputAttachments = 0;
    subpass.pResolveAttachments = 0;
    subpass.preserveAttachmentCount = 0;
    subpass.pPreserveAttachments = 0;

    // 8. Subpass 依赖
    VkSubpassDependency dependency;
    dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
    dependency.dstSubpass = 0;
    dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    dependency.srcAccessMask = 0;
    dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
    dependency.dependencyFlags = 0;

    // 9. 创建 Renderpass
    VkRenderPassCreateInfo render_pass_create_info = {VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
    render_pass_create_info.attachmentCount = attachment_description_count;
    render_pass_create_info.pAttachments = attachment_descriptions;
    render_pass_create_info.subpassCount = 1;
    render_pass_create_info.pSubpasses = &subpass;
    render_pass_create_info.dependencyCount = 1;
    render_pass_create_info.pDependencies = &dependency;
    render_pass_create_info.pNext = 0;
    render_pass_create_info.flags = 0;

    VK_CHECK(vkCreateRenderPass(
        context->device.logical_device,
        &render_pass_create_info,
        context->allocator,
        &out_renderpass->handle));
}
🔬 逐步解析

步骤 1-2:准备工作

VkSubpassDescription subpass = {};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
  • 创建 Subpass 描述
  • GRAPHICS 表示用于图形渲染(对应还有 COMPUTE

步骤 3:颜色附件配置

字段含义
formatswapchain 格式与交换链图像格式一致(通常是 BGRA8)
samplesVK_SAMPLE_COUNT_1_BIT无多重采样(1 个样本)
loadOpCLEAR渲染前清空附件
storeOpSTORE渲染后保存结果(需要显示)
initialLayoutUNDEFINED不关心之前的内容(性能优化)
finalLayoutPRESENT_SRC_KHR渲染后用于呈现

步骤 5:深度附件配置

depth_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
depth_attachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;

关键设计

  • storeOp = DONT_CARE:深度缓冲不需要保存(仅当前帧使用)
  • ✅ 性能优化:TBR GPU 可以保留深度在 tile memory,无需写回

步骤 8:Subpass 依赖

dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;

解释

  • VK_SUBPASS_EXTERNAL → Subpass 0:外部到第一个 Subpass
  • 确保在开始渲染前,颜色附件已准备好

2️⃣ 开始 Renderpass
📄 vulkan_renderpass_begin 函数
void vulkan_renderpass_begin(
    vulkan_command_buffer* command_buffer,
    vulkan_renderpass* renderpass,
    VkFramebuffer frame_buffer) {

    VkRenderPassBeginInfo begin_info = {VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
    begin_info.renderPass = renderpass->handle;
    begin_info.framebuffer = frame_buffer;
    begin_info.renderArea.offset.x = renderpass->x;
    begin_info.renderArea.offset.y = renderpass->y;
    begin_info.renderArea.extent.width = renderpass->w;
    begin_info.renderArea.extent.height = renderpass->h;

    VkClearValue clear_values[2];
    kzero_memory(clear_values, sizeof(VkClearValue) * 2);
    clear_values[0].color.float32[0] = renderpass->r;
    clear_values[0].color.float32[1] = renderpass->g;
    clear_values[0].color.float32[2] = renderpass->b;
    clear_values[0].color.float32[3] = renderpass->a;
    clear_values[1].depthStencil.depth = renderpass->depth;
    clear_values[1].depthStencil.stencil = renderpass->stencil;

    begin_info.clearValueCount = 2;
    begin_info.pClearValues = clear_values;

    vkCmdBeginRenderPass(command_buffer->handle, &begin_info, VK_SUBPASS_CONTENTS_INLINE);
    command_buffer->state = COMMAND_BUFFER_STATE_IN_RENDER_PASS;
}

关键点

  1. Render Area:指定渲染区域(通常是整个 framebuffer)
  2. Clear Values:清除值数组,索引对应 attachment
    • clear_values[0] → 颜色附件(索引 0)
    • clear_values[1] → 深度附件(索引 1)
  3. INLINE:绘制命令直接记录在主命令缓冲(对应 SECONDARY_COMMAND_BUFFERS

3️⃣ 结束 Renderpass
void vulkan_renderpass_end(vulkan_command_buffer* command_buffer, vulkan_renderpass* renderpass) {
    vkCmdEndRenderPass(command_buffer->handle);
    command_buffer->state = COMMAND_BUFFER_STATE_RECORDING;
}

简洁明了

  • 调用 vkCmdEndRenderPass
  • 更新命令缓冲状态为 RECORDING

4️⃣ 销毁 Renderpass
void vulkan_renderpass_destroy(vulkan_context* context, vulkan_renderpass* renderpass) {
    if (renderpass && renderpass->handle) {
        vkDestroyRenderPass(context->device.logical_device, renderpass->handle, context->allocator);
        renderpass->handle = 0;
    }
}

防御性编程

  • 检查指针有效性
  • 销毁后置零,避免重复释放

🔄 vulkan_types.inl - 类型扩展

📄 新增类型定义
// Renderpass 状态枚举
typedef enum vulkan_render_pass_state {
    READY,
    RECORDING,
    IN_RENDER_PASS,
    RECORDING_ENDED,
    SUBMITTED,
    NOT_ALLOCATED
} vulkan_render_pass_state;

// Renderpass 结构体
typedef struct vulkan_renderpass {
    VkRenderPass handle;
    f32 x, y, w, h;       // 渲染区域
    f32 r, g, b, a;       // 清除颜色

    f32 depth;            // 清除深度值
    u32 stencil;          // 清除模板值

    vulkan_render_pass_state state;
} vulkan_renderpass;

// 命令缓冲状态枚举
typedef enum vulkan_command_buffer_state {
    COMMAND_BUFFER_STATE_READY,
    COMMAND_BUFFER_STATE_RECORDING,
    COMMAND_BUFFER_STATE_IN_RENDER_PASS,
    COMMAND_BUFFER_STATE_RECORDING_ENDED,
    COMMAND_BUFFER_STATE_SUBMITTED,
    COMMAND_BUFFER_STATE_NOT_ALLOCATED
} vulkan_command_buffer_state;

// 命令缓冲结构体
typedef struct vulkan_command_buffer {
    VkCommandBuffer handle;
    vulkan_command_buffer_state state;
} vulkan_command_buffer;

设计分析

结构体字段用途
vulkan_renderpasshandleVulkan 对象句柄
x, y, w, h渲染区域(通常是 0, 0, width, height)
r, g, b, a清除颜色(每帧使用)
depth, stencil清除深度和模板值
state当前状态(预留,暂未使用)
vulkan_command_bufferhandle命令缓冲句柄
state命令缓冲状态(用于验证操作顺序)

🔗 vulkan_backend.c - 集成使用

📄 初始化中创建 Renderpass
// 在 vulkan_renderer_backend_initialize 函数中
// 创建交换链后...

vulkan_renderpass_create(
    &context,
    &context.main_renderpass,
    0, 0, context.framebuffer_width, context.framebuffer_height,
    0.0f, 0.0f, 0.2f, 1.0f,  // 深蓝色背景
    1.0f,                     // 深度清除值
    0);                       // 模板清除值

KINFO("Vulkan renderer initialized successfully.");
📄 关闭时销毁 Renderpass
void vulkan_renderer_backend_shutdown(renderer_backend* backend) {
    // 按创建的逆序销毁

    // Renderpass
    vulkan_renderpass_destroy(&context, &context.main_renderpass);

    // Swapchain
    vulkan_swapchain_destroy(&context, &context.swapchain);

    // ... 其他资源
}

集成要点

  1. ✅ 在 Swapchain 之后创建(需要 image format)
  2. ✅ 在关闭时第一个销毁(依赖 Swapchain 的资源)
  3. ✅ 背景色设为 (0, 0, 0.2, 1),深蓝色

🎯 Attachment 详解

🌈 颜色附件(Color Attachment)

VkAttachmentDescription color_attachment;
color_attachment.format = context->swapchain.image_format.format;
color_attachment.samples = VK_SAMPLE_COUNT_1_BIT;
color_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
color_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
color_attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
color_attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
color_attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
color_attachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
Image Layout 转换流程
Renderpass Begin
渲染命令
Renderpass End
vkQueuePresentKHR
UNDEFINED
COLOR_ATTACHMENT_OPTIMAL
写入颜色数据
PRESENT_SRC_KHR
显示到屏幕

为什么 initialLayout = UNDEFINED?

✅ 性能优化:
   - 告诉 Vulkan 我们不关心之前的内容
   - GPU 可以跳过保留旧数据的操作
   - 特别适合每帧都清空的颜色缓冲

❌ 如果设为 COLOR_ATTACHMENT_OPTIMAL:
   - Vulkan 会尝试保留之前的内容
   - 增加不必要的内存操作

🔍 深度附件(Depth Attachment)

depth_attachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;

为什么不存储深度?

深度缓冲的生命周期:
┌─────────────────────────────────────────────────┐
│  帧 N 开始                                       │
│  ├─ 清空深度缓冲(1.0)                          │
│  ├─ 渲染所有物体(深度测试)                     │
│  └─ 帧 N 结束 ❌ 不需要保存                     │
│                                                 │
│  帧 N+1 开始                                     │
│  └─ 重新清空深度缓冲(1.0)← 重新开始            │
└─────────────────────────────────────────────────┘

✅ DONT_CARE 的好处:
   - TBR GPU:深度保留在 tile memory,不写回主内存
   - 节省带宽:深度缓冲通常很大(如 1920x1080 = 8MB)

例外情况(需要 STORE)

  • 🎮 需要深度信息做后处理(SSAO、景深效果)
  • 🔄 多 Subpass 之间共享深度
  • 📊 调试时保存深度缓冲

📊 Attachment 引用

VkAttachmentReference color_attachment_reference;
color_attachment_reference.attachment = 0;  // 索引
color_attachment_reference.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

引用机制

Attachment Descriptions (描述数组)
┌─────────────────────────────────┐
│ [0] 颜色附件(BGRA8, CLEAR)     │ ← attachment = 0
│ [1] 深度附件(D32, CLEAR)       │ ← attachment = 1
└─────────────────────────────────┘
        ↓
Subpass (使用引用)
┌─────────────────────────────────┐
│ pColorAttachments[0] → 索引 0    │
│ pDepthStencilAttachment → 索引 1 │
└─────────────────────────────────┘

🔗 Subpass 依赖详解

🔄 依赖关系配置

VkSubpassDependency dependency;
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.srcAccessMask = 0;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
dependency.dependencyFlags = 0;
🔬 逐字段分析
字段含义
srcSubpassVK_SUBPASS_EXTERNAL源:Renderpass 外部
dstSubpass0目标:第一个 Subpass
srcStageMaskCOLOR_ATTACHMENT_OUTPUT源阶段:颜色输出阶段
srcAccessMask0源访问:无(外部不需要)
dstStageMaskCOLOR_ATTACHMENT_OUTPUT目标阶段:颜色输出阶段
dstAccessMaskREAD | WRITE目标访问:读写颜色附件

⚡ 管线阶段(Pipeline Stages)

TOP_OF_PIPE
VERTEX_SHADER
FRAGMENT_SHADER
EARLY_FRAGMENT_TESTS
LATE_FRAGMENT_TESTS
COLOR_ATTACHMENT_OUTPUT
BOTTOM_OF_PIPE

为什么选择 COLOR_ATTACHMENT_OUTPUT?

Renderpass 外部 → Subpass 0 的依赖:
┌──────────────────────────────────────────────┐
│  帧 N-1: vkQueuePresentKHR                   │
│         ↓ (可能仍在进行)                     │
│  帧 N:  vkCmdBeginRenderPass                 │
│         ↓                                    │
│  需要等待:颜色附件输出完成                   │
│  才能开始:新帧的颜色附件读写                 │
└──────────────────────────────────────────────┘

🔐 访问掩码(Access Masks)

dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;

访问类型

访问掩码含义
COLOR_ATTACHMENT_READ读取颜色附件(混合时)
COLOR_ATTACHMENT_WRITE写入颜色附件
DEPTH_STENCIL_ATTACHMENT_READ读取深度(深度测试)
DEPTH_STENCIL_ATTACHMENT_WRITE写入深度
SHADER_READ着色器读取(纹理采样)
TRANSFER_READ传输操作读取

🎬 Renderpass 操作流程

完整使用示例

// 伪代码:完整的渲染流程
void render_frame() {
    // 1. 获取交换链图像
    u32 image_index;
    vkAcquireNextImageKHR(..., &image_index);

    // 2. 开始记录命令
    vkBeginCommandBuffer(cmd, ...);

    // 3. 开始 Renderpass
    vulkan_renderpass_begin(cmd, &main_renderpass, framebuffers[image_index]);

    // 4. 绑定管线
    vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);

    // 5. 绘制命令
    vkCmdDraw(cmd, 3, 1, 0, 0);  // 绘制三角形

    // 6. 结束 Renderpass
    vulkan_renderpass_end(cmd, &main_renderpass);

    // 7. 结束命令记录
    vkEndCommandBuffer(cmd);

    // 8. 提交命令
    vkQueueSubmit(graphics_queue, ...);

    // 9. 呈现
    vkQueuePresentKHR(present_queue, ...);
}

时序图

App Command Buffer GPU Screen vkCmdBeginRenderPass 清除颜色/深度缓冲 转换 Image Layout vkCmdDraw(...) vkCmdDraw(...) vkCmdEndRenderPass 转换 Layout 到 PRESENT_SRC vkQueueSubmit 执行渲染 vkQueuePresentKHR App Command Buffer GPU Screen

🎨 Clear Values 清除值

清除值数组

VkClearValue clear_values[2];
clear_values[0].color.float32[0] = 0.0f;  // R
clear_values[0].color.float32[1] = 0.0f;  // G
clear_values[0].color.float32[2] = 0.2f;  // B (深蓝色)
clear_values[0].color.float32[3] = 1.0f;  // A
clear_values[1].depthStencil.depth = 1.0f;     // 深度最远
clear_values[1].depthStencil.stencil = 0;      // 模板清零

清除值类型

附件类型清除值格式示例
颜色(浮点)color.float32[4]{0.0, 0.0, 0.2, 1.0}
颜色(整数)color.int32[4]{0, 0, 128, 255}
颜色(无符号)color.uint32[4]{0, 0, 128, 255}
深度/模板depthStencil.{depth, stencil}{1.0f, 0}

深度值约定

Vulkan 深度范围:[0.0, 1.0]
┌──────────────────────────────────┐
│  0.0 = 最近(Near Plane)         │
│  1.0 = 最远(Far Plane)          │
└──────────────────────────────────┘

清除为 1.0 的原因:
✅ 深度测试默认为 LESS(小于通过)
✅ 新物体深度 < 1.0,总能通过测试
✅ 逐步覆盖,近的物体遮挡远的物体

🏗️ 架构设计分析

模块职责划分

创建/销毁
使用
定义
定义
定义
执行
执行
执行
vulkan_backend
vulkan_renderpass
vulkan_command_buffer
Attachments
Subpasses
Dependencies
Begin Renderpass
Draw Commands
End Renderpass

状态管理

// 命令缓冲状态转换
READY
  → vkBeginCommandBuffer → RECORDING
  → vkCmdBeginRenderPass → IN_RENDER_PASS
  → vkCmdEndRenderPass   → RECORDING
  → vkEndCommandBuffer   → RECORDING_ENDED
  → vkQueueSubmit        → SUBMITTED
  → GPU 完成              → READY

状态验证的价值

// 示例:防止错误的 API 调用顺序
void vulkan_renderpass_begin(...) {
    KASSERT(command_buffer->state == COMMAND_BUFFER_STATE_RECORDING);
    // ...
    command_buffer->state = COMMAND_BUFFER_STATE_IN_RENDER_PASS;
}

void vulkan_renderpass_end(...) {
    KASSERT(command_buffer->state == COMMAND_BUFFER_STATE_IN_RENDER_PASS);
    // ...
    command_buffer->state = COMMAND_BUFFER_STATE_RECORDING;
}

🔬 深入理解:Image Layout 转换

Layout 类型详解

Layout用途性能特性
UNDEFINED初始状态,内容未定义⚡ 最快(可丢弃旧数据)
GENERAL通用布局,支持所有操作🐢 最慢(兼容性换性能)
COLOR_ATTACHMENT_OPTIMAL颜色附件读写⚡ 快
DEPTH_STENCIL_ATTACHMENT_OPTIMAL深度/模板附件⚡ 快
SHADER_READ_ONLY_OPTIMAL着色器采样纹理⚡ 快
TRANSFER_SRC_OPTIMAL传输源⚡ 快
TRANSFER_DST_OPTIMAL传输目标⚡ 快
PRESENT_SRC_KHR用于呈现到屏幕⚡ 快

自动转换 vs 手动转换

🔄 Renderpass 自动转换🔧 手动管线屏障
// Renderpass 定义转换
attachment.initialLayout = UNDEFINED;
attachment.finalLayout = PRESENT_SRC_KHR;

// Vulkan 自动执行:
// UNDEFINED → COLOR_ATTACHMENT_OPTIMAL
// (渲染)
// COLOR_ATTACHMENT_OPTIMAL → PRESENT_SRC_KHR

优点

  • ✅ 简洁
  • ✅ 驱动优化
  • ✅ 自动同步
// 手动管线屏障
VkImageMemoryBarrier barrier = {};
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
barrier.image = image;
// ... 其他字段

vkCmdPipelineBarrier(
    cmd,
    VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
    VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
    0, 0, 0, 0, 0, 1, &barrier);

优点

  • ✅ 灵活控制
  • ✅ 适用于复杂场景

💡 最佳实践

✅ 推荐做法

实践说明示例
UNDEFINED 初始布局不需要保留内容时使用每帧清空的颜色缓冲
DONT_CARE 存储不需要的附件不保存深度缓冲(通常)
匹配 Swapchain 格式颜色附件格式与交换链一致swapchain.image_format.format
正确的 finalLayout颜色附件用 PRESENT_SRC_KHR用于显示的图像
设置 Subpass 依赖确保正确同步EXTERNAL → 0 依赖
状态验证记录命令缓冲状态防止 API 误用

❌ 避免的错误

// ❌ 错误 1:遗忘 finalLayout
attachment.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
// 正确:应该是 PRESENT_SRC_KHR(用于呈现)

// ❌ 错误 2:不必要的 LOAD 操作
attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
// 正确:如果会清空或完全覆盖,使用 CLEAR 或 DONT_CARE

// ❌ 错误 3:深度缓冲使用 STORE
depth_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
// 正确:通常使用 DONT_CARE(除非需要深度信息)

// ❌ 错误 4:缺少 Subpass 依赖
// 可能导致:图像尚未准备好就开始渲染
// 正确:设置 VK_SUBPASS_EXTERNAL → 0 依赖

// ❌ 错误 5:Clear Values 数量不匹配
begin_info.clearValueCount = 1;  // 但有 2 个附件
// 正确:clearValueCount = 附件数量

🎯 本章总结

🎓 你学到了什么

🔧 技术技能

✅ 创建 Vulkan Renderpass
✅ 配置颜色和深度附件
✅ 设置 Subpass 和依赖
✅ 管理 Image Layout 转换
✅ 实现 Renderpass 生命周期管理
✅ 集成到渲染后端

💡 核心概念

✅ Renderpass 的作用和价值
✅ Attachment 的类型和配置
✅ Load/Store 操作的性能影响
✅ Subpass 依赖和同步机制
✅ Image Layout 优化策略
✅ TBR GPU 的优化技术

🎯 关键要点

╔════════════════════════════════════════════════════════╗
║  🔑 Renderpass 的三大核心                               ║
╠════════════════════════════════════════════════════════╣
║  1️⃣ Attachments(附件)                               ║
║     定义渲染目标的格式、操作、布局转换                   ║
║                                                        ║
║  2️⃣ Subpasses(子通道)                               ║
║     划分渲染阶段,实现高级渲染技术                       ║
║                                                        ║
║  3️⃣ Dependencies(依赖)                              ║
║     确保正确的同步和执行顺序                            ║
╚════════════════════════════════════════════════════════╝

🔄 完整流程回顾

创建 Renderpass
配置 Attachments
设置 Subpass
定义 Dependencies
vkCreateRenderPass
vkCmdBeginRenderPass
清除附件
转换 Layout
记录绘制命令
vkCmdEndRenderPass
转换 Layout 到 PRESENT
vkQueueSubmit
GPU 执行
vkQueuePresentKHR

❓ 常见问题(FAQ)

Q1: 为什么需要 Renderpass?直接绘制不行吗?

A: Vulkan 的设计哲学是"显式控制":

  1. 驱动优化:提前知道渲染流程,可以优化内存访问模式
  2. TBR 优化:移动 GPU 的 Tile-Based Rendering 依赖 Renderpass 信息
  3. 自动同步:Vulkan 自动处理 Image Layout 转换和依赖
  4. 多通道渲染:支持延迟渲染等高级技术

没有 Renderpass,这些优化都需要手动管理,极其复杂。

Q2: initialLayout 为什么设为 UNDEFINED?

A: 性能优化:

UNDEFINED:
  ✅ 告诉 Vulkan 不需要保留旧内容
  ✅ GPU 可以跳过读取操作
  ✅ 适用于每帧清空的缓冲

其他布局(如 COLOR_ATTACHMENT_OPTIMAL):
  ❌ Vulkan 会尝试保留内容
  ❌ 增加内存带宽消耗
  ❌ 仅适用于需要保留内容的场景
Q3: 深度缓冲为什么用 DONT_CARE 存储?

A: 深度缓冲通常只在当前帧使用:

每帧的深度流程:
  1. 清空深度为 1.0
  2. 渲染物体,更新深度
  3. 帧结束 → 深度数据无用
  4. 下一帧重新清空

使用 DONT_CARE:
  ✅ TBR GPU 可以保留深度在 tile memory
  ✅ 节省带宽(深度缓冲很大)
  ✅ 提升性能

例外:需要深度做后处理时使用 STORE

Q4: Subpass 依赖的 srcSubpass 为什么是 EXTERNAL?

A: VK_SUBPASS_EXTERNAL 表示 Renderpass 外部:

依赖:EXTERNAL → Subpass 0

含义:
  "在开始 Subpass 0 之前,等待外部操作完成"

具体场景:
  - 等待上一帧的 vkQueuePresentKHR 完成
  - 确保图像不再被显示系统使用
  - 才能开始新帧的渲染
Q5: 如何支持多个 Subpass(延迟渲染)?

A: 示例:G-Buffer 延迟渲染

// Subpass 0: 生成 G-Buffer
VkSubpassDescription subpass0 = {};
subpass0.colorAttachmentCount = 3;  // 位置、法线、颜色
VkAttachmentReference gbuffer_refs[3] = {...};
subpass0.pColorAttachments = gbuffer_refs;

// Subpass 1: 光照计算
VkSubpassDescription subpass1 = {};
subpass1.inputAttachmentCount = 3;  // 读取 G-Buffer
subpass1.pInputAttachments = gbuffer_refs;
subpass1.colorAttachmentCount = 1;  // 最终颜色
subpass1.pColorAttachments = &final_color_ref;

// 依赖:Subpass 0 → Subpass 1
VkSubpassDependency dep = {};
dep.srcSubpass = 0;
dep.dstSubpass = 1;
dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dep.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
dep.dstAccessMask = VK_ACCESS_INPUT_ATTACHMENT_READ_BIT;
Q6: Clear Values 的索引如何对应?

A: 清除值数组索引对应附件描述索引:

// Attachment 描述
VkAttachmentDescription attachments[2];
attachments[0] = color_attachment;   // 索引 0
attachments[1] = depth_attachment;   // 索引 1

// Clear Values
VkClearValue clear_values[2];
clear_values[0].color = {...};       // 对应索引 0(颜色)
clear_values[1].depthStencil = {...};// 对应索引 1(深度)

// ⚠️ 顺序必须匹配!

📝 练习题

🥉 初级练习

1. 修改背景颜色

任务:将清除颜色从深蓝色 (0, 0, 0.2, 1) 改为红色 (1, 0, 0, 1)

提示:修改 vulkan_backend.c 中的 vulkan_renderpass_create 调用。

参考答案

vulkan_renderpass_create(
    &context,
    &context.main_renderpass,
    0, 0, context.framebuffer_width, context.framebuffer_height,
    1.0f, 0.0f, 0.0f, 1.0f,  // 红色
    1.0f,
    0);
2. 添加调试日志

任务:在 vulkan_renderpass_create 中打印 Renderpass 创建信息。

参考答案

KDEBUG("Creating Renderpass: size=%dx%d, clear_color=(%.2f,%.2f,%.2f,%.2f)",
    (u32)w, (u32)h, r, g, b, a);

VK_CHECK(vkCreateRenderPass(...));

KINFO("Renderpass created successfully: handle=0x%p", out_renderpass->handle);

🥈 中级练习

3. 支持自定义渲染区域

任务:允许渲染到窗口的一部分(如左上角 1/4 区域)。

提示

  • 修改 vulkan_renderpass_createx, y, w, h 参数
  • 更新 vulkan_renderpass_begin 中的 renderArea

参考实现

// 渲染到左上角 1/4
vulkan_renderpass_create(
    &context,
    &context.quarter_renderpass,
    0, 0, context.framebuffer_width / 2, context.framebuffer_height / 2,
    0.0f, 0.0f, 0.2f, 1.0f,
    1.0f, 0);
4. 添加模板缓冲支持

任务:修改深度附件,启用模板缓冲。

关键修改

// 1. 使用带模板的格式
depth_attachment.format = VK_FORMAT_D32_SFLOAT_S8_UINT;

// 2. 配置模板操作
depth_attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
depth_attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;

// 3. 清除模板值
clear_values[1].depthStencil.stencil = 0;

🥇 高级练习

5. 实现双 Subpass 延迟渲染

任务:创建支持 G-Buffer 和光照计算的 Renderpass。

框架

// Attachment 0-2: G-Buffer (位置、法线、颜色)
// Attachment 3: 最终颜色
// Attachment 4: 深度

// Subpass 0: 生成 G-Buffer
// Subpass 1: 光照计算(读取 G-Buffer,输出最终颜色)

// 依赖:Subpass 0 → Subpass 1

参考资料:Vulkan Tutorial - Deferred Rendering

6. 支持 MSAA 多重采样

任务:修改 Renderpass 支持 4x MSAA。

关键点

  1. 创建多重采样颜色附件(VK_SAMPLE_COUNT_4_BIT
  2. 创建 Resolve 附件(VK_SAMPLE_COUNT_1_BIT
  3. 在 Subpass 中设置 pResolveAttachments
  4. 更新 Framebuffer 创建

参考:Vulkan Spec - Resolving Multisample Images


🔗 参考资料

📚 官方文档

资源链接说明
Vulkan SpecRenderpass Chapter官方规范
Vulkan GuideRenderpassKhronos 官方指南
ARM GuideVulkan Best PracticesARM 移动 GPU 优化

📖 推荐阅读

  • 📘 Vulkan Programming Guide - Chapter 7: Render Passes
  • 📙 Learning Vulkan - Chapter 5: Command Buffer and Render Pass
  • 📕 Mastering Graphics Programming - Renderpass Design Patterns

🎬 视频教程

🔧 工具推荐

工具用途
RenderDocRenderpass 调试可视化
Nsight GraphicsNVIDIA GPU Renderpass 分析
Radeon GPU ProfilerAMD GPU Renderpass 优化

🎉 恭喜你完成了 Vulkan Renderpass 教程!

现在你已经掌握了 Vulkan 渲染通道的创建和使用。

下一步


📖 关注公众号

关注【shangshoushiyanshi】,领取章节视频教程


💖 支持作者

如果这篇教程对你有帮助,欢迎请作者喝杯咖啡 ☕

您的支持是我持续创作的动力!

感谢每一位支持者!🙏


📅 最后更新:2025-11-20
✍️ 作者:上手实验室

### Vulkan 渲染通道概念 渲染通道(Render Pass)是 Vulkan 中的一个重要特性,用于描述图像子资源的状态转换过程以及附着点之间的依赖关系。通过这种方式,应用程序可以精确地告诉 GPU 如何处理不同阶段的数据流动和状态变化[^1]。 在创建一个 `VkRenderPass` 对象时,需要指定多个参数,包括但不限于颜色、深度/模板缓冲区等信息。这些设置决定了后续帧缓存对象如何与之关联并执行具体的绘制操作[^4]。 具体来说,在初始化过程中涉及到如下几个方面: - **附件描述符 (Attachment Descriptions)**:定义了参与此渲染流程中的所有图像视图及其属性; - **子通道 (Subpass)** :表示一组命令集,它们共享相同的输入输出配置; - **依赖项 (Dependencies)**:指定了各子通道间存在的同步条件或屏障机制。 ```cpp // 创建 VkRenderPass 的 C++ 代码片段 VkRenderPassCreateInfo renderPassInfo{}; renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; renderPassInfo.attachmentCount = static_cast<uint32_t>(attachments.size()); renderPassInfo.pAttachments = attachments.data(); renderPassInfo.subpassCount = 1; // 假设只有一个子通道 renderPassInfo.pSubpasses = &subpass; if (vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) { throw std::runtime_error("failed to create render pass!"); } ``` 这段代码展示了如何构建一个简单的渲染通道实例,并将其绑定至逻辑设备上。这里假设只存在单一类型的附件列表及相应的子通道结构体。 当实际应用中涉及复杂的场景渲染任务时,则可能需要设计更加精细的多级嵌套式的渲染管道架构,从而充分利用现代GPU的强大功能实现高效的图形渲染效果。 #### 使用示例 为了更好地理解上述理论部分的内容,下面给出一段完整的C++代码用来展示怎样在一个窗口内呈现基本的颜色填充矩形区域: ```cpp void initFramebuffers() { swapChainFramebuffers.resize(swapChainImageViews.size()); for (size_t i = 0; i < swapChainImageViews.size(); ++i){ VkFramebufferCreateInfo framebufferInfo{}; framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; framebufferInfo.renderPass = renderPass; framebufferInfo.attachmentCount = 1; framebufferInfo.pAttachments = &swapChainImageViews[i]; 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!"); } } } void drawFrame(){ vkWaitForFences(device,1,&inFlightFences[currentFrame],VK_TRUE,UINT64_MAX); uint32_t imageIndex; auto result = vkAcquireNextImageKHR( device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); ... VkSubmitInfo submitInfo{}; ... vkQueueSubmit(graphicsQueue,1,&submitInfo,fence); vkQueuePresentKHR(presentQueue,&presentInfo); } ``` 以上函数分别实现了交换链帧缓存器(`VkFramebuffer`) 和绘图帧 (`drawFrame`) 的准备工作。前者负责连接特定于平台表面的具体像素数据源到之前所建立好的渲染通道路由之中去;后者则包含了提交给队列的一系列指令集合,最终完成整个画面显示的过程。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值