在 Vulkan 和图形学中,交换链(Swapchain) 是连接“游戏渲染引擎”和“显示器屏幕”之间的核心桥梁。
简单来说,它的本质是一个由多个图形缓冲区(Images/Buffers)组成的队列(通常是 2 个或 3 个),负责在后台悄悄接收显卡画好的图像,并在恰当的时机把图像“交换”到屏幕上显示出来。
1. 为什么需要交换链?(解决“撕裂”问题)#
为了理解交换链,我们先看看如果没有它会发生什么。
假设显卡直接往屏幕要显示的内存(帧缓冲区)里画画:显卡画得极快,而屏幕刷新得很慢(比如 60Hz 屏幕,每 16.6 毫秒才从上到下刷新一次)。
当显卡画到一半,屏幕刚好开始刷新,屏幕就会把上一帧的下半部分和当前帧的上半部分一起显示出来,这就造成了画面断层的屏幕撕裂(Screen Tearing)现象。
2. 交换链的工作原理:前后缓冲区#
交换链通过引入多个缓冲区和轮流交替机制完美解决了这个问题。最经典的是双缓冲区(Double Buffering)机制:
后缓冲区(Back Buffer):显卡(Vulkan)正在默默在后台拼命绘制画面的那张画布。
前缓冲区(Front Buffer):当前正在被显示器读取并显示在屏幕上的那张画布。
经典的工作循环如下:
渲染:显卡把游戏画面渲染到后缓冲区。
提交(Present):显卡画完了,向交换链发出申请:“我画好了,准备展示!”
交换(Swap / Flip):在屏幕刷新的间隙(垂直同步 VBlank 时期),交换链将前、后缓冲区对调。原来的后缓冲区变成前缓冲区显示在屏幕上;原来的前缓冲区变成新的后缓冲区,等待接收下一帧。
3. 三缓冲区(Triple Buffering)与展示模式#
随着硬件发展,双缓冲区也存在瓶颈:如果显卡画得太快,画完后缓冲区后,必须干等着屏幕刷新才能交换,显卡就被迫“卡住”停工了。
于是交换链引入了三缓冲区(1个前缓冲区,2个后缓冲区),并提供了不同的展示模式(Present Modes):
- VK_PRESENT_MODE_FIFO_KHR(强启垂直同步):
缓冲队列像一个先进先出的管道。如果队列满了,显卡必须排队等待屏幕刷新。优点:绝对不撕裂;缺点:有输入延迟。
- VK_PRESENT_MODE_MAILBOX_KHR(三倍缓冲/邮箱模式):
如果显卡画得比屏幕刷新快,它不会停工,而是直接用最新画好的帧覆盖替换掉掉队列里还没来得及显示的旧帧。优点:绝对不撕裂,且延迟极低,是 Vulkan 最理想的模式。
- VK_PRESENT_MODE_IMMEDIATE_KHR(关闭垂直同步):
显卡一画完,不管屏幕刷到哪里,立刻硬塞给屏幕。优点:延迟最低;缺点:画面疯狂撕裂。
4. 为什么 Vulkan 的交换链很特殊?#
在老旧的 OpenGL 中,交换链(Window System Integration)是被强行封装在 API 内部的,开发者很难插手控制。
而在 Vulkan 中,交换链是一个明确的、需要开发者手动创建和管理的显卡对象(VkSwapchainKHR)。
需要亲自查询显卡支持多少种分辨率、支持什么色彩格式(如 RGBA 还是 BGRA)。
需要自己决定是要双缓冲还是三缓冲。
需要亲自去交换链里“索要”一张当前空闲的画布(
vkAcquireNextImageKHR),画完之后再亲自把它“推”给屏幕(vkQueuePresentKHR)。
Vulkan 交换链创建的流程#
第一步:确认硬件是否支持“显示”#
Vulkan 是一个跨平台的 API,有些场景(比如服务器端跑离线渲染、高性能计算)是不需要显示画面的。
确认队列能力:首先要向物理显卡确认,当前挑选的显卡硬件队列,是否有能力把画面“推”到当前创建的窗口桌面上。
开启交换链扩展:在正式激活逻辑设备(Device)时,明确告诉显卡需要开启呈现扩展服务。
第二步:向窗口系统“查家底”(查询支持参数)#
每个人的显示器、显卡和操作系统组合起来,能支持的画面特性都是不一样的。因此需要从硬件那里查到以下三种关键信息:
基础表面能力:当前窗口的宽高是多少像素?最少需要几个缓冲区(双缓冲还是三缓冲)?最大支持多少个?画面支不支持旋转、翻转?
像素格式列表:支持哪些色彩格式?例如是红绿蓝(RGB)还是蓝绿红(BGR)?支持不引人注目的普通色彩,还是支持 sRGB 伽马校正的色彩空间?
垂直同步模式:支持哪些冲刷画面的方式?是支持立即显示(会导致画面撕裂),还是支持严格的垂直同步(无撕裂但锁帧),或者是支持三重缓冲(既不撕裂又低延迟)?
第三步:制定策略,挑选最优配置#
拿到上一步的支持列表后,程序开始做选择题,挑出最合适的参数:
定数量:通常在硬件允许的最小缓冲区数量上
+ 1,尽量凑出三缓冲,让渲染和显示各不干扰。定分辨率:让交换链的画面宽高完美匹配当前窗口的物理像素大小。
定格式:优先挑选符合人眼视觉习惯的 sRGB 渲染格式。
定模式:如果硬件支持,优先选择三重缓冲(Mailbox),否则用强制保底的传统垂直同步(FIFO)。
第四步:结合业务需求,正式向显卡申请创建#
把上面挑选好的参数全部打包。在这一步,还需要把核心用途位掩码也加进去:
明确告诉显卡,这批图像主要用来当作颜色输出画布。
顺便探测一下,如果显卡支持,把截图、数据复制、后处理等权限也一并申请过来。
处理队伍矛盾:告诉系统,如果图形渲染队列和画面呈现队列不是同一个硬件通道,这批图像需要开启“共享模式”以便它们互相传递。
最终,向 Vulkan 驱动发出指令,正式在显存里开辟出这一组交换链对象。
第五步:认领并接管这一组“隐形画布”#
交换链成功创建后,Vulkan 在底层其实已经帮你创建好了一组图像(Image)。
此时,程序需要向交换链“索要”这几张画布的控制句柄,并把它们存进一个数组里。
只有拿到这些句柄,渲染管线才知道以后要把画面画到哪里去。
第六步:给每张画布装上“镜头”(创建图像视图)#
在 Vulkan 中有一个非常严格的规定:不能直接去读写一张原始的图像,而必须通过一个媒介去“看待”它。 这个媒介就是图像视图(Image View)。
需要为数组里的每一张交换链画布,单独包装一个对应的图像视图。
图像视图就像是一个说明书,它明确告诉显卡:虽然这是一块原始内存,但请把它当成一个标准的、2D 的、只关注红绿蓝蓝四个通道的颜色渲染目标来看待。
最终状态#
当第六步的图像视图全部包装完毕后,整个交换链的初始化才真正宣告结束。
接下来,这些图像视图就会被送进帧缓冲区(Framebuffer)里,和渲染流程(Render Pass)绑定。显卡就可以开始开足马力,在这些画布上轮流画画,并把画好的成品源源不断地推送到显示器上了
为什么创建交换链之后需要调用回调函数#
核心原因是为了通知并同步整个渲染引擎的其他模块,让他们根据新的窗口状态重新调整自己的“画布”和依赖资源。
具体来说,主要有以下四个深层次的原因:
1. 视口(Viewport)与裁剪区(Scissor)需要同步更新#
交换链一旦创建或重建,意味着窗口的大小(分辨率)可能已经发生了变化。
- 图形管线在绘制时,需要明确知道要把画面投射到多大的屏幕区域(Viewport),以及哪些区域需要被裁剪(Scissor)。
- 触发回调函数可以第一时间把最新的窗口宽高(Width / Height)传给渲染器,动态更新视口和裁剪区的大小,防止画面出现拉伸、变形或者只渲染在屏幕角落的情况。
2. 依赖屏幕分辨率的资源(如深度缓冲区)需要随之重建#
在渲染 3D 游戏或复杂场景时,除了交换链提供的颜色画布(Color Buffer)外,我们通常还需要其他辅助画布,比如深度缓冲区(Depth Buffer)或抗锯齿缓冲区(MSAA Buffer)。
- 这些缓冲区的大小必须与交换链的图像大小完全一致。
- 交换链本身不负责创建深度图。因此,当交换链创建完毕后,必须通过回调函数通知负责管理资源的模块:“新画布的尺寸变了,请立刻销毁旧的深度图,并按照新尺寸重新申请一块深度显存。”
3. 重新构建帧缓冲区(Framebuffer)#
在 Vulkan 中,交换链的图像视图(Image View)和深度视图(Depth View)必须打包绑定在一起,塞进一个叫做帧缓冲区(Framebuffer)的对象里,渲染管线(Render Pass)才能正常工作。
- 既然交换链变了(旧的图像视图已被销毁,新的已创建),那么原本的 Framebuffer 自然就失效了。
- 回调函数的一个核心任务,就是拿到全新的一组交换链图像视图,结合刚刚重建的深度视图,重新生成一整套 Framebuffer 数组,为下一帧的录制做好准备。
4. 录制命令缓冲区(Command Buffer)的解冻与重录#
如果你的引擎设计采用了静态/预录制命令缓冲区的策略(即把渲染命令提前录制好,每帧直接提交以换取极致性能):
- 命令缓冲区内部记录了具体的 Framebuffer 绑定信息和 Viewport 状态。
- 一旦交换链重建,这些录制好的命令就全部报废了。通过回调函数,引擎可以安全地重置(Reset)这些命令缓冲区,并根据新的交换链状态重新录制一遍绘制指令。
总结#
交换链就像是显卡和显示器之间的一个高级传送带。显卡在传送带的一头不停地把画好的图片放上去,显示器在传送带的另一头按部就班地拿去播放。它不仅保障了画面的平滑顺畅,还掌控着游戏帧率和画面延迟的命脉。