上一篇:Vulkan 逻辑设备和交换链 | 下一篇:待定 | 返回目录
📚 快速导航
目录 (点击展开/折叠)- 🎯 本章目标
- 📖 教程概述
- 🏗️ Renderpass 核心概念
- 📁 项目结构分析
- 🔍 核心文件详解
- 🎯 Attachment 详解
- 🔗 Subpass 依赖详解
- 🎬 Renderpass 操作流程
- 🎨 Clear Values 清除值
- 🏗️ 架构设计分析
- 🔬 深入理解:Image Layout 转换
- 💡 最佳实践
- 🎯 本章总结
- ❓ 常见问题(FAQ)
- 📝 练习题
- 🔗 参考资料
🎯 本章目标
通过本教程,你将学会:
| 🎯 目标 | 📝 描述 | ✅ 成果 |
|---|---|---|
| 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
核心任务:
- ✅ 定义 Renderpass 数据结构
- ✅ 配置颜色和深度附件
- ✅ 创建 Subpass 描述
- ✅ 设置 Subpass 依赖关系
- ✅ 实现 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 生命周期
🎯 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 操作对比:
| 操作 | LOAD | CLEAR | DONT_CARE | STORE | DONT_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_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_destroy | context, 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:颜色附件配置
| 字段 | 值 | 含义 |
|---|---|---|
format | swapchain 格式 | 与交换链图像格式一致(通常是 BGRA8) |
samples | VK_SAMPLE_COUNT_1_BIT | 无多重采样(1 个样本) |
loadOp | CLEAR | 渲染前清空附件 |
storeOp | STORE | 渲染后保存结果(需要显示) |
initialLayout | UNDEFINED | 不关心之前的内容(性能优化) |
finalLayout | PRESENT_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;
}
关键点:
- Render Area:指定渲染区域(通常是整个 framebuffer)
- Clear Values:清除值数组,索引对应 attachment
clear_values[0]→ 颜色附件(索引 0)clear_values[1]→ 深度附件(索引 1)
- 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_renderpass | handle | Vulkan 对象句柄 |
x, y, w, h | 渲染区域(通常是 0, 0, width, height) | |
r, g, b, a | 清除颜色(每帧使用) | |
depth, stencil | 清除深度和模板值 | |
state | 当前状态(预留,暂未使用) | |
| vulkan_command_buffer | handle | 命令缓冲句柄 |
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);
// ... 其他资源
}
集成要点:
- ✅ 在 Swapchain 之后创建(需要 image format)
- ✅ 在关闭时第一个销毁(依赖 Swapchain 的资源)
- ✅ 背景色设为
(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 转换流程
为什么 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;
🔬 逐字段分析
| 字段 | 值 | 含义 |
|---|---|---|
srcSubpass | VK_SUBPASS_EXTERNAL | 源:Renderpass 外部 |
dstSubpass | 0 | 目标:第一个 Subpass |
srcStageMask | COLOR_ATTACHMENT_OUTPUT | 源阶段:颜色输出阶段 |
srcAccessMask | 0 | 源访问:无(外部不需要) |
dstStageMask | COLOR_ATTACHMENT_OUTPUT | 目标阶段:颜色输出阶段 |
dstAccessMask | READ | WRITE | 目标访问:读写颜色附件 |
⚡ 管线阶段(Pipeline Stages)
为什么选择 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, ...);
}
时序图:
🎨 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,总能通过测试
✅ 逐步覆盖,近的物体遮挡远的物体
🏗️ 架构设计分析
模块职责划分
状态管理
// 命令缓冲状态转换
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(依赖) ║
║ 确保正确的同步和执行顺序 ║
╚════════════════════════════════════════════════════════╝
🔄 完整流程回顾:
❓ 常见问题(FAQ)
Q1: 为什么需要 Renderpass?直接绘制不行吗?A: Vulkan 的设计哲学是"显式控制":
- 驱动优化:提前知道渲染流程,可以优化内存访问模式
- TBR 优化:移动 GPU 的 Tile-Based Rendering 依赖 Renderpass 信息
- 自动同步:Vulkan 自动处理 Image Layout 转换和依赖
- 多通道渲染:支持延迟渲染等高级技术
没有 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
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_create的x, 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。
关键点:
- 创建多重采样颜色附件(
VK_SAMPLE_COUNT_4_BIT) - 创建 Resolve 附件(
VK_SAMPLE_COUNT_1_BIT) - 在 Subpass 中设置
pResolveAttachments - 更新 Framebuffer 创建
参考:Vulkan Spec - Resolving Multisample Images
🔗 参考资料
📚 官方文档
| 资源 | 链接 | 说明 |
|---|---|---|
| Vulkan Spec | Renderpass Chapter | 官方规范 |
| Vulkan Guide | Renderpass | Khronos 官方指南 |
| ARM Guide | Vulkan Best Practices | ARM 移动 GPU 优化 |
📖 推荐阅读
- 📘 Vulkan Programming Guide - Chapter 7: Render Passes
- 📙 Learning Vulkan - Chapter 5: Command Buffer and Render Pass
- 📕 Mastering Graphics Programming - Renderpass Design Patterns
🎬 视频教程
🔧 工具推荐
| 工具 | 用途 |
|---|---|
| RenderDoc | Renderpass 调试可视化 |
| Nsight Graphics | NVIDIA GPU Renderpass 分析 |
| Radeon GPU Profiler | AMD GPU Renderpass 优化 |
🎉 恭喜你完成了 Vulkan Renderpass 教程!
现在你已经掌握了 Vulkan 渲染通道的创建和使用。
下一步:
- 📖 教程 14:Command Buffers(待定)
- 🔙 返回教程目录
📖 关注公众号
关注【shangshoushiyanshi】,领取章节视频教程
💖 支持作者
如果这篇教程对你有帮助,欢迎请作者喝杯咖啡 ☕
您的支持是我持续创作的动力!
感谢每一位支持者!🙏
📅 最后更新:2025-11-20
✍️ 作者:上手实验室
1201

被折叠的 条评论
为什么被折叠?



