Vulkan学习笔记9—暂存缓冲与索引缓冲

一、暂存缓冲

当前顶点缓冲功能正常,但 CPU 可访问的内存类型可能并非显卡读取的最优选择。最优内存需具备V K_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 标志,且在专用显卡上通常无法被 CPU 访问。本节将创建两个顶点缓冲区:

  • 暂存缓冲区:位于 CPU 可访问内存,用于从顶点数组上传数据;
  • 顶点缓冲区:位于设备本地内存,为显卡最优存储位置。

最后通过缓冲区复制命令,将数据从暂存缓冲区迁移至顶点缓冲区。

本节将创建多个缓冲区,提取缓冲区创建逻辑为辅助函数。创建一个新函数 createBuffer,并将 createVertexBuffer 中的代码(映射除外)移动到其中。

/**
 * 创建并配置VkBuffer及关联的设备内存
 * 
 * @param physicalDevice 物理设备句柄,用于内存类型查找
 * @param device 逻辑设备句柄,用于资源创建
 * @param size 缓冲区大小(字节)
 * @param usage 缓冲区使用标志(如VK_BUFFER_USAGE_VERTEX_BUFFER_BIT)
 * @param properties 内存属性标志(如VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT)
 * @param[out] buffer 输出的缓冲区对象
 * @param[out] bufferMemory 输出的关联设备内存
 * 
 * @throws std::runtime_error 缓冲区创建或内存分配失败时抛出异常
 * 
 * 功能说明:
 * 1. 初始化VkBufferCreateInfo结构体,配置缓冲区大小、用途和共享模式
 * 2. 创建VkBuffer对象
 * 3. 查询缓冲区的内存需求(大小、对齐要求、内存类型位掩码)
 * 4. 通过findMemoryType函数查找符合属性要求的内存类型索引
 * 5. 分配设备内存并绑定到缓冲区
 * 
 * 注意:
 * - 该函数不包含内存映射操作(用于数据上传)
 * - 内存分配遵循最佳实践,使用专用分配而非内存池
 */
void createBuffer(VkPhysicalDevice physicalDevice,
                  VkDevice device,
                  VkDeviceSize size,
                  VkBufferUsageFlags usage,
                  VkMemoryPropertyFlags properties,
                  VkBuffer& buffer,
                  VkDeviceMemory& bufferMemory) {
    // 配置缓冲区创建信息
    VkBufferCreateInfo bufferInfo{};
    bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
    bufferInfo.size = size;           // 缓冲区大小(字节)
    bufferInfo.usage = usage;         // 用途标志(顶点缓冲/索引缓冲/统一缓冲等)
    bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; // 不共享(单队列族访问)

    // 创建缓冲区对象
    if (vkCreateBuffer(device, &bufferInfo, nullptr, &buffer) != VK_SUCCESS) {
        throw std::runtime_error("创建缓冲失败!");
    }

    // 查询缓冲区的内存需求
    VkMemoryRequirements memRequirements;
    vkGetBufferMemoryRequirements(device, buffer, &memRequirements);

    // 配置内存分配信息
    VkMemoryAllocateInfo allocInfo{};
    allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
    allocInfo.allocationSize = memRequirements.size; // 按需求大小分配
    // 查找符合属性要求的内存类型索引
    allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties, physicalDevice);

    // 分配设备内存
    if (vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) {
        throw std::runtime_error("分配内存失败!");
    }

    // 将分配的内存绑定到缓冲区
    vkBindBufferMemory(device, buffer, bufferMemory, 0);
}

编写一个函数,用于将内容从一个缓冲区复制到另一个缓冲区,称为 copyBuffer

/**
 * 执行VkBuffer之间的内存复制操作(同步方式)
 * 
 * @param srcBuffer 源缓冲区
 * @param dstBuffer 目标缓冲区
 * @param size 复制数据大小(字节)
 * @param vkcontext Vulkan上下文,包含设备、命令池和队列信息
 * 
 * 功能说明:
 * 1. 从命令池中分配一个主命令缓冲区
 * 2. 录制缓冲区复制命令
 * 3. 提交命令到图形队列并等待完成(同步操作)
 * 
 * 工作流程:
 * 1. 分配临时命令缓冲区(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT)
 * 2. 开始命令缓冲区录制
 * 3. 设置VkBufferCopy区域(默认为从源起始到目标起始的整块复制)
 * 4. 记录vkCmdCopyBuffer命令
 * 5. 结束命令缓冲区录制
 * 6. 创建提交信息并提交到图形队列
 * 7. 等待队列执行完成(vkQueueWaitIdle)
 * 
 * 注意:
 * - 此函数采用同步方式(等待队列空闲),适合初始化阶段使用
 * - 对于频繁复制操作,建议使用异步方式并结合 fences/semaphores
 * - 源和目标缓冲区必须已正确创建并绑定内存
 * - 复制操作受物理设备内存属性限制(如非HOST_VISIBLE内存无法直接CPU写入)
 */
void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size, VkContext* vkcontext) {
    // 1. 分配临时命令缓冲区(用于单次提交的主命令缓冲区)
    VkCommandBufferAllocateInfo allocInfo{};
    allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
    allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; // 可直接提交到队列
    allocInfo.commandPool = vkcontext->commandPool;    // 使用预创建的命令池
    allocInfo.commandBufferCount = 1;                  // 分配1个命令缓冲区

    VkCommandBuffer commandBuffer;
    vkAllocateCommandBuffers(vkcontext->device, &allocInfo, &commandBuffer);

    // 2. 开始命令缓冲区录制(单次提交模式)
    VkCommandBufferBeginInfo beginInfo{};
    beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; // 单次使用后重置

    vkBeginCommandBuffer(commandBuffer, &beginInfo);

    // 3. 配置缓冲区复制区域(默认从源起始到目标起始的整块复制)
    VkBufferCopy copyRegion{};
    copyRegion.size = size; // 复制大小(字节)
    // 源和目标偏移默认为0(可设置非零值实现部分复制)

    // 4. 记录缓冲区复制命令
    vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, &copyRegion);

    // 5. 结束命令缓冲区录制
    vkEndCommandBuffer(commandBuffer);

    // 6. 提交命令到图形队列执行
    VkSubmitInfo submitInfo{};
    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = &commandBuffer;

    vkQueueSubmit(vkcontext->graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);

    // 7. 同步等待队列执行完成(确保复制操作完成)
    vkQueueWaitIdle(vkcontext->graphicsQueue);

    // 8. 可添加命令缓冲区释放逻辑(根据命令池管理策略)
    // 注:此处未显式释放命令缓冲区,依赖命令池重置机制
}

更新后的 顶点缓冲创建代码:

 // 创建顶点缓冲
{
    VkBuffer stagingBuffer;
    VkDeviceMemory stagingBufferMemory;

    createBuffer(vkcontext->physicalDevice, 
	vkcontext->device, 
	vkcontext->vertexBufferSize,
	VK_BUFFER_USAGE_TRANSFER_SRC_BIT, 
	VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, 
	stagingBuffer, 
	stagingBufferMemory
    );

    void* data;
    vkMapMemory(vkcontext->device, stagingBufferMemory, 0, vkcontext->vertexBufferSize, 0, &data);
    memcpy(data, vkcontext->vertexData, (size_t) vkcontext->vertexBufferSize);
    vkUnmapMemory(vkcontext->device, stagingBufferMemory);

    createBuffer(vkcontext->physicalDevice, 
	vkcontext->device, 
	vkcontext->vertexBufferSize,
	VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, 
	VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 
	vkcontext->vertexBuffer, 
	vkcontext->vertexBufferMemory);

    copyBuffer(stagingBuffer, vkcontext->vertexBuffer, vkcontext->vertexBufferSize, vkcontext);
    vkDestroyBuffer(vkcontext->device, stagingBuffer, nullptr);
    vkFreeMemory(vkcontext->device, stagingBufferMemory, nullptr);
}

重新编译运行检查验证层日志一切OK。

二、索引缓冲区

实际应用程序中渲染的 3D 网格通常会在多个三角形之间共享顶点。即使是绘制一个简单的矩形,也会发生这种情况。

索引缓冲区是指向顶点缓冲区的指针数组,可重新排列顶点数据,实现数据重用。以矩形为例,若顶点缓冲区含 4 个唯一顶点,索引缓冲区前 3 个索引定义右上角三角形,后 3 个索引定义左下角三角形,避免顶点重复存储。

VkContext 结构体新增成员:

struct VkContext {
    ...
    void* indicesData;
    uint32_t indicesNum{0};
    size_t indexBufferSize{0};
    VkBuffer indexBuffer;
    VkDeviceMemory indexBufferMemor
};

新增绘制矩形的类: HelloRect

// ----------HelloRect.h----------------
#pragma once
#include <vector>

#include "renderer/VkTypes.h"

using namespace renderer;

class HelloRect {
public:
    HelloRect();

    const std::vector<Vertex> vertices;
    const std::vector<uint16_t> indices;
};




// ----------HelloRect.cpp-----------------
#include "HelloRect.h"

HelloRect::HelloRect()
    : vertices({{{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}},
                {{0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}},
                {{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}},
                {{-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f}}})
    , indices({0, 1, 2, 2, 3, 0}) {}

紧接着顶点缓冲创建功能代码,新增创建索引缓冲:

// 创建索引缓冲
{
    VkBuffer stagingBuffer;
    VkDeviceMemory stagingBufferMemory;
     createBuffer(vkcontext->physicalDevice, 
	vkcontext->device, 
	vkcontext->indexBufferSize,
	VK_BUFFER_USAGE_TRANSFER_SRC_BIT, 
	VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, 
	stagingBuffer, 
	stagingBufferMemory
    );

    void* data;
    vkMapMemory(vkcontext->device, stagingBufferMemory, 0, vkcontext->indexBufferSize, 0, &data);
    memcpy(data, vkcontext->indicesData, vkcontext->vertexBufferSize);
    vkUnmapMemory(vkcontext->device, stagingBufferMemory);

    createBuffer(vkcontext->physicalDevice, 
	vkcontext->device, 
	vkcontext->indexBufferSize, 
	VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT, 
	VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 
	vkcontext->indexBuffer, 
	vkcontext->indexBufferMemory);

    copyBuffer(stagingBuffer, vkcontext->indexBuffer, vkcontext->indexBufferSize, vkcontext);
    vkDestroyBuffer(vkcontext->device, stagingBuffer, nullptr);
    vkFreeMemory(vkcontext->device, stagingBufferMemory, nullptr);
}

索引缓冲区应该在程序结束时清理,就像顶点缓冲区一样:

void vkClean() {
    cleanupSwapChain();

    vkDestroyBuffer(vkcontext->device, vkcontext->indexBuffer, nullptr);
    vkFreeMemory(vkcontext->device, vkcontext->indexBufferMemory, nullptr);

    vkDestroyBuffer(vkcontext->device, vkcontext->vertexBuffer, nullptr);
    vkFreeMemory(vkcontext->device, vkcontext->vertexBufferMemory, nullptr);
    ...
}

修改命令缓冲录制代码,绑定索引缓冲,调用索引绘制方法:

void recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex, VkContext* vkcontext) {
    VkCommandBufferBeginInfo beginInfo{};

    ...

    VkDeviceSize offsets[] = {0};
    vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vkcontext->vertexBuffer, offsets);
    vkCmdBindIndexBuffer(commandBuffer, vkcontext->indexBuffer, 0, VK_INDEX_TYPE_UINT16);

    vkCmdDrawIndexed(commandBuffer, vkcontext->indicesNum, 1, 0, 0, 0);
    vkCmdEndRenderPass(commandBuffer);
    if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {
	throw std::runtime_error("录制命令缓冲失败!");
    }
}

更新后完整的主函数:

#include <cstdlib>
#include <iostream>
#include <stdexcept>
#include <vector>
#include "renderer/VkRenderer.h"
#include "HelloRect.h"



using namespace renderer;

GLFWwindow* window;
const uint32_t WIDTH = 800;
const uint32_t HEIGHT = 600;

VkContext vkcontext{};


static void framebufferResizeCallback(GLFWwindow* window, int width, int height) {
    vkcontext.framebufferResized = true;
}

void initWindow() {
    glfwInit();
    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    window = glfwCreateWindow(WIDTH, HEIGHT, "Hello Vulkan", nullptr, nullptr);
    glfwSetFramebufferSizeCallback(window, framebufferResizeCallback);
}

int main() {
    initWindow();
    vkcontext.window = window;

    HelloRect app;

    vkcontext.vertexData = (void*)app.vertices.data();
    vkcontext.vertexNum = app.vertices.size();
    vkcontext.vertexBufferSize = app.vertices.size() * sizeof(Vertex);

    vkcontext.indicesData = (void*)app.indices.data();
    vkcontext.indicesNum = app.indices.size();
    vkcontext.indexBufferSize = app.indices.size() * sizeof(app.indices[0]);

    if (!vkInit(&vkcontext)) {
        throw std::runtime_error("Vulkan 初始化失败!");
    }

    try {
        while (!glfwWindowShouldClose(window)) {
            glfwPollEvents();
            vkRender(&vkcontext);
        }
        vkDeviceWaitIdle(vkcontext.device);
        vkClean(&vkcontext);
        glfwDestroyWindow(window);
        glfwTerminate();

    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

重新构建运行效果:

一切正常,验证层没有报错!

当前代码分支为: 07_stagingbuffer_indexbuffer_drawrect

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值