1. 为什么一帧画面必须交给交换链才能"出门"
在跟着教程把Vulkan的实例、物理设备、队列家族都配好之后,突然要和一个叫 Swap Chain 的东西打交道,很多人第一时间是懵的。我当时也一样:它到底解决了什么问题?前面的流程里,管线、命令缓冲、渲染目标都已经能画了,为什么显示到屏幕这一步非要再多一个环节?
如果离开交换链去理解Vulkan,你会觉得渲染流程其实很直接:把绘制命令提交给队列,GPU按管线执行,最终把结果写入一块图像内存。问题出在最后一步——图像内存写完之后,怎么让屏幕看到它。屏幕不是等GPU画完才更新的,它有自己的刷新节奏,比如常见的60Hz,也就是每16.6毫秒从头到尾扫描一遍。如果图像刚写完一半就被屏幕读走了,画面上半部分是新帧、下半部分还是旧帧,就成了撕裂。这个现象在早期的直接写屏游戏里非常常见。
于是就有了交换链。它本质上是一批可以轮换使用的图像缓冲区,由驱动和窗口系统共同管理。GPU往其中一张图像上渲染,屏幕可能正在读另一张,等这张渲染完,再通过呈现队列把角色换过来。图像不是直接暴露给应用随便用的,而是像货架上的仓位一样,一次只能取一个可用空位,用完还得还给系统。这像极了物流中心的运作方式:渲染器是生产车间,显示器是门店货架,交换链就是中间的调度中枢,负责安排哪批货进哪个仓位、什么时候装车发走。
需要强调一个容易误解的点:交换链并不是Vulkan渲染管线的一部分,它只在"渲染结果要呈现在窗口表面"时存在。如果你只是在做离屏渲染、完全没有窗口界面,比如后台生成纹理或者做批量图像处理,就根本不用创建交换链。我见过有人把交换链当成Vulkan的必需品一路照抄,结果离线渲染代码里也挂着个大而无用的交换链对象。理解它的职责边界,比背住创建流程更值钱。
那为什么会有多个图像而不是一块图像?因为显示引擎(present engine)和GPU渲染器会同时接触这些图像。如果只有一张图,渲染器还没画完,显示引擎就来读了,或者显示引擎还在读,渲染器就要覆盖写,两者必然打架。交换链用多张图像做隔离:一张在展示、一张在等待、一张在渲染。至于几张合适,就是后面要说的minImageCount问题。
2. 动工前先摸底:查询表面能力的三件套
跟Vulkan里几乎所有对象一样,交换链不是一个填好参数就能拍脑袋创建的东西,它依赖物理设备、窗口表面和驱动三者的共同能力。你先得知道硬件能提供什么,才能决定怎么配置。很多教程会直接给出创建代码,但不解释为什么有这些查询步骤,导致读者遇到创建失败时完全没有排查方向。
交换链的一切都挂在"表面"(surface)上。表面是窗口系统与Vulkan之间的抽象层,它在创建交换链之前就必须存在。拿桌面平台来说,你要先通过窗口系统接口创建一个VkSurfaceKHR,再检查这个物理设备上是否存在同时支持图形队列和呈现队列的队列家族。注意,呈现队列和图形队列不一定是同一个,某些平台上它们甚至分属不同的队列家族。如果设备连呈现支持都没有,那后面全是白搭。
真正决定交换链配置的,是这三个查询函数:查询表面能力、查询支持的图像格式、查询支持的呈现模式。三件套一个都不能少,而且顺序也固定:先用vkGetPhysicalDeviceSurfaceCapabilitiesKHR拿到基本能力,再枚举格式列表,再枚举呈现模式列表。
VkSurfaceCapabilitiesKHR caps; vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &caps); uint32_t formatCount = 0; vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, nullptr); std::vector<VkSurfaceFormatKHR> formats(formatCount); vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, formats.data()); uint32_t modeCount = 0; vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &modeCount, nullptr); std::vector<VkPresentModeKHR> presentModes(modeCount); vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &modeCount, presentModes.data());表面能力结构体里最有用的几个字段:minImageCount和maxImageCount决定了你能申请多少张图像;currentExtent是当前表面在像素坐标系下的尺寸;currentTransform表示窗口当前是否处于旋转或翻转状态;supportedUsageFlags则告诉你能把交换链图像当什么用途。很多人只关心格式和呈现模式,忽略了currentExtent,结果窗口尺寸一变就各种崩。
选格式也不是随便挑一个好看的。Vulkan要求交换链图像格式必须是表面支持的,如果乱填一个没有列出的格式,创建直接失败。最常见的做法是找VK_FORMAT_B8G8R8A8_SRGB + VK_COLOR_SPACE_SRGB_NONLINEAR_KHR的组合,这也确实是绝大多数桌面平台默认提供的组合。
VkSurfaceFormatKHR chooseSurfaceFormat( const std::vector<VkSurfaceFormatKHR>& available) { for (const auto& fmt : available) { if (fmt.format == VK_FORMAT_B8G8R8A8_SRGB && fmt.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { return fmt; } } return available[0]; }为什么非要SRGB?因为显示器最终期望的颜色空间就是SRGB,你渲染过程中用线性空间做光照计算,最终输出到交换链图像时再做编码转换,颜色才不会偏。如果你选了个UNORM格式而采样时又混用SRGB视图,画面上会出现奇怪的偏灰或者偏色。这是一个非常隐蔽但出现概率极高的坑。
查询完这三样,你对这块设备在当前窗口上的能力就有了底。下一步才是真正的"拍板"阶段。
3. 交换链设计表:VkSwapchainCreateInfoKHR 逐字段权衡
创建交换链的核心就是填一个VkSwapchainCreateInfoKHR。这个结构体字段多、关系复杂,光靠死记很容易出错。我习惯把它当成一张"物流方案设计表"来看,每个字段都是在回答一个问题:你准备在什么场地、用多少个库位、用什么规格的货车、按什么节奏发运。
先看一份典型的创建代码:
VkSwapchainCreateInfoKHR createInfo{}; createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; createInfo.surface = surface; createInfo.minImageCount = caps.minImageCount + 1; createInfo.imageFormat = surfaceFormat.format; createInfo.imageColorSpace = surfaceFormat.colorSpace; createInfo.imageExtent = extent; createInfo.imageArrayLayers = 1; createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; createInfo.preTransform = caps.currentTransform; createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; createInfo.presentMode = presentMode; createInfo.clipped = VK_TRUE; createInfo.oldSwapchain = VK_NULL_HANDLE;minImageCount为什么要在能力下限之上再加1?因为驱动内部可能会额外占用一张图像做管理,如果卡在下限值,应用拿到的可用图像数会比预期少。最典型的情况是FIFO模式:一张图像正在被显示引擎扫描,一张在等待队列里排队,如果只有两张可轮换,CPU和GPU一旦有一方节奏波动,整条流水线就没法重叠执行。多数驱动上双缓冲勉强能工作,但这就像只有两辆车的物流线,任何一辆晚点全盘堵塞。我推荐采用caps.minImageCount + 1起步,条件允许时甚至可以设置3张,也就是真正的三重缓冲。
imageExtent要和caps.currentExtent仔细对齐。有些表面上currentExtent直接给出有效尺寸,直接用;有些则返回一个特殊值(UINT32_MAX),表示窗口尺寸由应用自己决定。后一种情况里,你拿到的窗口坐标尺寸通常会受DPI缩放影响,不能直接盲目使用,要拿窗口的真实像素尺寸来计算。尺寸填错,后面渲染的视口、裁剪矩形、帧缓冲尺寸跟着全错,整个画面可能偏移或裁剪,而不是报错,排查起来伤脑筋。
preTransform看起来是最好抄的字段,很多人直接写VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR。但如果你在窗口旋转90度的环境下这么做,合成器为了适配变换就得做额外的旋转补偿,轻则性能下降,重则显示异常。正确的做法是把它设置成caps.currentTransform,沿用当前的窗口变换,让交换链自己适配。这个字段的坑在窗口允许旋转或用户切换屏幕方向时最容易暴露。
compositeAlpha通常用VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,表示图像不透明。如果你要做透明窗口,那才需要去查表面的supportedCompositeAlpha,使用INHERIT类型。绝大多数桌面窗口程序不需要透明,选OPAQUE最稳。在没确认能力前,不要一上来就选别的组合。
imageSharingMode与队列家族有关。如果你的图形队列和呈现队列属于同一个队列家族(桌面平台很常见),直接使用VK_SHARING_MODE_EXCLUSIVE就行。如果它们分属不同家族,你就得考虑用VK_SHARING_MODE_CONCURRENT并显式指定队列家族索引,或者接受在队列间做图像所有权转移的开销。这个字段不少初学的人直接忽略,遇到多队列设备时就会出现隐性的同步问题。
还有个细节:交换链创建成功之后,你申请的是一回事,驱动实际分配的是另一回事。minImageCount在你这边只是下限值,创建完成后一定要用vkGetSwapchainImagesKHR再查一次,拿到真正的图像数量和图像句柄。不要想当然地认为你申请了3张就有3张,某些平台驱动可能给你4张甚至更多。把这一步漏掉,后面创建图像视图和帧缓冲时空指针访问,问题很难查。接下来的工作就是为这些图像创建图像视图、再为每张图像创建帧缓冲,为渲染循环做准备。
4. 帧的完整物流周期:获取、渲染、呈现如何被同步管住
交换链创建好了,一大堆图像躺在那里,但你不能想用哪张就用哪张。每一帧都要走一条固定的"物流流程":先从交换链申请一个可用的图像槽位,然后让GPU往这个槽位渲染,渲染完了再把它交给呈现队列发出去。这三段之间最核心的问题不是画画,而是同步。
为什么同步这么难?因为交换链图像不仅被你的GPU命令使用,还被显示引擎使用。你在上一帧往图像A上渲染的时候,屏幕可能正在读图像A,也可能显示引擎已经准备在下一帧读取图像A。如果没有同步机制,GPU和显示引擎同时读写同一块图像内存,画面就该出鬼影和撕裂了。Vulkan解决这个问题的工具就是信号量(Semaphore)和栅栏(Fence),它们的角色完全不同:信号量主要在GPU内部做执行顺序约束,栅栏用来把执行结果通知给CPU。
这里我用一个"对讲机和门禁杆"的类比来记:GPU队列是车间里的传送带,信号量是对讲机——A工位完成前喊一声,B工位听到后才开工;栅栏是工厂门口的抬杆——只有杆子抬起来,外面等着的人(CPU)才知道里面已经完活了。传送带之间靠对讲机协调,对外部人员的放行靠抬杆。知道了这个区别,你在选择用信号量还是栅栏时就不会犯糊涂。
一帧的典型循环长这样:
uint32_t imageIndex = 0; vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, imageAvailableSemaphores[frameIndex], VK_NULL_HANDLE, &imageIndex); vkResetCommandBuffer(commandBuffers[frameIndex], 0); recordCommandBuffer(commandBuffers[frameIndex], imageIndex); VkSemaphore waitSemaphore = imageAvailableSemaphores[frameIndex]; VkPipelineStageFlags waitStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; VkSubmitInfo submitInfo{}; submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.waitSemaphoreCount = 1; submitInfo.pWaitSemaphores = &waitSemaphore; submitInfo.pWaitDstStageMask = &waitStage; submitInfo.signalSemaphoreCount = 1; submitInfo.pSignalSemaphores = &renderFinishedSemaphores[frameIndex]; submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &commandBuffers[frameIndex]; vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[frameIndex]); VkPresentInfoKHR presentInfo{}; presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; presentInfo.waitSemaphoreCount = 1; presentInfo.pWaitSemaphores = &renderFinishedSemaphores[frameIndex]; presentInfo.swapchainCount = 1; presentInfo.pSwapchains = &swapchain; presentInfo.pImageIndices = &imageIndex; vkQueuePresentKHR(presentQueue, &presentInfo);vkAcquireNextImageKHR干了一件很微妙的事:它返回的imageIndex只是一个索引,代表交换链里某个图像槽位的编号。拿到这个索引后,渲染命令里所有跟图像相关的资源都得切换到这个索引对应的那块图像上,比如帧缓冲、图像布局转换、清晰操作等。如果一帧里忘了一处,画面就会出现那种"一块区域是上一帧内容、一块区域是当前帧内容"的诡异现象。
为什么要用信号量等VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT阶段?因为颜色附件写入只发生在管线的这个阶段,你的渲染命令里真正修改交换链图像内容的,就是这个阶段的写操作。只有在信号量保证图像已经被显示引擎释放后,颜色写入才能开始,否则就是跟显示引擎抢着写同一块内存。
还有一个特别容易被忽略的同步细节:同一帧渲染用的CommandBuffer、信号量、栅栏,并不是每次循环都用0号。通常你会准备2到3套独立的资源,比如perFrame[0]、perFrame[1]、perFrame[2],按帧编号轮询使用。原因很简单,CPU在提交第N帧命令时,GPU可能还在执行第N-1帧,如果大家共用同一个命令缓冲和同一个栅栏,上一帧命令还没跑完,这一帧的vkResetCommandBuffer就会重置掉正在执行的命令,直接引发不可预知的错误。
vkQueuePresentKHR负责把渲染完成的图像发到呈现队列。这里等待的信号量是renderFinishedSemaphores[frameIndex],它保证图像内容已经完整渲染完毕,显示引擎可以安全读取。整个流程就是:等槽位、做渲染、发走,然后下一轮循环再等下一个可用的槽位。图像历史中所有在等待的帧,组成了一个环形的流水线。只要任何一环出现同步缺失,帧与帧之间就会互相踩踏,这是交换链相关同步错误最常见的来源。
5. 呈现模式与画面节奏:FIFO、MAILBOX、IMMEDIATE 怎么选
同步搞清楚了,接下来要面对的是用户体验级别的选择:呈现模式。呈现模式决定了图像送到屏幕的节奏,它直接关系到你看到的画面是平滑、撕裂,还是延迟低。Vulkan对交换链的呈现模式定义得非常明确,但很多人搞不清楚它们在实践中的差别。
| 呈现模式 | 是否垂直同步 | 是否会撕裂 | 延迟特性 | 适合场景 |
|---|---|---|---|---|
| VK_PRESENT_MODE_FIFO_KHR | 是,同步到刷新率 | 不会 | 可能多等待一帧,延迟较高 | 默认选择、办公界面、视频播放 |
| VK_PRESENT_MODE_MAILBOX_KHR | 是,同步到刷新率 | 不会 | 延迟较低,但会丢弃旧帧 | 游戏渲染、追求低延迟 |
| VK_PRESENT_MODE_IMMEDIATE_KHR | 否 | 会 | 延迟最低 | 画面刷新要求极低延迟、能接受撕裂的场景 |
| VK_PRESENT_MODE_FIFO_RELAXED_KHR | 半同步,刷新率跟不上时可篡位 | 会 | 延迟比FIFO低 | 视频播放,允许偶尔撕裂 |
初学阶段最容易犯的选择错误是:为了让画面"流畅",直接上 IMMEDIATE。实际上 IMMEDIATE 只是把图像即刻发送给显示引擎,不做任何节奏调理,屏幕刷新期间读到一个未完成的画面就可能撕裂。很多人的第一反应是撕裂都是垂直同步没开,其实在FIFO模式下撕裂基本不会出现,但你会发现输入到画面显示之间的延迟明显增加。FIFO 是绝对的大多数应用默认项,也是所有Vulkan实现都必须支持的模式。
MAILBOX 是我的游戏项目里最常用的模式。它本质上就是三重缓冲的垂直同步:显示引擎永远显示最新一帧,中间如果有一帧来不及,宁可丢弃旧帧再赶下一帧,也不会去把屏幕拖进卡顿。你可以把MAILBOX理解成"始终保持在货架上放最新鲜的货",过期的货直接扔掉。它带来的延迟通常只比垂直同步多一点点,但观感比FIFO跟手很多。不过它有一个注意点:不是所有平台都支持,代码里要先在枚举结果里找一遍,不支持再回退到FIFO。
如果做了一个跟随鼠标移动的绘图应用,或者做的是一个对操作反馈极度敏感的交互Demo,IMMEDIATE 会是更合适的方案。它基本上是完全放养状态,GPU画完立即送显,所以输入到画面的路径最短,但撕裂必须做好心理准备。我一般不把它放在默认配置,而是放在设置菜单里让用户自己选。
值得注意的是,呈现模式本身不限制渲染帧率。很多人以为用了 FIFO 就自动锁60FPS了,其实它锁的是"画面被发送给显示器的频率",而不是"你执行渲染循环的频率"。如果你的循环不节流,CPU照样会疯狂提交命令、疯狂执行渲染,只是显示引擎只在刷新点上把最新帧推出去。要限制应用的渲染帧率,仍然需要自己在主循环里做节流或者用栅栏等待控制节奏。
选好呈现模式后,还有一个衍生问题需要考虑:当画面内容从一帧切到下一帧时,如果尺寸或内容发生了跳跃性变化,人的视觉会产生闪烁。为了缓解这个问题,交换链里那组图像并不是越少越好,而是要有足够深度来缓冲帧与帧之间的节奏差异。把 minImageCount 设置成3并且配合MAILBOX,是我在多个项目里实测下来最均衡的配置,兼顾了低延迟和稳定性。
6. 窗口一变,链路崩溃:交换链重建的完整时机
交换链的一个宿命是:它绑定在窗口表面上,而窗口表面是活的。窗口可以移动、可以缩放、可以被遮挡、可以在全屏和窗口模式之间切换,甚至显示器本身的刷新率、分辨率都可能变化。这些变化几乎都意味着旧的交换链不再适用,必须重建。如果对重建时机处理不当,轻则画面错乱,重则直接报错崩溃。
先认识两个错误码:VK_ERROR_OUT_OF_DATE_KHR和VK_SUBOPTIMAL_KHR。前者表示交换链已经完全不兼容当前表面状态,比如窗口尺寸变了,必须重建才能继续;后者表示交换链还能工作,但结果不是最优,比如窗口刚移动过但实际尺寸没变。开发中约定俗成的做法是:获取图像或呈现图像时收到VK_ERROR_OUT_OF_DATE_KHR,立刻重建交换链并跳过当前帧;收到VK_SUBOPTIMAL_KHR,可以继续画,但稳妥起见也重建一次。
重建并不是简单地把创建函数再调用一遍。交换链对象本身要旧换新,同时所有依赖交换链的对象都得跟着重建,包括每个交换链图像对应的图像视图、帧缓冲、深度缓冲,甚至某些渲染通道里跟图像格式相关的附件描述。我最早偷懒只重建了交换链,结果下一次记录命令缓冲时帧缓冲还是旧的图像句柄,画面直接黑屏,Vulkan连个明显的错误都不给,全靠逐行日志排查。
一个典型的重建流程长这样:
void recreateSwapchain() { vkDeviceWaitIdle(device); cleanupSwapchainObjects(); // 析构 image views、framebuffer、depth buffer swapchain = createSwapchainWithOldHandle(oldSwapchain); createImageViews(); createDepthResources(); createFramebuffers(); }这里有一个细节很值得注意:新的交换链创建函数里有一个oldSwapchain字段。为什么要保留旧句柄?因为驱动可以利用旧交换链里尚未释放的图像资源做平滑过渡,减少重建过程中的画面闪烁和内存压力。正确做法是先带着oldSwapchain创建新交换链,再销毁旧交换链。如果每次都暴力销毁再创建,某些驱动上重建期间会出现明显黑屏。
窗口最小化是重建逻辑里最容易炸的地方。窗口最小化时表面上currentExtent往往变成0或者无效值,如果你在这种状态下强行重建交换链,图像尺寸为0,后面的渲染命令一执行就崩。比较稳妥的方案是在resize回调里只记录一个"需要重建"的标记,真正执行重建放到主循环里,同时先查询currentExtent,发现尺寸为0就挂起重建,等窗口恢复后再继续。这个方案能同时避免最小化崩溃和反复横跳式重建。
当交换链需要重建时,正在主循环里的上一帧可能还在GPU上执行,直接销毁交换链会造成正在使用的图像被提前释放,从而引发未定义行为。所以重建的第一步通常是vkDeviceWaitIdle,别小看这一句。如果重建发生在渲染循环的中途,记得在重建后直接continue跳过本帧,不要继续用旧状态向新的交换链提交命令。
多窗口程序里重建问题会放大。我见过有项目在处理显示器DPI热切换时,连续触发了两次resize事件,第一次重建还没完成第二次又来了,导致旧交换链句柄悬空。后来我学到的做法是对重建操作加个互斥:标记位加锁,更新过程中如果又收到resize事件,就把标记位重新置上,等重建完成后再进入下一轮。这属于事件驱动常见的竞态问题,和Vulkan本身无关,但遇到概率不低。
7. 交换链调试经验:我踩过的几个坑与排查链路
讲完理论,说一点实操层面踩过的坑。每个问题背后都有一段排查链,如果你也碰到类似现象,可以参照着快速定位。
第一个坑是图像数量太少导致的间歇性卡顿。某次我把minImageCount设置成了表面支持的下限2,FIFO模式下窗口拖动时总是一顿一顿的。当时我以为是指针问题,查了很久最后发现就是图像数量少了:2张图像,一张被显示引擎扫描,一张在排队。GPU画完当前帧想取下一张时,队列里的那一张还没轮到发车,于是整个流程被迫等待。把数量改成3之后,流水线的三个角色(显示中、排队中、渲染中)各自有独立的"库位",卡顿立刻消失。
第二个坑是忘记了 acquire 信号量的等待,导致画面出现闪烁。某次为了调渲染逻辑,临时把vkAcquireNextImageKHR里的信号量传成VK_NULL_HANDLE,想着反正只需要索引。结果画面隔几秒闪一下,像是渲染结果被随机破坏。原因是显示引擎还没读完上一帧,渲染器就已经开始往同一张图像上覆盖写了。在交换链的使用中,任何对同步对象的"偷工减料"都会以奇怪的视觉故障报复你。这种问题用启用验证层(Validation Layers)的方式比较好定位,验证层会直接报出对象同步错误。
第三个坑是窗口移动时画面错位。这个问题的排查链路比前两个长,先是重启程序能恢复,最后定位到preTransform字段:某次我手动指定了IDENTITY,但窗口经过旋转后,驱动给currentTransform返回的是90度旋转,合成器需要额外做旋转补偿,部分区域的显示尺寸就对不上了。后来改成始终跟随caps.currentTransform,问题消失。这个坑提醒我:交换链相关配置别只抄代码,要实际打印出硬件给的值来对比。
第四个坑跟格式有关。某台设备的表面格式列表里居然没有SRGB,默认返回的是线性UNORM格式。那时我照搬了通用的 B8G8R8A8_SRGB 组合,结果创建交换链返回VK_ERROR_FORMAT_NOT_SUPPORTED。排查到最后才发现,那个环境的窗口合成器走的是线性颜色空间。这一步让我明白:不要假设每个平台的格式支持列表都一样,选择格式时最好把可用列表打出来看一眼,万一只支持一种格式就直接用它。
第五个坑是高DPI状态下窗口尺寸与像素尺寸不一致导致的拉伸。事件循环里拿到的逻辑尺寸是缩放后的,如果直接把那个值填进imageExtent,画面会糊成一片或者被裁剪。后来我统一用一个独立的函数去查询窗口的真实像素尺寸再做计算,并且在resize回调里强制重新计算。做了这个处理之后,窗口在不同缩放级别之间拖动就不会再出现边界对不齐的情况。
除了上面这些,还有一个我在调交换链时常用的排查手段:每创建完一个交换链,就把vkGetSwapchainImagesKHR返回的真实图像数量、格式、尺寸打印出来。这看起来不起眼,但能快速核对应用配置和驱动实际分配是否一致。颜色不对排查颜色,尺寸不对查事件循环,数量不对查 minImageCount。把这段打印留在日志里,一个被动发生的黑屏问题通常不用猜就能第一时间发现是哪一层出了问题。
写到这里,交换链相关的核心内容差不多就这些了。最后再分享一个我自己的建议:学习交换链时别急着往后推,先把创建后查一遍图像数量、窗口缩放时重建一次这两个环节亲手跑一遍。特别是重建那一步,很多问题的根源都在对OUT_OF_DATE的错误处理上。把这个流程彻底摸透,后面写多帧渲染、写多窗口、写VR立体呈现时,你都会少吃很多苦头。