本页详细介绍了高可用性渲染器 (HAR) 的完整图形流水线,跟踪了从 Figma 设计文档到屏幕上显示的最终像素的数据流。
概览
该流水线将高级界面定义转换为低级图形命令,并高效地在硬件显示屏上呈现这些命令。该流水线专为汽车安全关键型应用而设计,强调确定性渲染、高效的状态管理以及与平台图形子系统(例如直接渲染管理器 [DRM] 和通用缓冲区管理 [GBM])的稳健交互。
流水线可分为四个主要阶段:
- 预渲染:处理场景图、应用自定义设置和解析布局。
- 命令生成:将已解析的场景图转换为与后端无关的显示列表。
- 渲染:使用 Impeller 图形引擎执行绘制命令。
- 演示:管理帧缓冲区并与显示硬件同步。
图 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-corecrate 中提供。 - 界面模型:根据当前状态生成自定义设置。界面模型代码是使用
derive_customizationscrate 提供的DesignDocument宏生成的。harry-app-corecrate 中的UIModel结构体提供了一个示例。 - Squoosh:提供
SquooshView数据结构和变体代码库,用于根据设计呈现界面。序列化的设计文档由dc_bundlecrate 从 DesignCompose 库加载,并转换为SquooshView结构的树,以实现高效的运行时性能。
1.2 Reducer 循环
流水线由操作驱动。该框架指定了 Actions 枚举类型,用于定义框架本身使用的内部操作,但还包含 CustomAction 变体,使用户能够定义其他应用专用操作(例如 UpdateVehicleSpeed 或 ButtonPress)。
该框架还提供 StateAction 特征,可简化影响应用状态的操作的实现,并选择性地生成副作用,然后从 reducer 传递回应用以进行处理。harry-app-core crate 中的 CustomActions 枚举对此进行了详细说明。
以下是精简器循环的基本大纲:
- 操作处理:
Reducer接收操作并更新当前状态。这是原始数据,例如当前速度或哪些指示灯(警告灯)处于亮起状态。这还可能会产生副作用(例如,当安全带指示灯闪烁时,信号会播放铃声)。 - 演示:
Presenter将新状态映射到UIModel。UIModel是一个视图模型,用于保存专门为界面设置格式的数据(例如,将“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 动画
差异比较:将当前
SquooshView与PreRenderCache中的previous_squoosh_view进行比较。插值:如果属性已更改,
Squoosh会创建插值器,以便随时间平滑过渡值(例如不透明度或转换)。
第 2 阶段:命令生成
在 SquooshView 树完全解析并动画化后,它会转换为一系列线性绘制命令。
此阶段的关键组件是 DisplayList crate:
generate_dl:此函数以递归方式遍历SquooshView树。翻译:
- 形状和路径:转换为具有相应
DisplayListAppearance变体(例如Rect或Path)的DisplayListEntry - 文字:使用
TextHelper转换为文字绘制条目。 - 转换和剪辑:转换为
PushTransform3D和PopTransform3D或PushClipRegion和PopClipRegion对,以管理绘制状态堆栈。 - 遮盖:转换为
PushMaskLayer和PopMaskLayer对,以正确创建和混合图层。
- 形状和路径:转换为具有相应
最终结果是一个 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 中使用(
impeller和impeller-rs-bindgencrate)。TypographyContext:管理字体注册和文本塑形。
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 纹理绑定到 ImpellerTexture对象,以实现零复制渲染。
3.3 渲染通道
render 循环使用 DisplayListBuilder 构建 Impeller DisplayList 实例(不要与预渲染阶段生成的 Vec<DisplayListEntry> 混淆):
清除缓冲区并应用全局转换以进行 DPI 缩放和显示旋转。
遍历输入
DisplayListEntry项:- 状态:
save()和restore()用于推送和弹出转换以及剪裁区域。 - 基元:
Rect和RoundedRect是使用标准绘制操作绘制的。 - 路径:构建并绘制复杂的矢量路径(包括动态
Arc实例)。 - 文本:
Text和StyledText使用TypographyContext进行渲染。 - 图片:标准图片和外部图片使用
draw_texture_rect绘制。
- 状态:
使用
surface.draw_display_list()将构建的 Impeller 显示列表提交到 surface,从而生成底层 GL 命令。对底层上下文调用
swap_buffers()以触发第 4 阶段。
第 4 阶段:演示
此最终阶段负责处理与显示硬件的互动,以显示渲染的帧。HAR 在 Android Automotive OS (AAOS) 软件定义型车辆 (SDV) 上使用强大的直接渲染路径。
此阶段的关键组件是 HarDirectRenderingContext(位于 har-gl-context crate 中)。
4.1 架构
呈现层使用双缓冲方法,并采用屏幕外绘制目标:
绘制缓冲区:Impeller 在其中渲染场景的屏幕外 FBO。
解决缓冲区(可选):支持多重采样抗锯齿 (MSAA) 的可选辅助缓冲区
- 底层 OpenGL ES 实现或配置可在需要时启用此功能。在这种情况下,它充当中间目标,用于在 blit(位块传输)到渲染缓冲区之前解析多重采样绘制缓冲区。
渲染缓冲区:由 GBM 对象支持的通用缓冲区,对应于典型图形交换链中的后缓冲区。
前端缓冲区:扫描到显示屏的 GBM 缓冲区。
4.2 交换链
当 swap_buffers 被调用时,HAR 会按以下步骤操作:
将绘制缓冲区的内容 blit 到渲染缓冲区(如果实现需要,则通过中间 blit 到解析缓冲区)。
对 GL 上下文调用
glFlush(),并创建EGL_SYNC_NATIVE_FENCE_ANDROID的实例以跟踪 GPU 完成情况。构建 DRM 原子请求,以将渲染缓冲区交换到屏幕。此请求包含 GPU 栅栏 FD(称为入栅栏),以防止显示控制器在 GPU 完成绘制之前显示渲染缓冲区。
同时从 DRM 请求一个新栅栏(称为输出栅栏),以便在上一个缓冲区(前一帧的前缓冲区)不再显示在屏幕上时发出信号。
使用非阻塞标志提交原子请求,以使主线程在图形子系统保持同步的同时继续运行。
将新的 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:管理一组用于双重或三重缓冲的LocalSurface(Texture加EGLImage)缓冲区。SharedSurfaceExternalImage:一种线程安全型封装容器,用于在外部服务和主渲染器之间传递EGLImage句柄。
4.4.2 工作流
工作流程遵循以下顺序:
外部服务启动并向主 Looper 注册自身,以标识其渲染的 Figma 标记(例如
#cluster/3d-car)。该服务会等待来自 Looper 的
RenderStart信号,以使其渲染与显示屏的 VSYNC 信号保持一致。在屏幕外,服务将其内容渲染到
SurfacePool提供的帧缓冲区中。服务在其上下文中调用
swap_buffers,这会轮换池并使已完成的帧作为SharedSurface的实例提供。SharedSurface封装在ExternalImage中,并通过 Rust MPSC 渠道发送到 Looper。主 Impeller 渲染器(第 3 阶段)接收外来图片。它不会复制像素数据,而是将底层
EGLImage直接绑定到纹理并将其绘制为主要场景的一部分,从而实现零复制合成。
4.5 开发和测试平台 (har-platform-linux)
出于开发和测试目的,HAR 应用可以面向标准 Linux 桌面环境和无头设置。这些平台是在 crates/reference/platforms/har-platform-linux crate 中实现的。
与正式版 AAOS SDV 目标平台不同,这些平台不使用 har-gl-context 的 direct-rendering 子系统进行显示输出。而是依赖于标准 Rust OpenGL crate:
窗口模式:使用
winit进行窗口管理和事件循环,并使用glutin创建 OpenGL ES 上下文并与窗口系统集成。无头模式:使用
har-gl-contextcrate 创建具有默认 EGL 显示的屏幕外 pbuffer 上下文。这样一来,无需可见窗口或直接访问显示硬件,即可渲染到屏幕外缓冲区,主要用于自动化测试或后端处理。