上一篇:教程 21 - Vulkan图形管线 | 下一篇:教程 23 - 绘制到屏幕 | 返回目录
📚 快速导航
目录 (点击展开/折叠)🎯 本章目标
通过本教程,你将学会:
| 🎯 目标 | 📝 描述 | ✅ 成果 |
|---|---|---|
| 缓冲区概念 | 理解VkBuffer和VkDeviceMemory的分离 | 掌握Vulkan内存模型 |
| 内存类型 | 选择合适的内存类型 | 区分DEVICE_LOCAL和HOST_VISIBLE |
| 缓冲区创建 | 实现缓冲区创建和绑定 | 完成vulkan_buffer.h/c |
| 内存映射 | 实现CPU到GPU的数据传输 | lock/unlock机制 |
| 暂存缓冲区 | 使用两步传输策略 | 最优性能的数据上传 |
| 顶点和索引 | 创建顶点和索引缓冲区 | 为渲染做好数据准备 |
📖 教程概述
🔍 什么是Vulkan缓冲区?
缓冲区(Buffer)是GPU内存中的一块线性存储区域,用于存储各种类型的数据。
┌────────────────────────────────────────────────────┐
│ GPU内存布局 │
├────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ │
│ │ 顶点缓冲区 │ ← 顶点位置、法线、UV等 │
│ │ (Vertex Buffer) │ │
│ └──────────────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ 索引缓冲区 │ ← 三角形索引 │
│ │ (Index Buffer) │ │
│ └──────────────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ Uniform缓冲区 │ ← 变换矩阵、材质参数 │
│ │ (UBO) │ │
│ └──────────────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ 暂存缓冲区 │ ← 临时数据传输 │
│ │ (Staging Buffer)│ │
│ └──────────────────┘ │
│ │
└────────────────────────────────────────────────────┘
❓ 为什么需要缓冲区?
Vulkan缓冲区 vs 传统内存
| 特性 | 普通内存 | Vulkan缓冲区 |
|---|---|---|
| 位置 | 系统RAM | GPU内存 |
| 访问速度 | CPU快,GPU慢 | GPU极快 |
| 可见性 | CPU可见 | 需要映射才能CPU访问 |
| 生命周期 | 应用管理 | 显式创建/销毁 |
| 用途 | 通用数据 | 渲染数据 |
╔════════════════════════════════════════════════════╗
║ 为什么不能直接使用系统内存? ║
╠════════════════════════════════════════════════════╣
║ ✗ GPU无法直接访问系统RAM ║
║ ✗ PCIe总线带宽有限(比GPU内存慢得多) ║
║ ✗ 延迟高(GPU等待数据从CPU传输) ║
║ ✗ 无法优化内存布局 ║
║ ║
║ ✓ Vulkan缓冲区位于GPU内存,访问速度极快 ║
║ ✓ 可选择最佳内存类型(性能 vs 可访问性) ║
║ ✓ 显式控制数据传输时机 ║
╚════════════════════════════════════════════════════╝
🛠️ 实现路线图
🧠 核心概念
缓冲区与内存
在Vulkan中,缓冲区和内存是分离的!
┌─────────────────────────────────────────────────┐
│ 1. 创建缓冲区对象 (VkBuffer) │
│ - 定义用途(顶点、索引等) │
│ - 定义大小 │
│ - 此时还没有实际的内存! │
└──────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 2. 查询内存需求 │
│ - Vulkan告诉你需要多少内存 │
│ - 需要什么类型的内存 │
│ - 对齐要求 │
└──────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 3. 分配设备内存 (VkDeviceMemory) │
│ - 实际分配GPU内存 │
│ - 可以分配一大块,多个缓冲区共享 │
└──────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 4. 绑定缓冲区到内存 │
│ - vkBindBufferMemory() │
│ - 现在缓冲区才真正可用! │
└─────────────────────────────────────────────────┘
💡 设计理由:
- 灵活性 - 可以将多个小缓冲区绑定到一块大内存(子分配)
- 优化 - 减少内存分配次数(分配开销大)
- 控制 - 精确控制内存类型和对齐
内存类型
GPU有多种类型的内存,各有优缺点:
| 内存类型 | 位置 | CPU访问 | GPU访问 | 速度 | 用途 |
|---|---|---|---|---|---|
| DEVICE_LOCAL | GPU VRAM | ❌ 不可见 | ✅ 极快 | 🚀🚀🚀 | 顶点、纹理 |
| HOST_VISIBLE | 系统RAM或GPU | ✅ 可映射 | ⚠️ 较慢 | 🚀 | 暂存缓冲区 |
| HOST_COHERENT | 系统RAM或GPU | ✅ 自动同步 | ⚠️ 较慢 | 🚀 | 频繁更新的数据 |
| HOST_CACHED | 系统RAM | ✅ CPU缓存 | ⚠️ 较慢 | 🚀🚀 | CPU读取的数据 |
内存属性标志
// 常用组合
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT // GPU最快访问
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT // CPU可映射
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT // 自动同步,无需flush
VK_MEMORY_PROPERTY_HOST_CACHED_BIT // CPU缓存加速
// 典型使用场景
// 顶点缓冲区(最终)
DEVICE_LOCAL
// 暂存缓冲区(CPU→GPU传输)
HOST_VISIBLE | HOST_COHERENT
// Uniform缓冲区(频繁更新)
HOST_VISIBLE | HOST_COHERENT
缓冲区用途
// VkBufferUsageFlagBits - 告诉Vulkan缓冲区如何使用
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT // 顶点缓冲区
VK_BUFFER_USAGE_INDEX_BUFFER_BIT // 索引缓冲区
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT // Uniform缓冲区
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT // 存储缓冲区(SSBO)
VK_BUFFER_USAGE_TRANSFER_SRC_BIT // 传输源(复制from)
VK_BUFFER_USAGE_TRANSFER_DST_BIT // 传输目标(复制to)
主机可见 vs 设备本地
两步传输策略(最佳性能):
┌──────────────────────────────────────────────────────┐
│ 步骤1:CPU → 暂存缓冲区(HOST_VISIBLE) │
│ ┌─────────┐ │
│ │ CPU数据 │ ─────────→ [暂存缓冲区] │
│ └─────────┘ (系统RAM) │
└──────────────────────────────────────────────────────┘
│
│ vkCmdCopyBuffer()
▼
┌──────────────────────────────────────────────────────┐
│ 步骤2:暂存缓冲区 → 设备本地缓冲区(DEVICE_LOCAL) │
│ │
│ [暂存缓冲区] ─────────→ [顶点缓冲区] │
│ (系统RAM) (GPU VRAM) │
│ ⚡ GPU访问极快 │
└──────────────────────────────────────────────────────┘
⚠️ 为什么不直接映射DEVICE_LOCAL内存?
- 某些GPU不允许CPU直接访问VRAM
- 即使允许,通过PCIe访问也很慢
- 两步传输虽然麻烦,但性能最优
💻 核心实现
步骤1:定义缓冲区结构
在 engine/src/renderer/vulkan/vulkan_types.inl 中添加:
/**
* @brief Vulkan缓冲区封装
*
* 包含缓冲区对象和其绑定的内存
*/
typedef struct vulkan_buffer {
/// 缓冲区总大小(字节)
u64 total_size;
/// Vulkan缓冲区句柄
VkBuffer handle;
/// 缓冲区用途标志
VkBufferUsageFlagBits usage;
/// 内存是否已被锁定(映射)
b8 is_locked;
/// 绑定的设备内存
VkDeviceMemory memory;
/// 内存类型索引
i32 memory_index;
/// 内存属性标志
u32 memory_property_flags;
} vulkan_buffer;
步骤2:创建缓冲区
创建 engine/src/renderer/vulkan/vulkan_buffer.h:
#pragma once
#include "vulkan_types.inl"
/**
* @brief 创建Vulkan缓冲区
*
* @param context Vulkan上下文
* @param size 缓冲区大小(字节)
* @param usage 缓冲区用途标志
* @param memory_property_flags 内存属性标志
* @param bind_on_create 是否立即绑定内存
* @param out_buffer 输出缓冲区对象
* @return 成功返回true
*/
b8 vulkan_buffer_create(
vulkan_context* context,
u64 size,
VkBufferUsageFlagBits usage,
u32 memory_property_flags,
b8 bind_on_create,
vulkan_buffer* out_buffer
);
/**
* @brief 销毁缓冲区
*/
void vulkan_buffer_destroy(vulkan_context* context, vulkan_buffer* buffer);
/**
* @brief 调整缓冲区大小
*/
b8 vulkan_buffer_resize(
vulkan_context* context,
u64 new_size,
vulkan_buffer* buffer,
VkQueue queue,
VkCommandPool pool
);
/**
* @brief 绑定缓冲区到内存
*/
void vulkan_buffer_bind(vulkan_context* context, vulkan_buffer* buffer, u64 offset);
/**
* @brief 锁定(映射)缓冲区内存
*/
void* vulkan_buffer_lock_memory(
vulkan_context* context,
vulkan_buffer* buffer,
u64 offset,
u64 size,
u32 flags
);
/**
* @brief 解锁(取消映射)缓冲区内存
*/
void vulkan_buffer_unlock_memory(vulkan_context* context, vulkan_buffer* buffer);
/**
* @brief 加载数据到缓冲区
*/
void vulkan_buffer_load_data(
vulkan_context* context,
vulkan_buffer* buffer,
u64 offset,
u64 size,
u32 flags,
const void* data
);
/**
* @brief 在GPU上复制缓冲区数据
*/
void vulkan_buffer_copy_to(
vulkan_context* context,
VkCommandPool pool,
VkFence fence,
VkQueue queue,
VkBuffer source,
u64 source_offset,
VkBuffer dest,
u64 dest_offset,
u64 size
);
创建 engine/src/renderer/vulkan/vulkan_buffer.c:
#include "vulkan_buffer.h"
#include "vulkan_device.h"
#include "vulkan_command_buffer.h"
#include "vulkan_utils.h"
#include "core/logger.h"
#include "core/kmemory.h"
b8 vulkan_buffer_create(
vulkan_context* context,
u64 size,
VkBufferUsageFlagBits usage,
u32 memory_property_flags,
b8 bind_on_create,
vulkan_buffer* out_buffer) {
// 初始化输出结构
kzero_memory(out_buffer, sizeof(vulkan_buffer));
out_buffer->total_size = size;
out_buffer->usage = usage;
out_buffer->memory_property_flags = memory_property_flags;
// ========================================
// 1. 创建缓冲区对象
// ========================================
VkBufferCreateInfo buffer_info = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
buffer_info.size = size;
buffer_info.usage = usage;
buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; // 仅在一个队列中使用
VK_CHECK(vkCreateBuffer(
context->device.logical_device,
&buffer_info,
context->allocator,
&out_buffer->handle
));
// ========================================
// 2. 查询内存需求
// ========================================
VkMemoryRequirements requirements;
vkGetBufferMemoryRequirements(
context->device.logical_device,
out_buffer->handle,
&requirements
);
// ========================================
// 3. 找到合适的内存类型
// ========================================
out_buffer->memory_index = context->find_memory_index(
requirements.memoryTypeBits,
out_buffer->memory_property_flags
);
if (out_buffer->memory_index == -1) {
KERROR("Unable to create vulkan buffer because the required memory type index was not found.");
return false;
}
// ========================================
// 4. 分配设备内存
// ========================================
VkMemoryAllocateInfo allocate_info = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocate_info.allocationSize = requirements.size;
allocate_info.memoryTypeIndex = (u32)out_buffer->memory_index;
VkResult result = vkAllocateMemory(
context->device.logical_device,
&allocate_info,
context->allocator,
&out_buffer->memory
);
if (result != VK_SUCCESS) {
KERROR("Unable to create vulkan buffer because the required memory allocation failed. Error: %i", result);
return false;
}
// ========================================
// 5. 绑定缓冲区到内存(可选)
// ========================================
if (bind_on_create) {
vulkan_buffer_bind(context, out_buffer, 0);
}
return true;
}
🔍 关键步骤解析:
- 创建缓冲区对象 - 定义大小和用途,但还没有内存
- 查询内存需求 - Vulkan告诉我们需要多少内存、对齐要求
- 查找内存类型 - 根据需求标志找到合适的内存堆
- 分配内存 - 实际分配GPU内存
- 绑定 - 将缓冲区对象关联到内存
步骤3:内存映射
/**
* @brief 绑定缓冲区到内存
*
* 将VkBuffer与VkDeviceMemory关联
*/
void vulkan_buffer_bind(vulkan_context* context, vulkan_buffer* buffer, u64 offset) {
VK_CHECK(vkBindBufferMemory(
context->device.logical_device,
buffer->handle,
buffer->memory,
offset
));
}
/**
* @brief 锁定(映射)缓冲区内存
*
* 映射GPU内存到CPU可访问的地址空间
* 仅适用于HOST_VISIBLE内存
*
* @return 映射后的CPU可访问指针
*/
void* vulkan_buffer_lock_memory(
vulkan_context* context,
vulkan_buffer* buffer,
u64 offset,
u64 size,
u32 flags) {
void* data;
VK_CHECK(vkMapMemory(
context->device.logical_device,
buffer->memory,
offset,
size,
flags,
&data
));
return data;
}
/**
* @brief 解锁(取消映射)缓冲区内存
*
* 取消映射后,CPU不应再访问该指针
*/
void vulkan_buffer_unlock_memory(vulkan_context* context, vulkan_buffer* buffer) {
vkUnmapMemory(context->device.logical_device, buffer->memory);
}
⚠️ 映射限制:
- 只能映射
HOST_VISIBLE内存- 映射后必须最终解除映射
- 非
HOST_COHERENT内存需要手动flush/invalidate
步骤4:加载数据
/**
* @brief 便捷函数:加载数据到缓冲区
*
* 内部处理映射→复制→解除映射的完整流程
*/
void vulkan_buffer_load_data(
vulkan_context* context,
vulkan_buffer* buffer,
u64 offset,
u64 size,
u32 flags,
const void* data) {
// 1. 映射内存
void* data_ptr;
VK_CHECK(vkMapMemory(
context->device.logical_device,
buffer->memory,
offset,
size,
flags,
&data_ptr
));
// 2. 复制数据
kcopy_memory(data_ptr, data, size);
// 3. 解除映射
vkUnmapMemory(context->device.logical_device, buffer->memory);
}
使用示例:
// 创建HOST_VISIBLE缓冲区
vulkan_buffer staging_buffer;
vulkan_buffer_create(
context,
sizeof(vertices),
VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
true,
&staging_buffer
);
// 直接加载数据
vulkan_buffer_load_data(context, &staging_buffer, 0, sizeof(vertices), 0, vertices);
步骤5:缓冲区复制
/**
* @brief 在GPU上复制缓冲区数据
*
* 使用GPU复制比CPU复制快得多!
* 这是从暂存缓冲区传输到设备本地缓冲区的关键操作
*/
void vulkan_buffer_copy_to(
vulkan_context* context,
VkCommandPool pool,
VkFence fence,
VkQueue queue,
VkBuffer source,
u64 source_offset,
VkBuffer dest,
u64 dest_offset,
u64 size) {
// 等待队列空闲
vkQueueWaitIdle(queue);
// ========================================
// 1. 创建临时命令缓冲区
// ========================================
vulkan_command_buffer temp_command_buffer;
vulkan_command_buffer_allocate_and_begin_single_use(
context,
pool,
&temp_command_buffer
);
// ========================================
// 2. 记录复制命令
// ========================================
VkBufferCopy copy_region;
copy_region.srcOffset = source_offset;
copy_region.dstOffset = dest_offset;
copy_region.size = size;
vkCmdCopyBuffer(
temp_command_buffer.handle,
source,
dest,
1,
©_region
);
// ========================================
// 3. 提交并等待完成
// ========================================
vulkan_command_buffer_end_single_use(
context,
pool,
&temp_command_buffer,
queue
);
}
💡 为什么使用GPU复制?
- GPU的DMA引擎专门优化了内存复制
- 比CPU通过PCIe复制快得多
- 异步操作,不阻塞CPU
步骤6:缓冲区调整大小
/**
* @brief 调整缓冲区大小
*
* 步骤:
* 1. 创建新的更大缓冲区
* 2. 复制旧数据到新缓冲区
* 3. 销毁旧缓冲区
* 4. 更新缓冲区句柄
*/
b8 vulkan_buffer_resize(
vulkan_context* context,
u64 new_size,
vulkan_buffer* buffer,
VkQueue queue,
VkCommandPool pool) {
// ========================================
// 1. 创建新缓冲区
// ========================================
VkBufferCreateInfo buffer_info = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
buffer_info.size = new_size;
buffer_info.usage = buffer->usage;
buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VkBuffer new_buffer;
VK_CHECK(vkCreateBuffer(
context->device.logical_device,
&buffer_info,
context->allocator,
&new_buffer
));
// ========================================
// 2. 查询并分配新内存
// ========================================
VkMemoryRequirements requirements;
vkGetBufferMemoryRequirements(context->device.logical_device, new_buffer, &requirements);
VkMemoryAllocateInfo allocate_info = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocate_info.allocationSize = requirements.size;
allocate_info.memoryTypeIndex = (u32)buffer->memory_index;
VkDeviceMemory new_memory;
VkResult result = vkAllocateMemory(
context->device.logical_device,
&allocate_info,
context->allocator,
&new_memory
);
if (result != VK_SUCCESS) {
KERROR("Unable to resize vulkan buffer because the required memory allocation failed. Error: %i", result);
return false;
}
// ========================================
// 3. 绑定新缓冲区
// ========================================
VK_CHECK(vkBindBufferMemory(
context->device.logical_device,
new_buffer,
new_memory,
0
));
// ========================================
// 4. 复制旧数据
// ========================================
vulkan_buffer_copy_to(
context,
pool,
0,
queue,
buffer->handle, // 旧缓冲区
0,
new_buffer, // 新缓冲区
0,
buffer->total_size // 复制全部旧数据
);
// ========================================
// 5. 等待GPU完成复制
// ========================================
vkDeviceWaitIdle(context->device.logical_device);
// ========================================
// 6. 销毁旧缓冲区和内存
// ========================================
if (buffer->memory) {
vkFreeMemory(context->device.logical_device, buffer->memory, context->allocator);
}
if (buffer->handle) {
vkDestroyBuffer(context->device.logical_device, buffer->handle, context->allocator);
}
// ========================================
// 7. 更新缓冲区属性
// ========================================
buffer->total_size = new_size;
buffer->memory = new_memory;
buffer->handle = new_buffer;
return true;
}
⚠️ 性能警告:
- 调整大小开销很大(创建+复制+销毁)
- 尽量预分配足够大的缓冲区
- 或使用动态缓冲区策略(多个小缓冲区)
步骤7:销毁缓冲区
/**
* @brief 销毁缓冲区
*
* 必须先销毁缓冲区对象,再释放内存
*/
void vulkan_buffer_destroy(vulkan_context* context, vulkan_buffer* buffer) {
if (buffer->memory) {
vkFreeMemory(
context->device.logical_device,
buffer->memory,
context->allocator
);
buffer->memory = 0;
}
if (buffer->handle) {
vkDestroyBuffer(
context->device.logical_device,
buffer->handle,
context->allocator
);
buffer->handle = 0;
}
buffer->total_size = 0;
buffer->usage = 0;
buffer->is_locked = false;
}
🎮 顶点缓冲区与索引缓冲区
顶点缓冲区
顶点缓冲区存储模型的顶点数据(位置、法线、UV等)。
// 示例:创建顶点缓冲区的完整流程
// 顶点数据(三角形)
vertex_3d vertices[] = {
{{-0.5f, -0.5f, 0.0f}}, // 左下
{{ 0.5f, -0.5f, 0.0f}}, // 右下
{{ 0.0f, 0.5f, 0.0f}} // 顶部
};
// ========================================
// 步骤1:创建暂存缓冲区(HOST_VISIBLE)
// ========================================
vulkan_buffer staging;
vulkan_buffer_create(
context,
sizeof(vertices),
VK_BUFFER_USAGE_TRANSFER_SRC_BIT, // 作为传输源
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
true,
&staging
);
// ========================================
// 步骤2:加载数据到暂存缓冲区
// ========================================
vulkan_buffer_load_data(context, &staging, 0, sizeof(vertices), 0, vertices);
// ========================================
// 步骤3:创建设备本地顶点缓冲区
// ========================================
vulkan_buffer vertex_buffer;
vulkan_buffer_create(
context,
sizeof(vertices),
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, // 顶点缓冲区 + 传输目标
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, // GPU专用内存
true,
&vertex_buffer
);
// ========================================
// 步骤4:从暂存缓冲区复制到顶点缓冲区
// ========================================
vulkan_buffer_copy_to(
context,
context->device.graphics_command_pool,
0,
context->device.graphics_queue,
staging.handle,
0,
vertex_buffer.handle,
0,
sizeof(vertices)
);
// ========================================
// 步骤5:清理暂存缓冲区
// ========================================
vulkan_buffer_destroy(context, &staging);
索引缓冲区
索引缓冲区存储顶点索引,允许顶点重用,节省内存。
不使用索引(重复顶点):
V0, V1, V2 V3, V4, V5
└─三角形1─┘ └─三角形2─┘
需要6个顶点
使用索引(顶点重用):
顶点: V0, V1, V2, V3
索引: [0, 1, 2, 1, 3, 2]
└三角形1┘ └三角形2┘
只需4个顶点!
// 示例:四边形(两个三角形)
// 顶点数据
vertex_3d vertices[] = {
{{-0.5f, -0.5f, 0.0f}}, // 0: 左下
{{ 0.5f, -0.5f, 0.0f}}, // 1: 右下
{{ 0.5f, 0.5f, 0.0f}}, // 2: 右上
{{-0.5f, 0.5f, 0.0f}} // 3: 左上
};
// 索引数据(两个三角形)
u32 indices[] = {
0, 1, 2, // 第一个三角形
2, 3, 0 // 第二个三角形
};
// 创建索引缓冲区(流程与顶点缓冲区相同)
vulkan_buffer index_buffer;
vulkan_buffer_create(
context,
sizeof(indices),
VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
true,
&index_buffer
);
// ... 使用暂存缓冲区传输数据 ...
在后端集成
在 vulkan_context 中添加缓冲区(已在类型定义中):
typedef struct vulkan_context {
// ... 其他成员 ...
/// 对象顶点缓冲区
vulkan_buffer object_vertex_buffer;
/// 对象索引缓冲区
vulkan_buffer object_index_buffer;
// ... 其他成员 ...
} vulkan_context;
在 vulkan_backend.c 中初始化:
// 在 vulkan_renderer_backend_initialize() 中
// TODO: 临时测试数据(下一个教程会实际使用)
const u64 vertex_buffer_size = sizeof(vertex_3d) * 1024 * 1024; // 1MB
if (!vulkan_buffer_create(
&context,
vertex_buffer_size,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
true,
&context.object_vertex_buffer)) {
KERROR("Error creating vertex buffer.");
return false;
}
const u64 index_buffer_size = sizeof(u32) * 1024 * 1024; // 1MB
if (!vulkan_buffer_create(
&context,
index_buffer_size,
VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
true,
&context.object_index_buffer)) {
KERROR("Error creating index buffer.");
return false;
}
KINFO("Buffers created successfully.");
🏗️ 架构图解
缓冲区创建流程
graph TD
A[开始] --> B[创建VkBuffer]
B --> C[查询内存需求]
C --> D[选择内存类型]
D --> E{内存类型找到?}
E -->|否| F[错误:不支持的内存类型]
E -->|是| G[分配VkDeviceMemory]
G --> H{分配成功?}
H -->|否| I[错误:内存不足]
H -->|是| J[绑定缓冲区到内存]
J --> K[完成]
style A fill:#e1f5ff
style K fill:#e8f5e9
style F fill:#ffe1e1
style I fill:#ffe1e1
暂存缓冲区传输流程
┌─────────────────────────────────────────────────────┐
│ 第1步:CPU准备数据 │
│ ┌─────────────┐ │
│ │ CPU内存 │ (vertex_3d vertices[]) │
│ └─────────────┘ │
└──────────────────┬──────────────────────────────────┘
│ vulkan_buffer_load_data()
▼
┌─────────────────────────────────────────────────────┐
│ 第2步:暂存缓冲区 │
│ ┌─────────────────────────────────────────┐ │
│ │ HOST_VISIBLE | HOST_COHERENT │ │
│ │ TRANSFER_SRC_BIT │ │
│ └─────────────────────────────────────────┘ │
│ (系统RAM或GPU BAR - CPU可直接写入) │
└──────────────────┬──────────────────────────────────┘
│ vkCmdCopyBuffer()
│ (GPU DMA操作)
▼
┌─────────────────────────────────────────────────────┐
│ 第3步:设备本地缓冲区 │
│ ┌─────────────────────────────────────────┐ │
│ │ DEVICE_LOCAL │ │
│ │ VERTEX_BUFFER_BIT | TRANSFER_DST_BIT │ │
│ └─────────────────────────────────────────┘ │
│ (GPU VRAM - 极快访问) │
└──────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 第4步:渲染使用 │
│ vkCmdBindVertexBuffers() │
│ vkCmdDraw() │
└─────────────────────────────────────────────────────┘
💡 最佳实践
✅ 内存管理最佳实践
// ✅ 好的做法:复用大缓冲区(子分配)
vulkan_buffer large_buffer;
vulkan_buffer_create(context, 10 * 1024 * 1024, ...); // 10MB
// 手动管理偏移量
u64 offset = 0;
bind_mesh_1_to_offset(large_buffer, offset);
offset += mesh_1_size;
bind_mesh_2_to_offset(large_buffer, offset);
// ✅ 好的做法:批量传输
vulkan_buffer staging;
// 加载所有数据到暂存缓冲区
// 一次性复制到GPU
// ✅ 好的做法:异步传输
// 使用transfer队列,不阻塞graphics队列
vkCmdCopyBuffer(..., transfer_queue_buffer);
// ❌ 坏的做法:为每个小对象创建缓冲区
for (int i = 0; i < 1000; i++) {
vulkan_buffer tiny_buffer;
vulkan_buffer_create(context, 16, ...); // 过多的分配!
}
// ❌ 坏的做法:频繁调整大小
while (adding_data) {
vulkan_buffer_resize(...); // 每次调整都重新分配!
}
// ❌ 坏的做法:忘记清理暂存缓冲区
vulkan_buffer staging;
// ... 使用暂存缓冲区 ...
// 忘记 vulkan_buffer_destroy(&staging); ← 内存泄漏!
✅ 性能优化建议
- 预分配:启动时分配大缓冲区,避免运行时分配
- 批量传输:合并多个小传输为一次大传输
- Transfer队列:使用专用传输队列,并行传输和渲染
- 持久映射:对于频繁更新的缓冲区,保持映射状态
❓ 常见问题
Q1: 为什么要分离缓冲区和内存?答:这是Vulkan的核心设计哲学 - 灵活性。
好处:
- 可以将多个小缓冲区绑定到一大块内存(减少分配次数)
- 可以自由选择内存类型
- 可以实现自定义内存分配器
示例:
// 分配1GB内存
VkDeviceMemory large_memory = allocate_1gb();
// 绑定100个小缓冲区到这块内存
for (int i = 0; i < 100; i++) {
vkBindBufferMemory(buffer[i], large_memory, i * buffer_size);
}
这比调用100次 vkAllocateMemory() 高效得多!
答:当目标缓冲区是 DEVICE_LOCAL 时。
需要暂存缓冲区:
// 目标:DEVICE_LOCAL顶点缓冲区(最快)
vulkan_buffer vertex_buffer; // DEVICE_LOCAL
vulkan_buffer staging; // HOST_VISIBLE
// CPU → staging → vertex_buffer
不需要暂存缓冲区:
// 目标:HOST_VISIBLE Uniform缓冲区(频繁更新)
vulkan_buffer ubo; // HOST_VISIBLE | HOST_COHERENT
// CPU直接写入ubo(每帧更新)
void* data = vulkan_buffer_lock_memory(...);
memcpy(data, &mvp_matrix, sizeof(mvp_matrix));
vulkan_buffer_unlock_memory(...);
权衡:
- 暂存缓冲区:初始化慢,渲染极快(适合静态数据)
- 直接映射:初始化快,渲染稍慢(适合动态数据)
答:内存一致性(Coherence)指CPU和GPU看到的内存是否自动同步。
HOST_COHERENT:
// 自动同步,无需手动操作
void* data = vkMapMemory(...);
memcpy(data, new_data, size);
vkUnmapMemory(...);
// GPU立即看到新数据 ✅
非HOST_COHERENT:
// 需要手动刷新
void* data = vkMapMemory(...);
memcpy(data, new_data, size);
VkMappedMemoryRange range = {...};
vkFlushMappedMemoryRanges(device, 1, &range); // 手动刷新!
vkUnmapMemory(...);
权衡:
HOST_COHERENT:方便,但可能稍慢- 非
HOST_COHERENT:更快,但需要手动管理
大多数情况使用 HOST_COHERENT 即可。
答:取决于使用场景。
静态几何体:
// 预先计算所需大小
u64 size = model_vertex_count * sizeof(vertex_3d);
动态缓冲区(粒子、UI等):
// 预分配最大可能大小
u64 max_particles = 10000;
u64 size = max_particles * sizeof(particle_vertex);
Uniform缓冲区:
// 小,频繁更新
u64 size = sizeof(mvp_matrix); // 64字节
经验法则:
- 宁可稍大,避免调整大小
- 但也别浪费(如分配1GB只用1KB)
- 典型顶点缓冲区:1-10MB
答:取决于实现方式。
阻塞版本(当前实现):
vkQueueWaitIdle(queue); // 等待队列空闲
vkCmdCopyBuffer(...);
vkQueueSubmit(...);
vkDeviceWaitIdle(...); // 等待复制完成
这会阻塞,适合初始化阶段。
非阻塞版本(高级):
// 使用fence异步等待
VkFence fence = create_fence();
vkCmdCopyBuffer(...);
vkQueueSubmit(..., fence);
// 继续其他工作...
// 稍后检查
if (vkGetFenceStatus(device, fence) == VK_SUCCESS) {
// 复制完成!
}
Transfer队列(最优):
// 在transfer队列复制,graphics队列继续渲染
vkQueueSubmit(transfer_queue, copy_commands, ...);
vkQueueSubmit(graphics_queue, render_commands, ...);
🐛 故障排查
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| “memory type index not found” | 请求的内存属性不支持 | 检查物理设备支持的内存类型,降低要求 |
| 内存分配失败 | GPU内存不足 | 减少缓冲区大小,复用内存 |
| 渲染数据错误 | 数据未正确传输 | 验证暂存缓冲区流程,检查偏移量 |
| 崩溃:访问映射内存 | 映射了DEVICE_LOCAL内存 | 只映射HOST_VISIBLE内存 |
| 数据更新不生效 | 非COHERENT内存未flush | 使用HOST_COHERENT或手动flush |
| 性能下降 | 过多的小缓冲区 | 合并为大缓冲区,使用子分配 |
| 验证层警告:内存泄漏 | 忘记销毁缓冲区 | 确保所有缓冲区都destroy |
📝 练习题
基础练习
练习1:动态顶点缓冲区
实现一个可以动态添加顶点的缓冲区:
typedef struct dynamic_vertex_buffer {
vulkan_buffer buffer;
u64 capacity; // 总容量
u64 used; // 已使用
} dynamic_vertex_buffer;
b8 dynamic_vb_create(vulkan_context* context, u64 initial_capacity, dynamic_vertex_buffer* out_dvb);
b8 dynamic_vb_add_vertices(vulkan_context* context, dynamic_vertex_buffer* dvb, vertex_3d* vertices, u32 count);
💡 参考答案
b8 dynamic_vb_create(vulkan_context* context, u64 initial_capacity, dynamic_vertex_buffer* out_dvb) {
out_dvb->capacity = initial_capacity;
out_dvb->used = 0;
return vulkan_buffer_create(
context,
initial_capacity,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
true,
&out_dvb->buffer
);
}
b8 dynamic_vb_add_vertices(vulkan_context* context, dynamic_vertex_buffer* dvb, vertex_3d* vertices, u32 count) {
u64 size_needed = count * sizeof(vertex_3d);
// 检查是否需要扩容
if (dvb->used + size_needed > dvb->capacity) {
u64 new_capacity = dvb->capacity * 2; // 双倍扩容
if (!vulkan_buffer_resize(
context,
new_capacity,
&dvb->buffer,
context->device.graphics_queue,
context->device.graphics_command_pool)) {
return false;
}
dvb->capacity = new_capacity;
}
// 使用暂存缓冲区传输
vulkan_buffer staging;
vulkan_buffer_create(
context,
size_needed,
VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
true,
&staging
);
vulkan_buffer_load_data(context, &staging, 0, size_needed, 0, vertices);
vulkan_buffer_copy_to(
context,
context->device.graphics_command_pool,
0,
context->device.graphics_queue,
staging.handle,
0,
dvb->buffer.handle,
dvb->used, // 追加到已使用的末尾
size_needed
);
vulkan_buffer_destroy(context, &staging);
dvb->used += size_needed;
return true;
}
进阶挑战
挑战1:内存池分配器
实现一个简单的缓冲区内存池:
typedef struct buffer_pool {
VkDeviceMemory memory;
u64 total_size;
u64 used;
// TODO: 空闲块链表
} buffer_pool;
b8 buffer_pool_create(vulkan_context* context, u64 size, u32 memory_flags, buffer_pool* out_pool);
b8 buffer_pool_allocate(vulkan_context* context, buffer_pool* pool, u64 size, vulkan_buffer* out_buffer);
void buffer_pool_free(buffer_pool* pool, vulkan_buffer* buffer);
💡 提示
关键点:
- 创建一大块
VkDeviceMemory - 维护空闲块列表(偏移量 + 大小)
- 分配时从空闲列表找到合适块
- 释放时将块归还到空闲列表
- 合并相邻的空闲块(避免碎片)
这是一个小型的内存分配器实现!
挑战2:异步传输系统
实现一个异步的缓冲区传输系统:
typedef struct transfer_request {
vulkan_buffer* dest;
void* data;
u64 size;
b8 completed;
} transfer_request;
typedef struct async_transfer_system {
VkQueue transfer_queue;
VkCommandPool transfer_pool;
darray transfer_requests; // 待处理请求队列
} async_transfer_system;
void async_transfer_init(vulkan_context* context, async_transfer_system* system);
void async_transfer_submit(async_transfer_system* system, vulkan_buffer* dest, void* data, u64 size);
void async_transfer_update(async_transfer_system* system); // 每帧检查完成状态
💡 提示
关键点:
- 使用transfer队列而非graphics队列
- 使用Fence跟踪每个传输的完成状态
submit只是将请求加入队列update检查fence状态,标记完成的请求- 可以并行多个传输(多个fence)
高级:使用信号量同步transfer队列和graphics队列。
📚 参考资料
🔗 官方文档
- Vulkan内存管理 - https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap11.html
- VkBuffer - https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkBuffer.html
- 内存类型和堆 - https://registry.khronos.org/vulkan/specs/1.3-extensions/html/chap10.html#memory
📖 深入学习
- Vulkan Memory Allocator (VMA) - https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator
- Memory Management Strategies - https://developer.nvidia.com/vulkan-memory-management
- Staging Buffers Best Practices - https://gpuopen.com/learn/vulkan-barriers-explained/
📂 Kohi引擎资源
- 源代码 - https://github.com/travisvroman/kohi
- Commit 877eb02 - 查看本教程对应的提交
🎯 本章总结
✅ 你已经掌握了
┌─────────────────────────────────────────────────────────┐
│ ✓ Vulkan缓冲区的核心概念和工作原理 │
│ ✓ 缓冲区与内存的分离设计 │
│ ✓ 不同内存类型的选择策略 │
│ ✓ 内存映射和数据传输方法 │
│ ✓ 暂存缓冲区的两步传输流程 │
│ ✓ 缓冲区间的GPU复制操作 │
│ ✓ 顶点缓冲区和索引缓冲区的创建 │
│ ✓ 缓冲区的动态调整大小 │
└─────────────────────────────────────────────────────────┘
🎯 核心要点
- 缓冲区 ≠ 内存 - 它们是分离的,需要手动绑定
- 内存类型很重要 -
DEVICE_LOCAL最快,HOST_VISIBLE可映射 - 暂存缓冲区 - 传输到DEVICE_LOCAL内存的桥梁
- GPU复制 - 使用GPU DMA比CPU复制快得多
- 内存管理 - 预分配、复用、批量传输
🚀 下一步学习
| 教程 | 主题 | 依赖 |
|---|---|---|
| 教程 23 | 绘制到屏幕 | 本教程 + 教程21 |
| 教程 24 | 描述符集和UBO | 教程 23 |
| 教程 25 | 纹理加载 | 本教程 |
🎉 恭喜你完成了Vulkan缓冲区系统的学习!
现在你已经掌握了GPU内存管理的核心技术。
下一步:
📖 关注公众号

关注我,领取章节视频教程
💖 支持作者
📅 最后更新:2025-11-21
✍️ 作者:上手实验室
1393

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



