目录
完整渲染流程(在屏渲染)
初始化流程:
- 创建
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
用于渲染,控制呈现模式(MailBox
orImmedaite
)并处理窗口大小的变化 -
呈现(
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_OPTIMA
L 切换到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
是更合适的同步机制