Vulkan 学习笔记(十七)从帧缓冲到命令缓冲
各位图形学冒险家们,欢迎回到Vulkan教程的"画三角形"系列!前几章我们搭建了各种基础设施,配置了图形管线,但屏幕依旧漆黑一片——就像程序员的社交生活一样。别担心,今天我们要揭开Vulkan渲染的核心秘密:帧缓冲和命令缓冲!准备见证你的三角形首次在屏幕上亮相的神奇时刻!
🎯 帧缓冲(Framebuffers) —— 为三角形准备画布
在过去的章节中,我们无数次提到"帧缓冲"这个词,甚至在渲染通道中配置了它的格式,但就像程序员承诺的"明天一定重构代码"一样,我们一直没真正创建它!
什么是帧缓冲?
简单来说,**帧缓冲(Framebuffer)**就是Vulkan用来存储渲染结果的"画布"。它把之前创建的VkImageView对象(也就是交换链图像的视图)包装起来,让渲染通道能够使用它们。关键点来了:我们需要为交换链中的每个图像都创建一个帧缓冲,因为Vulkan每次渲染时使用的图像可能是不同的!
创建帧缓冲容器
首先,在类中添加一个成员变量来存储所有的帧缓冲:
std::vector<VkFramebuffer> swapChainFramebuffers;
然后在initVulkan函数中,紧接在图形管线创建之后调用新函数createFramebuffers:
void initVulkan(){
createInstance();
setupDebugMessenger();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
createSwapChain();
createImageViews();
createRenderPass();
createGraphicsPipeline();
createFramebuffers(); // 就是这里!
}
实现帧缓冲创建
现在,让我们真正创建这些帧缓冲:
void createFramebuffers(){
// 1. 调整容器大小以容纳所有帧缓冲
swapChainFramebuffers.resize(swapChainImageViews.size());
// 2. 为每个图像视图创建对应的帧缓冲
for (size_t i = 0; i < swapChainImageViews.size(); i++) {
// 指定要使用的图像视图(这里只有一个颜色附件)
VkImageView attachments[] = {
swapChainImageViews[i]
};
VkFramebufferCreateInfo framebufferInfo{};
framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebufferInfo.renderPass = renderPass; // 关联之前创建的渲染通道
framebufferInfo.attachmentCount = 1; // 附件数量
framebufferInfo.pAttachments = attachments; // 附件数组
framebufferInfo.width = swapChainExtent.width; // 宽度要匹配交换链
framebufferInfo.height = swapChainExtent.height; // 高度也要匹配
framebufferInfo.layers = 1; // 图层数,我们只需要1层
// 3. 真正创建帧缓冲
if (vkCreateFramebuffer(device, &framebufferInfo, nullptr, &swapChainFramebuffers[i]) != VK_SUCCESS) {
throw std::runtime_error("failed to create framebuffer!"); // 创建失败?扔个异常!
}
}
}
专业小贴士:帧缓冲的创建相当直接,但有几个关键点:
- •
layers参数指图像数组中的层数,我们的交换链图像是单层的
清理工作
别忘了在程序退出时清理这些帧缓冲(在图像视图和渲染通道之前销毁):
void cleanup(){
for (auto framebuffer : swapChainFramebuffers) {
vkDestroyFramebuffer(device, framebuffer, nullptr);
}
// ... 其他清理代码
}
🎉 里程碑时刻:现在我们拥有了渲染所需的所有对象!接下来,让我们进入Vulkan的"灵魂"部分——命令缓冲!
🚀 命令缓冲(Command buffers) —— Vulkan的"操作手册"
在Vulkan中,没有直接的函数调用来绘制图形或进行内存传输。这就像你不能直接对厨师说"给我做顿饭",而是要先写好详细的菜谱,然后交给厨师执行。**命令缓冲(Command Buffer)**就是这份"菜谱"!
为什么需要命令缓冲?
Vulkan的设计哲学是:把所有命令一次性提交,这样驱动程序可以更高效地处理它们。这就像是把购物清单一次性给超市员工,而不是让他们来回跑100次。此外,命令记录可以在多线程中进行,提高效率。
创建命令池
在创建命令缓冲之前,我们需要先创建命令池(Command Pool)。命令池管理用于存储命令缓冲的内存。
在类中添加命令池成员变量:
VkCommandPool commandPool;
然后在initVulkan中调用createCommandPool(在帧缓冲创建之后):
void initVulkan(){
// ... 之前的代码
createFramebuffers();
createCommandPool(); // 新增这行
}
实现createCommandPool函数:
void createCommandPool(){
// 1. 获取图形队列族索引
QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice);
// 2. 配置命令池创建信息
VkCommandPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; // 允许重置单个命令缓冲
poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value(); // 使用图形队列族
// 3. 创建命令池
if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) {
throw std::runtime_error("failed to create command pool!");
}
}
命令池标志详解:
- •
VK_COMMAND_POOL_CREATE_TRANSIENT_BIT:提示命令缓冲会频繁重新录制(可能改变内存分配行为) - •
VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT:允许单独重置命令缓冲(我们每帧都要重录,所以需要这个)
分配命令缓冲
现在创建一个命令缓冲:
VkCommandBuffer commandBuffer; // 类成员变量
在initVulkan中调用createCommandBuffer:
void initVulkan(){
// ... 之前的代码
createCommandPool();
createCommandBuffer(); // 新增这行
}
实现createCommandBuffer函数:
void createCommandBuffer(){
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool; // 指定命令池
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; // 主命令缓冲
allocInfo.commandBufferCount = 1; // 只分配一个
if (vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer) != VK_SUCCESS) {
throw std::runtime_error("failed to allocate command buffers!");
}
}
命令缓冲级别:
- •
VK_COMMAND_BUFFER_LEVEL_PRIMARY:可以直接提交到队列执行,不能被其他命令缓冲调用 - •
VK_COMMAND_BUFFER_LEVEL_SECONDARY:不能直接提交,但可以被主命令缓冲调用(适合复用操作)
录制命令缓冲
重头戏来了!现在我们要录制实际的绘制命令。创建一个函数来记录命令:
void recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex){
// 1. 开始录制命令缓冲
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = 0; // 无特殊标志
beginInfo.pInheritanceInfo = nullptr; // 仅用于次级命令缓冲
if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) {
throw std::runtime_error("failed to begin recording command buffer!");
}
// 2. 开始渲染通道
VkRenderPassBeginInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = renderPass; // 指定渲染通道
renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex]; // 指定帧缓冲
// 设置渲染区域
renderPassInfo.renderArea.offset = {0, 0};
renderPassInfo.renderArea.extent = swapChainExtent;
// 设置清除颜色(黑色)
VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}};
renderPassInfo.clearValueCount = 1;
renderPassInfo.pClearValues = &clearColor;
// 真正开始渲染通道
vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
// 3. 绑定图形管线
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
// 4. 设置动态状态(视口和裁剪区域)
VkViewport viewport{};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = static_cast<float>(swapChainExtent.width);
viewport.height = static_cast<float>(swapChainExtent.height);
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
VkRect2D scissor{};
scissor.offset = {0, 0};
scissor.extent = swapChainExtent;
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
// 5. 发出绘制命令!
vkCmdDraw(commandBuffer, 3, 1, 0, 0); // 3个顶点,1个实例,从顶点0开始,从实例0开始
// 6. 结束渲染通道
vkCmdEndRenderPass(commandBuffer);
// 7. 结束录制
if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {
throw std::runtime_error("failed to record command buffer!");
}
}
命令缓冲录制关键点:
- • 以
vkCmd开头的函数都是录制命令,不会立即执行 - •
vertexCount: 要绘制的顶点数(3个,三角形) - •
instanceCount: 实例数量(1,不使用实例化) - •
firstVertex: 顶点缓冲中的起始索引(0) - •
firstInstance: 实例的起始索引(0)
清理命令池
命令缓冲会在命令池销毁时自动释放,所以我们只需在cleanup中销毁命令池:
void cleanup(){
vkDestroyCommandPool(device, commandPool, nullptr);
// ... 其他清理代码
}
但屏幕依然漆黑?别着急!在下一篇文章中,我们将把所有这些组件连接起来,创建主循环,真正将三角形显示在屏幕上。就像程序员调试代码时的耐心一样,我们离成功只差最后一步!
技术总结:Vulkan的复杂性在于其显式性。帧缓冲和命令缓冲是Vulkan渲染的核心概念,理解它们对掌握Vulkan至关重要。记住:Vulkan不会为你做任何假设,每一步都需要你明确指定。
下期预告:《渲染与呈现:让三角形真正出现在屏幕上!》我们将实现主循环,处理交换链图像获取、命令提交和呈现,见证Vulkan图形编程的第一次成功!
[帧缓冲源码]https://vulkan-tutorial.com/code/13_framebuffers.cpp
[命令缓冲源码]https://vulkan-tutorial.com/code/14_command_buffers.cpp