Hotdry.
systems-engineering

小型游戏引擎基础 Vulkan 渲染器:描述符集、管线状态、命令缓冲与同步实现三角形到纹理渲染循环

面向小型游戏引擎,提供 Vulkan 基础渲染器的核心实现,包括描述符集、管线状态、命令缓冲及同步,用于高效的三角形到纹理渲染循环。

小型游戏引擎需要高效、轻量的渲染后端,Vulkan API 以其低开销和高控制力成为理想选择。本文聚焦基础渲染器实现,覆盖描述符集(Descriptor Sets)、管线状态(Pipeline States)、命令缓冲(Command Buffers)及同步机制(Synchronization),构建从三角形渲染到纹理输出的闭环。不同于高级 bindless 或动态管线,本文强调初学者友好的核心设置,提供可直接落地的 C++ 代码片段与参数配置。

Vulkan 初始化基础

首先,建立 Vulkan 上下文:创建 VkInstance、选择物理设备(VkPhysicalDevice),并创建逻辑设备(VkDevice)。优先选择支持 graphics 和 present 队列族的设备。

// 伪代码示例
VkInstanceCreateInfo instanceInfo{};
// ... 启用验证层
vkCreateInstance(&instanceInfo, nullptr, &instance);

uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
std::vector<VkPhysicalDevice> devices(deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());

// 选择支持 VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_PRESENT_BIT 的设备

创建交换链(Swapchain)时,设置 imageCount=2(双缓冲),format=SwapchainKHR 的 surfaceFormat,presentMode=VK_PRESENT_MODE_FIFO_KHR(VSync)。对于小引擎,extent 设置为窗口尺寸,minImageCount=2 避免撕裂。

描述符集:资源绑定接口

描述符集是着色器与资源(如 Uniform Buffer、纹理)的桥梁。小引擎中,用于传递 MVP 矩阵和纹理采样器。

  1. 创建描述符集布局(VkDescriptorSetLayout):
    • Binding 0: Uniform Buffer (VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, stage=VK_SHADER_STAGE_VERTEX_BIT)
    • Binding 1: Combined Image Sampler (VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, stage=VK_SHADER_STAGE_FRAGMENT_BIT)
VkDescriptorSetLayoutBinding uboLayoutBinding{};
uboLayoutBinding.binding = 0;
uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
uboLayoutBinding.descriptorCount = 1;
uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;

VkDescriptorSetLayoutCreateInfo layoutInfo{};
layoutInfo.bindingCount = 2;  // UBO + Sampler
vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout);
  1. 创建描述符池(VkDescriptorPool),poolSize 为 uniformBuffers=MAX_FRAMES_IN_FLIGHT (2),samplers=1。
  2. 分配描述符集(VkDescriptorSet),更新(vkUpdateDescriptorSets)绑定 Uniform Buffer 和纹理视图。

参数建议:MAX_FRAMES_IN_FLIGHT=2,减少内存占用;使用 staging buffer 上传 Uniform 数据,每帧更新模型矩阵。

管线状态:固定渲染配置

VkGraphicsPipelineCreateInfo 定义管线状态,包括顶点输入、着色器阶段、光栅化、混合等。

  • 顶点着色器:输入位置(VK_FORMAT_R32G32B32_SFLOAT),输出到 gl_Position。
  • 片段着色器:采样纹理或简单颜色。
  • 输入装配:VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST。
  • 视口:动态状态(VK_DYNAMIC_STATE_VIEWPORT),scissor 匹配 swapchain extent。
  • 多采样:disabled(小引擎简化)。
  • 深度测试:disabled(平面三角形)。

创建管线缓存(VkPipelineCache)加速后续管线创建。渲染通道(RenderPass):颜色附件 VK_ATTACHMENT_LOAD_OP_CLEAR 到 VK_ATTACHMENT_STORE_OP_STORE,subpass 无依赖。

VkGraphicsPipelineCreateInfo pipelineInfo{};
pipelineInfo.pVertexShaderModule = vertexShader;
pipelineInfo.pFragmentShaderModule = fragmentShader;
pipelineInfo.pVertexInputState = &vertexInputInfo;  // stride=6*sizeof(float) for pos+uv
pipelineInfo.pInputAssemblyState = &inputAssembly;
pipelineInfo.pViewportState = &viewportInfo;  // dynamic viewport
pipelineInfo.pRasterizationState = &rasterizer;  // cullMode=VK_CULL_MODE_BACK_BIT
vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineInfo, nullptr, &graphicsPipeline);

落地参数:rasterizer.lineWidth=1.0f;depthBias=0;polygonMode=VK_POLYGON_MODE_FILL。

命令缓冲:绘制指令录制

命令池(VkCommandPool)绑定 graphics queue family。预分配命令缓冲(vkAllocateCommandBuffers),一级缓冲(primary)。

录制流程:

  1. vkCmdBeginRenderPass:clearValue color=(0.0f,0.0f,0.0f,1.0f)。
  2. vkCmdBindPipeline:graphicsPipeline。
  3. vkCmdBindDescriptorSets:slot=0。
  4. vkCmdBindVertexBuffers /vkCmdBindIndexBuffer(若索引)。
  5. vkCmdDraw (3,1,0,0) 或 vkCmdDrawIndexed。
  6. vkCmdEndRenderPass。

对于纹理渲染:创建 offscreen RenderPass(颜色附件为纹理 Image,VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT),独立 Framebuffer。命令中切换布局(vkCmdPipelineBarrier,VK_IMAGE_LAYOUT_UNDEFINED 到 VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL)。

小引擎优化:命令缓冲复用,每帧 vkResetCommandBuffer + 重新录制;使用 secondary buffers 嵌套复杂场景。

同步机制:帧间安全

双重缓冲需同步:

  • 信号量(VkSemaphore):imageAvailableSemaphore(acquire 后信号),renderFinishedSemaphore(submit 后信号)。
  • 栅栏(VkFence):inFlightFence [inFlightIndex],等待上一帧完成。

渲染循环:

while (!glfwWindowShouldClose(window)) {
    vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX);
    uint32_t imageIndex;
    vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);

    vkResetFences(device, 1, &inFlightFences[currentFrame]);
    vkResetCommandBuffer(commandBuffers[currentFrame], 0);
    recordCommandBuffer(commandBuffers[currentFrame], imageIndex);  // 包含纹理渲染

    VkSubmitInfo submitInfo{};
    VkSemaphore waitSemaphores[] = {imageAvailableSemaphores[currentFrame]};
    submitInfo.waitSemaphoreCount = 1;
    submitInfo.pWaitSemaphores = waitSemaphores;
    vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]);

    VkPresentInfoKHR presentInfo{};
    presentInfo.pWaitSemaphores = &renderFinishedSemaphores[currentFrame];
    vkQueuePresentKHR(presentQueue, &presentInfo);
    currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
}

参数:超时 UINT64_MAX;fenceCreateInfo.flags=VK_FENCE_CREATE_SIGNALED_BIT(初次)。

三角形到纹理渲染循环

  1. 创建纹理 Image(VK_IMAGE_TYPE_2D, extent=512x512, format=VK_FORMAT_R8G8B8A8_SRGB, usage= COLOR_ATTACHMENT | TRANSFER_SRC)。
  2. 分配 DeviceLocal 内存,transition 布局。
  3. RenderPass1:渲染三角形到纹理(clear red)。
  4. Barrier:纹理从 COLOR_ATTACHMENT_OPTIMAL 到 FRAGMENT_SHADER_READ_ONLY_OPTIMAL。
  5. RenderPass2:全屏 quad 采样纹理显示到 swapchain。

监控要点:RenderDoc 捕获帧,检查 barrier 布局错误;GPU 负载 <80%;帧时 <16ms。

回滚策略:fallback 到单缓冲(imageCount=1),禁用多采样。

此实现总代码~1500 行,适合小引擎原型。扩展时添加 push constants 减少描述符更新。

资料来源:Elias Daler 的 Vulkan 小游戏引擎经验(edw.is/learning-vulkan),Vulkan Tutorial 基础流程,vkguide.dev 同步最佳实践。

查看归档