目录
完整渲染流程(在屏渲染)
初始化流程:
- 创建
Vulkan Instance, 启用ValidationLayer - 根据
Instance创建PhysicalDevice同时查询并获取所需的QueueFamilyIndex - 创建
Logical Device, 获取Device Queue - 创建
SwapChain获取SwapChain中的vkImage,同时创建ImageView - 创建
RenderPass - 创建
GraphicPipeLine - 创建
FrameBuffer - 创建
CommandPool, 分配CommandBuffer - 创建
Sync Object(Fence/Semaphore)
绘制流程:
- 录制
commandBuffer - 提交
commandBuffer - 等待绘制完成
创建基本上下文
从创建 Vulkan Instance, 到获取 Device Queue 的过程比较基础,参考下面的博客
参考:
Vulkan Instance
Vulkan 物理设备和队列族
Vulkan 逻辑设备
创建 Surface 和 SwapChain
- 创建
vkSurfaceKHR对象
在Vulkan中,vkSurfaceKHR是一个抽象的表面对象,用于将渲染结果呈现到屏幕上,SwapChain依赖于vkSurfaceKHR,因为它需要知道渲染目标表面以及如何与之交互
vkSurfaceKHR 对象的作用如下:
-
连接
Vulkan和窗口系统
Windows下使用win32对接,Android下使用ANativeWindow对接 -
管理呈现模式
比如是否使用V-sync,窗口模式等 -
查询表面能力
在创建SwapChain之前,Vulkan需要查询表面属性(比如支持的格式,大小和呈现模式等)
下面的参考代码是从 vkSurfaceKHR 对象中获取颜色和大小等信息
SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) {
SwapChainSupportDetails details;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities);
uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr);
if (formatCount != 0) {
details.formats.resize(formatCount);
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data());
}
uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr);
if (presentModeCount != 0) {
details.presentModes.resize(presentModeCount);
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data());
}
return details;
}
- 创建
SwapChain对象
根据创建的vkSurfaceKHR对象查询出的表面属性(支持的格式,大小,色域等),创建出SwapChain
参考:Vulkan SwapChain 创建
SwapChain 在创建成功之后,内部已经包含了 vkImage 对象,但是这些 VkImage 是由 SwapChain 管理的,不能直接访问,需要调用 Vulkan API 来获取
这些 vkImage 对象的声明周期和 SwapChain 绑定,当 SwapChain 销毁时,对应的 vkImage 对象也会销毁
使用下面的方法获取SwapChain中的vkImage
uint32_t imageCount;
vkGetSwapchainImagesKHR(device, swapchain, &imageCount, nullptr);
std::vector<VkImage> swapchainImages(imageCount);
vkGetSwapchainImagesKHR(device, swapchain, &imageCount, swapchainImages.data());
需要注意的是,如果窗口变化或者失效,此时需要重建 SwapChain, 对应的 vkImage 也可能失效,需要重新获取
WSI 说明
Vulkan 与 WSI(Window System Integration,窗口系统集成)主要集中在Surface,SwapChain,呈现(Presentation) 和平台适配层等多个部分
-
vkSurfaceKHR
表面属性(支持的格式,大小等) -
VkSwapchainKHR
提供一组vkImage用于渲染,控制呈现模式(MailBoxorImmedaite)并处理窗口大小的变化 -
呈现(
vkQueuePresentKHR)
用于将SwapChain中的图像呈现到屏幕
创建 RenderPass 和 FrameBuffer
RenderPass 用于描述一次完整的渲染流程,说明了对 vkImage 的操作关系,vkImage 的操作关系是以颜色,模板、深度附件的形式关联到 RenderPass,RenderPass 只描述渲染流程,不包括渲染数据,渲染数据需要以 FrameBuffer 形式来获取到
这里创建从 SwapChain 获取到的 vkImage 的 vkImageView,在 RenderPass 中定义为颜色附件(作为渲染目标)
vkFrameBuffer 类似于一个图像的容器,里面包含一系列的vkImageView,vkImageView 定义了对应的 vkImage 的解析方式
同时也关联到了对应的vkImage,结合 RenderPass中 vkImage 的操作方法,就定义了一次渲染流程
创建 GraphicPipeLine
GraphicPipeLine 用于对应图形渲染管线,创建 GraphicPipeLine 也就是设置了图像管线的工作状态
最重要的,GraphicPipeLine 还包含对 VertexShader 和 FragementShader 的设置
创建 CommandPool 和 CommandBuffer
Vulkan 中,指令缓存(Command buffer)是用于记录和存储一系列绘制和计算指令的对象
这些指令将在 GPU 上执行,可以执行不同类型的工作,包括绑定顶点缓存、绑定流水线、录制渲染通道指令、设置视口和裁切矩形,设置绘制指令,执行图像和缓存内容的复制操作等
Command buffer 从 Command buffer Pool 中分配得到,Command buffer Pool是根据创建逻辑设备的queueFamilyIndex 作为参数创建的
录制 Command 命令
Command 命令包括绑定顶点缓存(可选)、绑定流水线、设置视口和裁切矩形、录制渲染通道指令、设置绘制指令等
recordCommandBuffer 的过程以 vkBeginCommandBuffer 开始,以 vkEndCommandBuffer 结束
// vkCmdBeginRenderPass
// vkCmdBindPipeline
// vkCmdSetViewport vkCmdSetScissor
// vkCmdDraw
// vkCmdEndRenderPass
// vkCmdEndRenderPass
voidrecordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex) {
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) {
throw std::runtime_error("failed to begin recording command buffer!");
}
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);
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
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);
vkCmdDraw(commandBuffer, 3, 1, 0, 0);
vkCmdEndRenderPass(commandBuffer);
if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {
throw std::runtime_error("failed to record command buffer!");
}
}
同步提交渲染到屏幕
Vulkan 的 渲染-呈现 流程涉及到两个关键的信号量:
imageAvailableSemaphore(由 vkAcquireNextImageKHR 触发),表示 交换链图像已经可用(即 GPU 已完成该图像的先前呈现,可以开始渲染)
renderFinishedSemaphore(由 vkQueueSubmit 触发)表示渲染完成,可以提交给 vkQueuePresentKHR 显示

vkQueuePresentKHR 为什么需要 Semaphore ?
-
Vulkan的Present是一个队列操作(vkQueuePresentKHR提交到VkQueue),意味着它由GPU驱动调度执行
它需要等待GPU渲染完成(通过pWaitSemaphores),确保图像可用后再显示,它可能涉及GPU端的图像布局转换(如从VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL 切换到VK_IMAGE_LAYOUT_PRESENT_SRC_KHR) -
最终,
vkQueuePresentKHR会将图像提交给窗口系统的合成器,这部分操作通常由GPU驱动与窗口系统协作完成
基于此,vkQueuePresentKHR要实现GPU之间队列的同步,所以需要引入一个Semaphore
除此之前在 vkAcquireNextImageKHR 之前,还需要等待 vkQueueSubmit 产生的 Fence:
为什么在 Semaphore 的前提下还需要 Fence:
Vulkan 的交换链(Swapchain)通常有 2 ~ 3 张图像(VK_SWAPCHAIN_IMAGE_COUNT),用于多缓冲
当 vkQueuePresentKHR 提交图像后,GPU 不会立即释放它,而是等待垂直同步(VSync)或窗口系统完成显示
如果过早复用同一张图像(即 vkAcquireNextImageKHR 返回的 imageIndex 仍被 GPU 使用),会导致写入冲突或者数据竞争
vkWaitForFences(device, 1, &inFlightFence, VK_TRUE, UINT64_MAX);
vkResetFences(device, 1, &inFlightFence);
uint32_t imageIndex;
vkAcquireNextImageKHR(device, swapChain,UINT64_MAX, \
imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex);
vkResetCommandBuffer(commandBuffer, 0);
recordCommandBuffer(commandBuffer, imageIndex);
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
// wait a semaphore and trigger an fence
VkSemaphore waitSemaphores[] = { imageAvailableSemaphore };
VkPipelineStageFlags waitStages[] = { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT };
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = waitSemaphores;
submitInfo.pWaitDstStageMask = waitStages;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;
VkSemaphore signalSemaphores[] = { renderFinishedSemaphore };
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;
if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFence) != VK_SUCCESS) {
throw std::runtime_error("failed to submit draw command buffer!");
}
VkPresentInfoKHR presentInfo{};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;
VkSwapchainKHR swapChains[] = { swapChain };
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = swapChains;
presentInfo.pImageIndices = &imageIndex;
vkQueuePresentKHR(presentQueue, &presentInfo);
Fence 和 Semaphore 区别
Fence 适用于 CPU-GPU 同步,而 Semaphore 适用于 GPU-GPU 同步
Fence主要用于CPU等待GPU完成某个操作(例如,确保渲染完成后再更新资源)。Semaphore主要用于GPU内部不同队列或操作之间的同步(例如,渲染完成后再呈现)。
vkQueuePresentKHR 本身是一个 GPU 操作,它需要等待 另一个 GPU 操作(如 vkQueueSubmit 的渲染)完成,因此 semaphore 是更合适的同步机制
2417

被折叠的 条评论
为什么被折叠?



