3、显示框架原理:Android SurfaceFlinger与HWC详解、QNX Screen Graphics Subsystem、Linux DRM/KMS框架
好,我们进入第三讲。这一讲我打算把三个主流车载系统的显示框架掰开揉碎了讲清楚。你想想看,多屏异显异触的核心,说白了就是「谁在管屏幕,怎么管」。Android、QNX、Linux 这三套方案,我都在项目里摸爬滚打踩过坑,今天把干货倒出来。
3.1 Android SurfaceFlinger 与 HWC
Android 的显示链路,我习惯把它分成三层:应用层画图、SurfaceFlinger 合成、HWC 送显。很多刚入行的朋友以为 SurfaceFlinger 就是最终干活的,其实它只是个「调度员」。
核心认知:SurfaceFlinger 负责决定「怎么合成」,HWC(Hardware Composer)负责「实际合成」。HWC 能走硬件就绝不走软件,这是性能关键。
3.1.1 SurfaceFlinger 的工作流
我简单描述一下流程:每个应用窗口对应一个 Surface,SurfaceFlinger 收到 VSync 信号后,开始收集所有可见 Surface 的 Buffer。然后它调用 HWC 的接口,问:「老兄,这些图层你能硬件合成几个?」
HWC 会回答:「我能合 3 个,剩下 2 个你软件合成。」于是 SurfaceFlinger 用 GPU 把剩下的图层合成一张图,再交给 HWC 去显示。
嗯,这里要注意:车载多屏场景下,每个屏幕都有自己的 DisplayDevice。SurfaceFlinger 会为每个屏幕维护一个独立的合成计划。我曾经在项目中遇到过一个问题:副屏的刷新率是 30fps,主屏是 60fps,结果 SurfaceFlinger 的 VSync 信号乱了套。后来怎么解决的?我强制给每个屏幕分配了独立的 VSync 源。
3.1.2 HWC 的硬件加速
HWC 是硬件厂商实现的 HAL 层模块。说白了,它就是个「硬件加速器」。它能做的事情包括:图层叠加、旋转、缩放、色彩转换。
车载场景下,HWC 最头疼的是什么?是图层数量。仪表盘、中控、HUD,每个屏幕可能都有 5-8 个图层。如果 HWC 硬件只能叠加 4 层,剩下的就得走 GPU 合成,功耗和延迟就上去了。
我的经验:在选 SoC 时,一定要确认 HWC 支持的硬件图层数。我见过一个项目,选了某款芯片,结果硬件只支持 3 层叠加,仪表盘导航一开就卡顿。最后只能改 UI 设计,把图层合并到 3 层以内。
3.1.3 关键代码路径
我们看一段 SurfaceFlinger 的核心逻辑,简化后的伪代码:
// SurfaceFlinger 主循环
void SurfaceFlinger::onMessageReceived() {
// 1. 等待 VSync
waitForVSync();
// 2. 收集所有可见 Layer
Vector<LayerState> layers = collectVisibleLayers();
// 3. 调用 HWC 进行合成预算
HWC::Display display = getDisplayForScreen(screenId);
HWC::LayerCompositionResult result = display.prepare(layers);
// 4. 如果 HWC 不能处理所有图层,用 GPU 合成剩余部分
if (result.remainingLayers > 0) {
gpuComposeRemainingLayers(result.remainingLayers);
}
// 5. 提交到 HWC 显示
display.commit();
}
这段代码我改过无数次。最坑的一次是,某个芯片的 HWC 驱动在 prepare() 阶段会死锁,原因是驱动内部用了同一个 mutex。我当时的解决方案是:在 SurfaceFlinger 侧加一个超时机制,如果 prepare 超过 16ms,就强制走 GPU 合成。
3.2 QNX Screen Graphics Subsystem
QNX 的显示框架,和 Android 完全是两套思路。Android 是「应用驱动」,QNX 是「服务驱动」。QNX 的 Screen Graphics Subsystem,我习惯叫它「Screen 框架」。
3.2.1 Screen 的核心概念
Screen 框架里有几个关键对象:
- Screen Context:全局上下文,一个进程一个
- Screen Window:对应一个显示区域,可以绑定到某个 Display
- Screen Display:物理屏幕或虚拟屏幕
- Screen Buffer:显存中的图像数据
你想想看,在 QNX 里做多屏异显,其实就是创建多个 Display 对象,然后把 Window 绑定到不同的 Display 上。每个 Window 可以独立设置分辨率、刷新率、旋转角度。
关键区别:QNX 没有 SurfaceFlinger 这种中间层。应用直接通过 Screen API 操作显存。这意味着延迟更低,但开发复杂度更高。
3.2.2 多屏配置实战
我记得在某个项目中,需要把仪表盘和中控分别显示在两个物理屏幕上。QNX 的配置是这样的:
// 创建两个 Display
screen_display_t display_main, display_cluster;
screen_create_display(&display_main, context, "main");
screen_create_display(&display_cluster, context, "cluster");
// 设置 Display 属性
screen_set_display_property_iv(display_main, SCREEN_PROPERTY_SIZE, main_size);
screen_set_display_property_iv(display_cluster, SCREEN_PROPERTY_SIZE, cluster_size);
// 创建 Window 并绑定到 Display
screen_window_t win_main, win_cluster;
screen_create_window(&win_main, context);
screen_create_window(&win_cluster, context);
screen_set_window_property_pv(win_main, SCREEN_PROPERTY_DISPLAY, &display_main);
screen_set_window_property_pv(win_cluster, SCREEN_PROPERTY_DISPLAY, &display_cluster);
这里有个坑:QNX 的 Screen 框架默认是单 Display 模式。如果你不显式创建第二个 Display,系统会把所有 Window 都堆到主屏上。我曾经调试一个 bug,副屏一直黑屏,查了两天才发现是 Display 对象没创建成功。
3.2.3 性能与安全
QNX 的优势在于确定性。它的显示调度是实时的,不会像 Android 那样因为 GC 或者后台进程导致掉帧。在仪表盘场景下,这是刚需。
但 QNX 也有短板:图形栈比较封闭。它的 Screen API 是 QNX 私有的,不像 Android 那样有开源社区支持。如果你需要做复杂的特效,比如模糊、阴影,QNX 的硬件加速支持不如 Android 成熟。
避坑指南:我曾经在 QNX 上做 HUD 投影,需要把图像做畸变校正。QNX 的 Screen 框架不支持直接做几何变换,最后我只能用 GPU 的 compute shader 自己实现。如果你有类似需求,提前评估好 GPU 的算力。
3.3 Linux DRM/KMS 框架
Linux 的显示框架,我把它看作是「最灵活但也最原始」的方案。DRM(Direct Rendering Manager)负责显存管理,KMS(Kernel Mode Setting)负责显示模式设置。
3.3.1 DRM 的核心对象
DRM 框架里有几个关键概念:
| 对象 | 作用 | 类比 |
|---|---|---|
| CRTC | 显示控制器,负责扫描输出 | 相当于一个「显示引擎」 |
| Encoder | 编码器,将 CRTC 的信号转换成物理接口 | HDMI、LVDS、MIPI 的驱动 |
| Connector | 物理连接器,代表实际的屏幕接口 | HDMI 接口、eDP 接口 |
| Plane | 硬件图层,可以叠加显示 | 类似 HWC 的硬件图层 |
你想想看,在 Linux 上做多屏异显,本质就是:为每个屏幕分配一个 CRTC,然后绑定对应的 Encoder 和 Connector。
3.3.2 KMS 的配置示例
我习惯用 modetest 工具来调试 KMS。下面是一个典型的双屏配置流程:
// 打开 DRM 设备
int fd = open("/dev/dri/card0", O_RDWR);
// 获取资源
drmModeRes *resources = drmModeGetResources(fd);
// 遍历 Connector,找到两个屏幕
for (int i = 0; i < resources->count_connectors; i++) {
drmModeConnector *connector = drmModeGetConnector(fd, resources->connectors[i]);
if (connector->connection == DRM_MODE_CONNECTED) {
// 为每个屏幕创建一个 CRTC
uint32_t crtc_id = findAvailableCRTC(resources);
// 设置显示模式
drmModeCrtc *crtc = drmModeGetCrtc(fd, crtc_id);
drmModeSetCrtc(fd, crtc_id, fb_id, 0, 0, &connector->connector_id, 1, &connector->modes[0]);
}
}
嗯,这里要注意:Linux 的 DRM 驱动质量参差不齐。我遇到过某款 SoC 的 DRM 驱动,在切换显示模式时会闪屏。原因是驱动在 drmModeSetCrtc 时没有做 VBlank 同步。解决方案是:在调用前先等待 VBlank 信号。
3.3.3 多屏异显的挑战
Linux 方案的优势是完全可控。你可以直接操作硬件寄存器,做任何你想做的事情。但代价是:
- 没有现成的合成框架:你需要自己实现图层合成逻辑
- 驱动调试困难:一旦出问题,可能得啃芯片手册
- 生态碎片化:不同厂商的 DRM 驱动行为不一致
我的建议:如果你的项目对延迟要求极高(比如仪表盘),Linux DRM/KMS 是首选。但如果你需要快速开发,还是用 Android 或 QNX 的现成框架更稳妥。
3.4 三种框架的对比总结
最后,我用一张表总结一下三者的差异:
| 维度 | Android SurfaceFlinger+HWC | QNX Screen | Linux DRM/KMS |
|---|---|---|---|
| 抽象层级 | 高(应用层到 HAL) | 中(系统服务层) | 低(内核层) |
| 多屏支持 | 原生支持,但需配置 | 原生支持,配置灵活 | 需手动管理 CRTC |
| 硬件加速 | 通过 HWC 实现 | 通过 Screen API 实现 | 直接操作硬件 |
| 实时性 | 一般(受 GC 影响) | 高(确定性调度) | 高(无中间层) |
| 开发难度 | 中等 | 中等偏高 | 高 |
| 典型场景 | 中控娱乐、副驾屏 | 仪表盘、HUD | 定制化显示、实验平台 |
我个人习惯是:中控娱乐用 Android,仪表盘用 QNX,原型验证用 Linux。当然,具体选型还得看项目需求和团队能力。好了,这一讲就到这里,下一讲我们聊聊多屏触控的同步与冲突处理。