HAR 图形流水线

本页详细介绍了高可用性渲染器 (HAR) 的完整图形流水线,跟踪了从 Figma 设计文档到屏幕上显示的最终像素的数据流。

概览

该流水线将高级界面定义转换为低级图形命令,并高效地在硬件显示屏上呈现这些命令。该流水线专为汽车安全关键型应用而设计,强调确定性渲染、高效的状态管理以及与平台图形子系统(例如直接渲染管理器 [DRM] 和通用缓冲区管理 [GBM])的稳健交互。

流水线可分为四个主要阶段:

  1. 预渲染:处理场景图、应用自定义设置和解析布局。
  2. 命令生成:将已解析的场景图转换为与后端无关的显示列表。
  3. 渲染:使用 Impeller 图形引擎执行绘制命令。
  4. 演示:管理帧缓冲区并与显示硬件同步。

HAR 图形流程

图 1. HAR 图形流程。

第 1 阶段:预渲染

此阶段会将静态 Figma 设计和动态应用状态转换为完全解析的内存中界面树,以便进行渲染。此阶段在专用的 reducer 线程上运行,与主显示循环分开。

1.1 DesignCompose 基础

HAR 流水线基于 DesignCompose 生态系统构建。

  • 来源:界面在 Figma 中设计,并使用 DesignCompose 插件导出。
  • 定义:输出是 DesignComposeDefinition 的一个实例,即设计(节点、样式、变体)的序列化表示形式。
  • 数据绑定:应用界面模型使用过程宏(例如 #[Design(node = "#speed")])将 Rust 结构体字段显式绑定到 Figma 文档中的特定命名节点。这样,应用状态就可以自动驱动视觉元素的属性。

此基础的关键组成部分包括:

  • Reducer:充当中央事件循环,处理操作并更新当前状态。框架提供 DefaultReducer,但如果需要,可以提供自定义的精简器实现。
  • Presenter:将当前状态与界面模型相关联。Presenter 特征由 harry 框架 crate 指定,参考实现 (UIModelPresenter) 在 harry-app-core crate 中提供。
  • 界面模型:根据当前状态生成自定义设置。界面模型代码是使用 derive_customizations crate 提供的 DesignDocument 宏生成的。harry-app-core crate 中的 UIModel 结构体提供了一个示例。
  • Squoosh:提供 SquooshView 数据结构和变体代码库,用于根据设计呈现界面。序列化的设计文档由 dc_bundle crate 从 DesignCompose 库加载,并转换为 SquooshView 结构的树,以实现高效的运行时性能。

1.2 Reducer 循环

流水线由操作驱动。该框架指定了 Actions 枚举类型,用于定义框架本身使用的内部操作,但还包含 CustomAction 变体,使用户能够定义其他应用专用操作(例如 UpdateVehicleSpeedButtonPress)。

该框架还提供 StateAction 特征,可简化影响应用状态的操作的实现,并选择性地生成副作用,然后从 reducer 传递回应用以进行处理。harry-app-core crate 中的 CustomActions 枚举对此进行了详细说明。

以下是精简器循环的基本大纲:

  • 操作处理Reducer 接收操作并更新当前状态。这是原始数据,例如当前速度或哪些指示灯(警告灯)处于亮起状态。这还可能会产生副作用(例如,当安全带指示灯闪烁时,信号会播放铃声)。
  • 演示Presenter 将新状态映射到 UIModelUIModel 是一个视图模型,用于保存专门为界面设置格式的数据(例如,将“120”速度格式化为字符串“65 mph”)。
  • 自定义生成:调用界面模型的 apply 方法来生成一组 RenderCustomization 实例。这些是用于修改 Figma 设计的明确指令(例如,“将节点 #speed 的文本设置为‘65 mph’”)。
  • UpdatePolicy 用于优化:每次预渲染传递后,系统都会返回一个 UpdatePolicy 值,用于指示何时需要进行下一次渲染更新。如果没有待处理的状态变化,也没有正在运行的动画,则 UpdatePolicy 表示暂时不需要进一步更新。在这种情况下,Reducer 会停止生成新的显示列表,从而防止不必要的渲染周期并节省资源,直到新的操作或事件触发更改。

1.3 查看注入和代码库初始化

流水线以 DesignComposeDefinition 实例开头。这是由 DesignCompose 序列化为协议缓冲区结构的 Figma 设计文档。

  • 初始加载:启动时,系统会将主要设计(由其根节点指定)从 DesignComposeDefinition 转换为初始 SquooshView 树。此过程是一次性的。

  • 代码库SquooshVariantRepository 管理可重用组件变体和初始加载的视图。

  • 延迟加载:为了最大限度地缩短启动时间和减少内存用量,系统仅在渲染逻辑明确引用并需要时(例如在列表自定义期间)才从文档中延迟加载其他视图(即不属于初始根节点树的视图)。

1.4 自定义通行证

遍历 SquooshView 树以应用动态应用状态:

  • 变体交换:根据运行时逻辑,组件实例会与特定变体进行交换(例如,将表示当前驾驶模式的图标从运动模式更改为经济模式)。

  • 列表展开:Figma 中的单个模板项会被替换为动态子项列表。系统会为这些子项生成新的唯一 ID,以验证动画的稳定身份。

  • 文本和样式替换:文本内容(例如速度值)和样式(例如不透明度、颜色)会根据当前状态进行更新。

1.5 可变分辨率

解析在 Figma 中或在应用本地定义的样式令牌和变量。

  • 绑定:引用变量(例如颜色或尺寸)的 SquooshView 属性会被替换为当前帧的实际值。

1.6 布局计算

  • 动态布局DynamicLayout 计算 SquooshView 树中每个节点的最终位置和大小(边界)。

  • 文字布局TextHelper 使用 LayoutHelper 特征的实现来计算文字指标、换行和字形。这有助于在渲染之前验证文本是否在限制范围内正确流动。

1.7 表盘和仪表

这是汽车界面的专用步骤。

  • MeterData:如果节点具有仪表数据(在 Figma 中定义),则其几何形状会根据 meter_value(例如车速)动态改变。
    • 弧形:扫描角度已调整。
    • 旋转:旋转转换是根据起始角度和结束角度计算的。
    • 进度条:矩形的宽度或高度会进行缩放。
    • 进度向量:调整向量路径的长度。

1.8 动画

  • 差异比较:将当前 SquooshViewPreRenderCache 中的 previous_squoosh_view 进行比较。

  • 插值:如果属性已更改,Squoosh 会创建插值器,以便随时间平滑过渡值(例如不透明度或转换)。

第 2 阶段:命令生成

SquooshView 树完全解析并动画化后,它会转换为一系列线性绘制命令。

此阶段的关键组件是 DisplayList crate:

  • generate_dl:此函数以递归方式遍历 SquooshView 树。

  • 翻译:

    • 形状和路径:转换为具有相应 DisplayListAppearance 变体(例如 RectPath)的 DisplayListEntry
    • 文字:使用 TextHelper 转换为文字绘制条目。
    • 转换和剪辑:转换为 PushTransform3DPopTransform3DPushClipRegionPopClipRegion 对,以管理绘制状态堆栈。
    • 遮盖:转换为 PushMaskLayerPopMaskLayer 对,以正确创建和混合图层。

最终结果是一个 Vec<DisplayListEntry> 实例,用于描述要绘制的内容,而与绘制方式无关。

2.1 切换到 Looper

生成 DisplayList 后,Reducer 会将其封装在 ViewDescriptor 的实例中,并通过 Rust MPSC 渠道 (LooperMessage) 将其发送到 Looper 线程。Looper 负责渲染和显示阶段,这可防止 Reducer 线程阻塞图形流水线。

第 3 阶段:渲染

与平台无关的 DisplayList 会移交给渲染后端,在其中,抽象命令会转换为 GPU 指令。

HAR 使用 Impeller,这是一种最初为 Flutter 构建的渲染引擎。Impeller 旨在通过在构建时预编译一组小巧高效的着色器,解决因着色器编译而导致的帧速率故障问题。这种方法与有效的批处理和高度优化的后端相结合,可实现以下目标:

  • 确定性性能:几乎消除了运行时着色器编译故障。
  • 快速启动:减少了初始化开销。
  • 占用空间小:生成紧凑的二进制文件大小。

如需详细了解 Impeller 的架构,请观看 [Introducing Impeller - Flutter's new rendering engine][impeller-video]。虽然视频讨论的是 Flutter,但这些核心优势直接赋能了 HAR 汽车堆栈。

渲染阶段的关键组成部分包括:

  • ImpellerRenderer:将预渲染阶段的显示列表转换为 Impeller 渲染命令。

  • Impeller Rust API:封装 Impeller 库,以便在 Rust 中使用(impellerimpeller-rs-bindgen crate)。

  • TypographyContext:管理字体注册和文本塑形。

impeller-video

3.1 初始化和 Surface 管理

  • 上下文创建:渲染器使用 OpenGL ES 后端初始化 impeller::Context 的实例,并传递一个回调来解析平台 GL 上下文中的 OpenGL ES 函数指针。

  • 封装的 FBO Surface:Impeller 不会创建自己的窗口,而是渲染到由 Phase 4 提供的现有 OpenGL 帧缓冲区对象 (FBO) 中。只需调用 Surface::create_wrapped_fbo 即可实现这一点。

3.2 资源管理

  • 图片:支持标准格式和 KTX2 压缩纹理。这些数据会上传到 GPU 纹理,并由内部 Resources 结构体进行管理。

  • 字体:TrueType 和 OpenType 字体已加载并注册到 TypographyContext 以进行文本渲染。

  • 外部图片:对外部纹理(例如相机 Feed 和外部 3D 渲染器)的专门处理涉及将 EGLImage 实例或外部 OpenGL 纹理绑定到 Impeller Texture 对象,以实现零复制渲染。

3.3 渲染通道

render 循环使用 DisplayListBuilder 构建 Impeller DisplayList 实例(不要与预渲染阶段生成的 Vec<DisplayListEntry> 混淆):

  1. 清除缓冲区并应用全局转换以进行 DPI 缩放和显示旋转。

  2. 遍历输入 DisplayListEntry 项:

    • 状态save()restore() 用于推送和弹出转换以及剪裁区域。
    • 基元RectRoundedRect 是使用标准绘制操作绘制的。
    • 路径:构建并绘制复杂的矢量路径(包括动态 Arc 实例)。
    • 文本TextStyledText 使用 TypographyContext 进行渲染。
    • 图片:标准图片和外部图片使用 draw_texture_rect 绘制。
  3. 使用 surface.draw_display_list() 将构建的 Impeller 显示列表提交到 surface,从而生成底层 GL 命令。

  4. 对底层上下文调用 swap_buffers() 以触发第 4 阶段。

第 4 阶段:演示

此最终阶段负责处理与显示硬件的互动,以显示渲染的帧。HAR 在 Android Automotive OS (AAOS) 软件定义型车辆 (SDV) 上使用强大的直接渲染路径。

此阶段的关键组件是 HarDirectRenderingContext(位于 har-gl-context crate 中)。

4.1 架构

呈现层使用双缓冲方法,并采用屏幕外绘制目标:

  1. 绘制缓冲区:Impeller 在其中渲染场景的屏幕外 FBO。

  2. 解决缓冲区(可选):支持多重采样抗锯齿 (MSAA) 的可选辅助缓冲区

    • 底层 OpenGL ES 实现或配置可在需要时启用此功能。在这种情况下,它充当中间目标,用于在 blit(位块传输)到渲染缓冲区之前解析多重采样绘制缓冲区。
  3. 渲染缓冲区:由 GBM 对象支持的通用缓冲区,对应于典型图形交换链中的后缓冲区。

  4. 前端缓冲区:扫描到显示屏的 GBM 缓冲区。

4.2 交换链

swap_buffers 被调用时,HAR 会按以下步骤操作:

  1. 将绘制缓冲区的内容 blit 到渲染缓冲区(如果实现需要,则通过中间 blit 到解析缓冲区)。

  2. 对 GL 上下文调用 glFlush(),并创建 EGL_SYNC_NATIVE_FENCE_ANDROID 的实例以跟踪 GPU 完成情况。

  3. 构建 DRM 原子请求,以将渲染缓冲区交换到屏幕。此请求包含 GPU 栅栏 FD(称为入栅栏),以防止显示控制器在 GPU 完成绘制之前显示渲染缓冲区。

  4. 同时从 DRM 请求一个新栅栏(称为输出栅栏),以便在上一个缓冲区(前一帧的前缓冲区)不再显示在屏幕上时发出信号。

  5. 使用非阻塞标志提交原子请求,以使主线程在图形子系统保持同步的同时继续运行。

  6. 将新的 out fence 存储在上下文中,以便 HAR 可以在后续帧上 swap_buffers 进程开始时等待其发出信号。这样可以防止 GPU 绘制到仍在显示的缓冲区。

4.3 直接模式设置

HAR 使用 DRM 和 Kernel Mode Setting (KMS) 子系统直接与内核交互,以配置显示分辨率 AAOS SDV,绕过与 SurfaceFlinger 等窗口管理器的交互(在特定配置中),从而实现对显示硬件的独占式高优先级控制。

4.4 外部渲染

HAR 支持将特定界面元素(由 Figma 中的标记标识)的渲染委托给外部进程或线程。这对于集成复杂的 3D 场景(例如,来自 Kanzi 或 Unity 等引擎的自我车辆可视化)或需要专用 OpenGL 上下文的其他内容非常有用。

4.4.1 主要组件

  • HarExternalRenderContext:用于外部服务的专用屏幕外 EGL 上下文。
  • SurfacePool:管理一组用于双重或三重缓冲的 LocalSurfaceTextureEGLImage)缓冲区。
  • SharedSurfaceExternalImage:一种线程安全型封装容器,用于在外部服务和主渲染器之间传递 EGLImage 句柄。

4.4.2 工作流

工作流程遵循以下顺序:

  1. 外部服务启动并向主 Looper 注册自身,以标识其渲染的 Figma 标记(例如 #cluster/3d-car)。

  2. 该服务会等待来自 Looper 的 RenderStart 信号,以使其渲染与显示屏的 VSYNC 信号保持一致。

  3. 在屏幕外,服务将其内容渲染到 SurfacePool 提供的帧缓冲区中。

  4. 服务在其上下文中调用 swap_buffers,这会轮换池并使已完成的帧作为 SharedSurface 的实例提供。

  5. SharedSurface 封装在 ExternalImage 中,并通过 Rust MPSC 渠道发送到 Looper。

  6. 主 Impeller 渲染器(第 3 阶段)接收外来图片。它不会复制像素数据,而是将底层 EGLImage 直接绑定到纹理并将其绘制为主要场景的一部分,从而实现零复制合成。

4.5 开发和测试平台 (har-platform-linux)

出于开发和测试目的,HAR 应用可以面向标准 Linux 桌面环境和无头设置。这些平台是在 crates/reference/platforms/har-platform-linux crate 中实现的。

与正式版 AAOS SDV 目标平台不同,这些平台不使用 har-gl-contextdirect-rendering 子系统进行显示输出。而是依赖于标准 Rust OpenGL crate:

  • 窗口模式:使用 winit 进行窗口管理和事件循环,并使用 glutin 创建 OpenGL ES 上下文并与窗口系统集成。

  • 无头模式:使用 har-gl-context crate 创建具有默认 EGL 显示的屏幕外 pbuffer 上下文。这样一来,无需可见窗口或直接访问显示硬件,即可渲染到屏幕外缓冲区,主要用于自动化测试或后端处理。