Flutter Impeller 引擎 Vulkan Swapchain 详解:面向多平台 WSI 的交换链实现

  • 跨平台
  • 图形学
  • 前端

【免费下载链接】engine

The Flutter engine

项目地址: https://gitcode.com/gh_mirrors/eng/engine
点击查看 免费下载

导读

本文以 Flutter 引擎中 Impeller 渲染器的 Vulkan 后端为对象,深入讲解其交换链(Swapchain)实现——这是 Impeller 在 Vulkan 上完成窗口系统集成(WSI,Window System Integration)的核心模块。文章围绕 swapchain 目录 中的 SwapchainVK 抽象、基于 VK_KHR_swapchain 扩展的通用实现(KHR Swapchain)以及面向 Android 的硬件缓冲交换链(AHB Swapchain)展开,结合源码剖析交换链创建、帧同步、图像获取与呈现的完整生命周期。读完本文,你将掌握 Impeller Vulkan 交换链的架构设计、KHR 与 AHB 两条实现路径的适用场景与取舍,以及底层 vkAcquireNextImageKHR / vkQueuePresentKHR 调用链中同步与错误处理的工程细节。

一、为什么 Impeller 需要自己的 Swapchain 抽象

在 Vulkan 中,向屏幕呈现渲染结果并不是直接写入某个固定缓冲,而是通过“交换链(swapchain)”向窗口系统申请可呈现的图像(image),渲染完成后再通过呈现队列交还给窗口系统。交换链与具体的窗口系统集成方式(WSI)强相关:桌面平台、移动平台与嵌入式平台的窗口抽象、缓冲格式、同步模型各不相同。

Flutter 引擎的 Impeller 渲染器在其 Vulkan 后端 中为此建立了一层独立的交换链模块,即 impeller/renderer/backend/vulkan/swapchain。该目录正如其 README 所述,“包含面向不同平台窗口系统集成(WSI)的交换链实现”,其核心目标是把“窗口系统差异”封装在交换链内部,让上层渲染管线只关心“获取下一帧可绘制表面”这一个统一接口。

这一抽象的直接体现是基类 swapchain_vk.h 中的 SwapchainVK:

class SwapchainVK {
 public:
  static std::shared_ptr<SwapchainVK> Create(
      const std::shared_ptr<Context>& context,
      vk::UniqueSurfaceKHR surface,
      const ISize& size,
      bool enable_msaa = true);

  virtual bool IsValid() const = 0;
  virtual std::unique_ptr<Surface> AcquireNextDrawable() = 0;
  virtual vk::Format GetSurfaceFormat() const = 0;
  virtual void UpdateSurfaceSize(const ISize& size) = 0;
  ...
};

从源码结构看,该抽象刻意只暴露四个关键操作:合法性查询(IsValid)、获取下一帧可绘制表面(AcquireNextDrawable)、查询表面格式(GetSurfaceFormat)、更新表面尺寸(UpdateSurfaceSize)。其中注释明确指出:交换链会自适应底层表面失效(out of date)的情况;如果调用方无法获取下一个 drawable,说明发生了不可恢复的错误,交换链必须连同新的表面一起重建。

UpdateSurfaceSize 的设计也反映了移动端高频场景:窗口尺寸变化(旋转、键盘弹出)时并不销毁整个交换链对象,而是将其标记为“配置脏”,在下一帧强制按新尺寸重建,见 khr_swapchain_impl_vk.cc 中重建逻辑与 oldSwapchain 的传递。

二、两条实现路径:KHR Swapchain 与 AHB Swapchain

SwapchainVK::Create 是两条路径的汇合点(swapchain_vk.cc):

  • 桌面 / 通用路径:接收一个已经创建好的 vk::UniqueSurfaceKHR,直接构造 KHRSwapchainVK;
  • Android 路径:接收一个 ANativeWindow*,先通过 createAndroidSurfaceKHRUnique 创建 Android 平台 surface,再进入通用创建流程。

Android 路径的代码注释写得很直白:“如果 AHB 交换链不可用,则回退到 KHR 交换链”(Fallback to KHR swapchains if AHB swapchains aren't available)。也就是说,KHR 路径既是通用方案,也是兜底方案。

2.1 KHR Swapchain:通用且可靠的兜底实现

KHR Swapchain 的 README 给出了明确的定位:一个依赖 VK_KHR_swapchain 扩展的 Vulkan 交换链实现,在大多数平台上可用,但在所有平台上未必是最优选择——不过它是一个良好的回退(good fallback)。

其实现由三个类协作完成:

类职责源码位置
KHRSwapchainVK对外门面,持有 impl、尺寸与 MSAA 开关khr_swapchain_vk.h
KHRSwapchainImplVK实际执行创建、获取、呈现的核心逻辑khr_swapchain_impl_vk.h
KHRSwapchainImageVK包装单个 swapchain 图像(image / image view)khr_swapchain_image_vk.h
表面格式与合成模式的选择

创建阶段的关键决策集中在 khr_swapchain_impl_vk.cc:

  • ChooseSurfaceFormat:先查询物理设备支持的 vk::SurfaceFormatKHR 列表,优先匹配 Impeller 默认颜色格式(ContextVK 的 GetDefaultColorFormat),否则在 B8G8R8A8Unorm 与 R8G8B8A8Unorm 两个 sRGB 选项中回退选择;若全部不支持则创建失败。
  • ChooseAlphaCompositionMode:按 eInherit → ePreMultiplied → ePostMultiplied → eOpaque 的优先级,从 surface capabilities 中挑选合成透明度模式。
SwapchainCreateInfo 的关键参数

创建信息(khr_swapchain_impl_vk.cc)中的几个参数值得关注:

  • presentMode = eFifo:使用 Vulkan 默认的 FIFO 垂直同步呈现模式,这是兼容性最好的选择;
  • imageExtent:将请求尺寸 clamp 到 minImageExtent 与 maxImageExtent 之间,避免越界;
  • minImageCount:以 minImageCount + 1 为期望值,并在 maxImageCount == 0(表示无上限)时做保护性处理;
  • imageUsage:包含 eColorAttachment(颜色附件,经 resolve 使用)、eTransferDst(blit 目标)与 eInputAttachment(输入附件);
  • clipped = true:告知 Vulkan 引擎永远不会回读该缓冲,注释中以 Metal 的 [CAMetalLayer framebufferOnly] 作类比;
  • preTransform = eIdentity:不做 Vulkan 预旋转,旋转场景通过轮询尺寸变化处理;
  • oldSwapchain:重建时传入旧交换链,实现图像的无缝交接。
帧同步机制:三个同步原语 + 三帧在飞

KHRSwapchainImplVK 为每帧在飞(frame in flight)维护一组同步对象(khr_swapchain_impl_vk.cc):

  • acquire(fence):以已信号状态创建,用于 CPU 端等待上一轮该帧槽的渲染彻底完成;
  • render_ready(semaphore):acquireNextImageKHR 在获取到图像时对其发信号,通知 GPU 该图像已可用;
  • present_ready(semaphore):渲染完成后在最终命令缓冲提交时发信号,供呈现队列等待。

帧在飞数量由 kMaxFramesInFlight = 3u 限定(khr_swapchain_impl_vk.cc),即同时允许三帧处于渲染管线中,以平衡延迟与吞吐。

获取下一帧(AcquireNextDrawable,khr_swapchain_impl_vk.cc)的流程为:

  1. 轮转 current_frame_,对当前帧槽的 fence 执行 waitForFences + resetFences(无限超时);
  2. 调用 acquireNextImageKHR,使用无限超时并携带 render_ready 信号量——代码注释特别提醒:某些旧 Android 设备不支持非无限超时,且只会产生令人困惑的日志刷屏,因此直接使用无限超时;
  3. 对返回结果分类处理:eSuccess 继续;eSuboptimalKHR / eErrorOutOfDateKHR 属于可恢复错误,标记为 out of date;其余为不可恢复错误,记录日志并返回失败;
  4. 将索引对应的 KHRSwapchainImageVK 包装成 SurfaceVK,并注册一个 swap 回调,回调最终会调用 Present(image, index)。

呈现(Present,khr_swapchain_impl_vk.cc)则分为三个阶段:

  1. 创建最终命令缓冲,将图像布局通过 barrier 从颜色附件输出过渡到 ePresentSrcKHR;
  2. 提交该命令缓冲,等待 render_ready、信号 present_ready,并用 acquire fence 作为提交完成信号;
  3. 调用 vkQueuePresentKHR 呈现图像,对 eErrorOutOfDateKHR、eErrorSurfaceLostKHR、eSuboptimalKHR 三类结果统一按“可恢复/保证队列操作仍会完成”处理,交给下一次获取时重建。

2.2 AHB Swapchain:Android 上的硬件缓冲直通方案

面向 Android 的 AHB(Android Hardware Buffer)交换链位于 ahb 子目录。其头文件注释给出了明确的适用边界(ahb_swapchain_vk.h):

该交换链并非在 Flutter 支持的所有 Android 版本上都可用。必须执行 IsAvailableOnPlatform 检查,若无法创建则回退到 KHR 交换链。此类交换链所需的基础构件仅存在于 Android API level >= 29。

IsAvailableOnPlatform() 的实现(ahb_swapchain_vk.cc)同时要求 SurfaceControl::IsAvailableOnPlatform() 与 HardwareBuffer::IsAvailableOnPlatform() 为真。

与 KHR 路径的核心差异在于图像的来源与呈现通道:

  • 图像不是由 VkSwapchainKHR 分配,而是来自 AHBTexturePoolVK 硬件缓冲纹理池(ahb_texture_pool_vk.h);
  • 呈现目标不是呈现队列,而是 android::SurfaceControl——Impeller 通过 Android Surface 事务(ASurfaceTransaction)把硬件缓冲交给系统合成器(ahb_swapchain_impl_vk.cc 起的 Present 实现);
  • 缓冲数量由 surface capabilities 计算得出(minImageCount + 1 并 clamp),并维护一个 pending_presents_ 信号量,其上限 kMaxPendingPresents = 3u(ahb_swapchain_impl_vk.cc):2 张图像交给系统合成器、1 张留给栅格线程;
  • 同步通过 外部 fence(ExternalFenceVK)与 render-ready fence 完成,GPU 需要等待 render-ready fence 被信号后才开始渲染(SubmitWaitForRenderReady),呈现完成后在 ASurfaceTransactionStats 回调(OnTextureUpdatedOnSurfaceControl)中确认纹理已被消费。

AHBSwapchainImplVK 按尺寸管理生命周期:每个尺寸对应一个 impl 实例,尺寸变化会导致该尺寸的 impl 连同其全部缓存与 transients 一起被丢弃重建(见 ahb_swapchain_impl_vk.h 的注释与 ahb_swapchain_vk.cc 的 UpdateSurfaceSize 实现——尺寸不变则直接复用,变化才重建)。这也是它在 Android 上比 KHR 更优的主要原因:图像直接来自硬件缓冲,可绕开一次额外的格式转换与拷贝。

三、Surface 包装与 transients 缓存:把交换链图像变成渲染目标

无论哪种交换链,最终获得的“可绘制表面”都被统一包装成 SurfaceVK(surface_vk.h),其注释说明这是为了“提供 Impeller 将交换链图像用作屏幕渲染目标所需的额外配置”。WrapSwapchainImage 同时负责创建配套的 MSAA 与深度模板纹理,并通过 SwapCallback 将 Present() 与交换链的呈现逻辑绑定。

这里有一个值得注意的性能设计:SwapchainTransientsVK(swapchain_transients_vk.h)。注释指出,被它缓存的是“直觉上创建很便宜、但在某些 Vulkan 驱动上实测非常耗时”的资源——设备本地的 MSAA 纹理与深度模板纹理。由于同一交换链的所有图像共享同一组 transients,这些纹理只需创建一次,在整条交换链生命周期内被所有 swapchain 图像复用(MSAA 与深度模板天然不依赖具体图像内容,只依赖尺寸与格式,因此可安全共享)。enable_msaa 参数控制是否启用 MSAA 与对应的缓存。

由此可以梳理出 Impeller 一帧的完整数据流:

SwapchainVK::AcquireNextDrawable()
      │
      ▼
  获取可呈现图像(KHR: acquireNextImageKHR / AHB: 纹理池弹出)
      │
      ▼
  SurfaceVK::WrapSwapchainImage(绑定 transients:MSAA + 深度模板)
      │
      ▼
  上层渲染(以交换链图像为最终 resolve 目标)
      │
      ▼
  SurfaceVK::Present() ──> swap 回调
      │
      ├─ KHR: 布局过渡 → 提交 → vkQueuePresentKHR
      └─ AHB: 外部 fence 同步 → SurfaceControl 事务呈现

四、如何阅读与验证:源码索引

若希望进一步深入,可按以下路径在仓库中对照阅读:

五、小结

Impeller 的 Vulkan 交换链模块回答了“在不同 WSI 平台上如何高效呈现渲染结果”这一问题,其设计要点可以概括为:

  1. 统一抽象:SwapchainVK 将 surface 失效、尺寸变化、格式协商等平台差异全部收敛到四个接口方法之内,上层渲染无需感知平台差异;
  2. 双实现 + 回退策略:KHR Swapchain 基于 VK_KHR_swapchain,覆盖绝大多数平台且作为兜底;AHB Swapchain 在 Android API 29+ 上通过硬件缓冲 + SurfaceControl 直达系统合成器,是 Android 平台的高性能选项,不可用时自动回退;
  3. 工程细节到位:三帧在飞的同步结构、可恢复错误与不可恢复错误的严格区分、oldSwapchain 无缝重建、transients 跨图像复用等,都是移动端 Vulkan 渲染中直接影响流畅度与兼容性的关键处理。

对于需要阅读或扩展 Flutter Impeller Vulkan 后端的开发者而言,swapchain 目录是理解“渲染到屏幕”这一环节的最佳入口,也是观察 Vulkan WSI 最佳实践的一个高质量参考实现。

  • 跨平台
  • 图形学
  • 前端

【免费下载链接】engine

The Flutter engine

项目地址: https://gitcode.com/gh_mirrors/eng/engine
点击查看 免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值