教程 22 - Vulkan 缓冲区系统

上一篇:教程 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缓冲区
位置系统RAMGPU内存
访问速度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_LOCALGPU 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;
}

🔍 关键步骤解析

  1. 创建缓冲区对象 - 定义大小和用途,但还没有内存
  2. 查询内存需求 - Vulkan告诉我们需要多少内存、对齐要求
  3. 查找内存类型 - 根据需求标志找到合适的内存堆
  4. 分配内存 - 实际分配GPU内存
  5. 绑定 - 将缓冲区对象关联到内存

步骤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,
        &copy_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); ← 内存泄漏!

✅ 性能优化建议

  1. 预分配:启动时分配大缓冲区,避免运行时分配
  2. 批量传输:合并多个小传输为一次大传输
  3. Transfer队列:使用专用传输队列,并行传输和渲染
  4. 持久映射:对于频繁更新的缓冲区,保持映射状态

❓ 常见问题

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() 高效得多!

Q2: 什么时候需要暂存缓冲区?

:当目标缓冲区是 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(...);

权衡

  • 暂存缓冲区:初始化慢,渲染极快(适合静态数据)
  • 直接映射:初始化快,渲染稍慢(适合动态数据)
Q3: HOST_COHERENT是什么意思?

内存一致性(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 即可。

Q4: 如何选择缓冲区大小?

:取决于使用场景。

静态几何体

// 预先计算所需大小
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
Q5: 缓冲区复制会阻塞渲染吗?

:取决于实现方式。

阻塞版本(当前实现):

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);
💡 提示

关键点:

  1. 创建一大块 VkDeviceMemory
  2. 维护空闲块列表(偏移量 + 大小)
  3. 分配时从空闲列表找到合适块
  4. 释放时将块归还到空闲列表
  5. 合并相邻的空闲块(避免碎片)

这是一个小型的内存分配器实现!


挑战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);  // 每帧检查完成状态
💡 提示

关键点:

  1. 使用transfer队列而非graphics队列
  2. 使用Fence跟踪每个传输的完成状态
  3. submit 只是将请求加入队列
  4. update 检查fence状态,标记完成的请求
  5. 可以并行多个传输(多个fence)

高级:使用信号量同步transfer队列和graphics队列。


📚 参考资料

🔗 官方文档

📖 深入学习

📂 Kohi引擎资源


🎯 本章总结

✅ 你已经掌握了

┌─────────────────────────────────────────────────────────┐
│  ✓ Vulkan缓冲区的核心概念和工作原理                       │
│  ✓ 缓冲区与内存的分离设计                                │
│  ✓ 不同内存类型的选择策略                                │
│  ✓ 内存映射和数据传输方法                                │
│  ✓ 暂存缓冲区的两步传输流程                              │
│  ✓ 缓冲区间的GPU复制操作                                 │
│  ✓ 顶点缓冲区和索引缓冲区的创建                          │
│  ✓ 缓冲区的动态调整大小                                  │
└─────────────────────────────────────────────────────────┘

🎯 核心要点

  1. 缓冲区 ≠ 内存 - 它们是分离的,需要手动绑定
  2. 内存类型很重要 - DEVICE_LOCAL 最快,HOST_VISIBLE 可映射
  3. 暂存缓冲区 - 传输到DEVICE_LOCAL内存的桥梁
  4. GPU复制 - 使用GPU DMA比CPU复制快得多
  5. 内存管理 - 预分配、复用、批量传输

🚀 下一步学习

教程主题依赖
教程 23绘制到屏幕本教程 + 教程21
教程 24描述符集和UBO教程 23
教程 25纹理加载本教程

🎉 恭喜你完成了Vulkan缓冲区系统的学习!

现在你已经掌握了GPU内存管理的核心技术。

下一步


📖 关注公众号

在这里插入图片描述

关注我,领取章节视频教程


💖 支持作者


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值