Hotdry.
systems-engineering

从零构建最小Vulkan游戏引擎:命令缓冲与信号量/栅栏同步

手把手实现Vulkan最小游戏引擎,聚焦instance/device/swapchain到command buffer/semaphore-fence sync,实现robust多帧渲染,避免撕裂与CPU stall。

在现代图形编程中,Vulkan API 以其低开销和高性能著称,但其显式控制特性对初学者而言陡峭的学习曲线是主要障碍。构建一个最小 Vulkan 游戏引擎,能从零跑通 instance、device、swapchain、graphics pipeline、command buffer 录制,直至 semaphore 和 fence 同步的完整渲染管线,不仅能验证核心概念,还能作为扩展复杂场景的基础。本文聚焦于 “small-engine-command-sync” 角度,强调命令缓冲录制与 robust 帧同步机制,提供可直接落地的 C++ 参数清单和监控要点,确保引擎在多帧飞行(frames in flight)下稳定运行,避免常见画面撕裂或 CPU 空转问题。

为什么需要最小引擎与命令同步?

Vulkan 不像 OpenGL 有隐式状态机,所有资源创建、命令提交和同步必须显式管理。一个最小引擎的核心是实现可靠的渲染循环:每帧从 swapchain 获取图像、录制绘制命令、提交队列并呈现。关键痛点在于异步执行 ——CPU 提交 vkQueueSubmit 后立即返回,但 GPU 执行滞后,若无同步,易导致 image 复用冲突(撕裂)或队列溢出(stall)。解决方案:用 semaphore 协调 GPU 队列间依赖,用 fence 阻塞 CPU 等待 GPU 完成。典型配置下,设置 MAX_FRAMES_IN_FLIGHT=2,支持双 / 三重缓冲,CPU 可并行录制下一帧命令,而 GPU 处理当前帧,实现~16.7ms 帧时下的高效重叠。

证据显示,在 Vulkan Tutorial 标准流程中,渲染一帧需等待前帧 fence、获取 image(信号 imageAvailableSemaphore)、录制 command buffer、提交(等待 imageAvailable、信号 renderFinished、附 fence)、呈现(等待 renderFinished)。此模式经 CSDN 等多教程验证,能将 CPU 利用率提升 30% 以上,避免单帧等待。

核心搭建步骤与参数落地

1. Instance & Device Setup

  • Instance 创建:启用 validation layers(开发时)和 surface 扩展(KHR)。用 GLFW 创建窗口 surface。
    VkInstanceCreateInfo createInfo{};
    createInfo.enabledExtensionCount = extensions.size();
    createInfo.ppEnabledExtensionNames = extensions.data();
    vkCreateInstance(&createInfo, nullptr, &instance);
    
  • Physical Device & Logical Device:枚举 physical devices,选择支持 graphics+present 队列族的设备。创建 device,获取 graphicsQueue 和 presentQueue(可共享)。
    • 参数:queue 家族优先级VK_QUEUE_GRAPHICS_BIT | VK_PRESENT_BIT,启用VK_KHR_swapchain扩展。
    • 监控点:vkGetPhysicalDeviceQueueFamilyProperties检查 queue count≥1,避免无图形支持设备。

2. Swapchain Config

  • 获取 surface capabilities(min/maxImageCount、formats、presentModes)。推荐VK_PRESENT_MODE_FIFO_KHR(VSync)。
    VkSwapchainCreateInfoKHR createInfo{};
    createInfo.minImageCount = surfaceCaps.minImageCount + 1;  // 双缓冲
    createInfo.imageFormat = surfaceFormat.format;  // VK_FORMAT_B8G8R8A8_SRGB
    createInfo.preTransform = surfaceCaps.currentTransform;
    createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
    createInfo.presentMode = VK_PRESENT_MODE_FIFO_KHR;
    vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain);
    
  • 提取 images(2-3 张),创建 ImageViews 和 Framebuffers。回滚:若VK_ERROR_OUT_OF_DATE_KHR,重建 swapchain。

3. Graphics Pipelines & Render Pass

  • Render Pass:单 subpass,颜色附件(loadOp=Clear,storeOp=Store),VK_ATTACHMENT_LOAD_OP_CLEAR。
  • Pipeline:简单 vertex/fragment shader(GLSL 编译 SPIR-V),固定 viewport(swapchain extent)。动态状态:scissor+viewport。
    • Shader 输入:位置(vec2),输出颜色。Pipeline layout 无 descriptor(最小化)。
    VkGraphicsPipelineCreateInfo pipelineInfo{};
    pipelineInfo.stageCount = 2;
    pipelineInfo.pStages = shaderStages;
    pipelineInfo.pVertexInputState = &vertexInputInfo;  // 无顶点缓冲,hardcode三角形
    pipelineInfo.pInputAssemblyState = &inputAssembly;
    pipelineInfo.pViewportState = &viewportState;
    pipelineInfo.pRasterizationState = &rasterizer;
    pipelineInfo.pMultisampleState = &multisampling;
    pipelineInfo.pColorBlendState = &colorBlending;
    pipelineInfo.layout = pipelineLayout;
    pipelineInfo.renderPass = renderPass;
    pipelineInfo.subpass = 0;
    vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline);
    

4. Command Buffer Recording

  • 创建 Command Pool(transient,queue family index)。
  • 分配 per-frame command buffers(MAX_FRAMES_IN_FLIGHT=2)。
  • 录制函数(recordCommandBuffer):
    void recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex) {
      VkCommandBufferBeginInfo beginInfo{};
      beginInfo.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;
      vkBeginCommandBuffer(commandBuffer, &beginInfo);
      VkRenderPassBeginInfo renderPassInfo{};
      renderPassInfo.renderPass = renderPass;
      renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex];
      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);
      vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
      VkViewport viewport{0.0f, 0.0f, (float)swapChainExtent.width, (float)swapChainExtent.height, 0.0f, 1.0f};
      vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
      vkCmdDraw(commandBuffer, 3, 1, 0, 0);  // 三角形
      vkCmdEndRenderPass(commandBuffer);
      vkEndCommandBuffer(commandBuffer);
    }
    
  • 要点:SIMULTANEOUS_USE_BIT 允许多帧复用;hardcode draw 3 顶点(最小验证)。

5. Semaphore/Fence Sync for Robust Frame Rendering

这是本文核心。创建 per-frame sync 对象:

const uint32_t MAX_FRAMES_IN_FLIGHT = 2;
std::vector<VkSemaphore> imageAvailableSemaphores(MAX_FRAMES_IN_FLIGHT);
std::vector<VkSemaphore> renderFinishedSemaphores(MAX_FRAMES_IN_FLIGHT);
std::vector<VkFence> inFlightFences(MAX_FRAMES_IN_FLIGHT);
VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;  // 首帧不阻塞
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
  vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphores[i]);
  vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]);
  vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]);
}

渲染循环(drawFrame):

  1. vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); 等待前帧 GPU 完成。
  2. vkResetFences(device, 1, &inFlightFences[currentFrame]);
  3. uint32_t imageIndex; vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);
  4. vkResetCommandBuffer(commandBuffers[currentFrame], 0); recordCommandBuffer(commandBuffers[currentFrame], imageIndex);
  5. Submit:
    VkSubmitInfo submitInfo{};
    submitInfo.waitSemaphoreCount = 1;
    submitInfo.pWaitSemaphores = &imageAvailableSemaphores[currentFrame];
    VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
    submitInfo.pWaitDstStageMask = waitStages;
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = &commandBuffers[currentFrame];
    submitInfo.signalSemaphoreCount = 1;
    submitInfo.pSignalSemaphores = &renderFinishedSemaphores[currentFrame];
    vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]);
    
  6. Present:
    VkPresentInfoKHR presentInfo{};
    presentInfo.waitSemaphoreCount = 1;
    presentInfo.pWaitSemaphores = &renderFinishedSemaphores[currentFrame];
    presentInfo.swapchainCount = 1;
    presentInfo.pSwapchains = &swapChain;
    presentInfo.pImageIndices = &imageIndex;
    vkQueuePresentKHR(presentQueue, &presentInfo);
    
  7. currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;

可落地参数 / 阈值清单

  • MAX_FRAMES_IN_FLIGHT: 2(平衡内存 / 性能,>3 易 stall)。
  • waitStages: COLOR_ATTACHMENT_OUTPUT_BIT(精确同步,避免过度等待)。
  • Image count: min+1(不超过 surfaceCaps.maxImageCount)。
  • Timeout: UINT64_MAX(生产用 1e9ns 防死锁)。
  • 监控:vkGetFenceStatus 检查 fence;若 present 返回 SUBOPTIMAL_KHR,重建 swapchain。
  • 风险回滚:image in flight fence 数组防复用(高级);VMA allocator 管理缓冲内存。

此最小引擎~900 行代码,即可渲染旋转三角形。扩展:加顶点缓冲、uniform、depth;多线程 command 录制。

资料来源

  • Vulkan Tutorial: Rendering and Frame Synchronization(标准流程参考)。
  • CSDN Vulkan 学习笔记:同步机制详解,“在 Vulkan 中,同步是一个复杂且关键的问题,特别是在涉及 CPU 和 GPU 之间的交互时。”
查看归档