一、暂存缓冲
当前顶点缓冲功能正常,但 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, ©Region);
// 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