13、显示与GPU驱动开发:QNX Screen Graphics Subsystem、Android SurfaceFlinger与HWC、GPU驱动(Adreno/Mali)集成、双屏异显实现
显示系统,说白了就是座舱的“脸面”。用户坐进车里,第一眼看到的就是屏幕。如果屏幕闪一下、延迟一下,体验立马就崩了。我在做第一代智能座舱项目时,就吃过这个亏——双屏异显死活调不通,最后发现是GPU的合成策略没选对。今天咱们就把这块彻底讲透。
13.1 QNX Screen Graphics Subsystem
QNX的显示架构,核心就是Screen这个图形子系统。它不像Linux那样走DRM/KMS,而是自己搞了一套完整的显示管线。
13.1.1 Screen架构概览
Screen本质上是一个客户端-服务器模型。应用程序作为客户端,通过libscreen库与Screen服务通信。Screen服务则直接操作GPU和显示硬件。
核心概念:
- Display:物理显示设备,比如LVDS、HDMI、DP接口
- Screen:逻辑显示区域,一个Display可以包含多个Screen
- Window:应用创建的绘图表面,每个Window对应一个图层
- Pixmap:离屏缓冲区,用于后台渲染
我个人习惯把Screen理解成“显示资源管理器”。它管理着所有显示相关的资源分配和调度。
13.1.2 Screen Buffer管理
Screen支持多种Buffer模式,最常用的是双缓冲和三缓冲。我建议在座舱场景下用三缓冲,虽然多占点内存,但能有效避免撕裂。
// 创建双缓冲Window
screen_window_t win;
screen_create_window(&win, screen_ctx);
screen_set_window_property_iv(win, SCREEN_PROPERTY_BUFFER_COUNT, &(int){2});
// 设置格式为ARGB8888
int format = SCREEN_FORMAT_RGBA8888;
screen_set_window_property_iv(win, SCREEN_PROPERTY_FORMAT, &format);
// 获取Buffer
screen_buffer_t buffers[2];
screen_get_window_property_pv(win, SCREEN_PROPERTY_BUFFERS, (void**)buffers);
// 渲染循环
while(1) {
screen_buffer_t buf;
screen_get_window_property_pv(win, SCREEN_PROPERTY_RENDER_BUFFER, (void**)&buf);
// 绘制内容到buf
screen_post_window(win, buf, 1, NULL, 0);
screen_flush_blits(screen_ctx);
}
避坑指南:我曾经在QNX 6.6上遇到一个坑——SCREEN_PROPERTY_RENDER_BUFFER返回的Buffer指针在双屏场景下会乱序。后来发现是Screen服务没正确同步两个Display的VSync。解决办法是手动调用screen_sync()强制同步。
13.1.3 Screen与GPU的交互
Screen本身不直接做渲染,它只是把Window交给GPU去画。GPU驱动通过Screen的扩展接口(比如screen_context_set_gpu)注册自己的渲染回调。
嗯,这里要注意:Screen的合成策略有两种——CPU合成和GPU合成。CPU合成适合图层少的场景,GPU合成适合复杂场景。我一般建议图层超过3个就用GPU合成。
13.2 Android SurfaceFlinger与HWC
Android的显示架构比QNX复杂得多。SurfaceFlinger负责合成所有应用的Surface,HWC(Hardware Composer)则负责把合成结果送到显示硬件。
13.2.1 SurfaceFlinger的工作流程
SurfaceFlinger本质上是一个合成引擎。它接收每个应用的Surface,然后决定怎么合成。我把它总结为三步:
- 收集:从每个应用获取Surface的Buffer
- 决策:判断哪些图层用GPU合成,哪些交给HWC
- 输出:把最终结果送到Display
关键点:SurfaceFlinger的合成策略是动态的。如果图层有透明区域、旋转、缩放,它就会走GPU合成。否则,它会尝试走HWC的硬件合成。
13.2.2 HWC的作用
HWC是硬件合成器,它直接操作显示控制器。说白了,HWC就是硬件加速版的SurfaceFlinger。它能同时合成多个图层,而且不占用GPU资源。
我记得在调试一个双屏项目时,发现HWC只支持4个硬件图层。一旦超过4个,多余的图层就会fallback到GPU合成。这会导致性能下降。解决办法是尽量减少图层数量,或者合并图层。
// HWC 2.0 接口示例
class MyHWC : public hwc2_device_t {
// 获取显示属性
int getDisplayAttribute(uint32_t display, uint32_t config,
int32_t attribute, int32_t* outValue) {
switch(attribute) {
case HWC2_ATTRIBUTE_VSYNC_PERIOD:
*outValue = 16666667; // 60Hz
break;
case HWC2_ATTRIBUTE_WIDTH:
*outValue = 1920;
break;
case HWC2_ATTRIBUTE_HEIGHT:
*outValue = 720;
break;
}
return HWC2_ERROR_NONE;
}
// 合成图层
int validateDisplay(uint32_t display,
uint32_t* outNumTypes,
hwc2_layer_t* outLayers) {
// 检查哪些图层可以硬件合成
// 返回需要GPU合成的图层列表
return HWC2_ERROR_NONE;
}
};
注意:HWC的validateDisplay()调用非常频繁,每帧都会调用。如果这个函数实现得不好,会直接影响帧率。我曾经见过一个项目,validateDisplay里做了大量的内存分配,结果帧率直接掉到30fps。
13.3 GPU驱动集成:Adreno与Mali
座舱里最常见的GPU就是高通的Adreno和ARM的Mali。这两家在QNX和Android上都有成熟的驱动方案。
13.3.1 Adreno驱动集成
Adreno的驱动在QNX上以.so形式提供,需要配合Screen的GPU扩展接口使用。集成步骤大致如下:
- 加载GPU驱动库:dlopen("libadreno.so")
- 注册GPU回调:screen_context_set_gpu()
- 初始化渲染上下文:eglInitialize()
- 创建EGLSurface关联到Screen Window
我个人习惯在QNX的启动脚本里提前加载GPU驱动,避免运行时动态加载导致延迟。
13.3.2 Mali驱动集成
Mali的驱动在QNX上稍微麻烦一点。它需要额外的DDK(Device Driver Kit)支持。我建议直接用ARM官方的QNX BSP包,里面包含了完整的Mali驱动和Screen适配层。
| 特性 | Adreno | Mali |
|---|---|---|
| 驱动形态 | 用户态.so + 内核模块 | 用户态库 + 内核DDK |
| Screen集成 | 原生支持 | 需要适配层 |
| OpenGL ES版本 | 最高3.2 | 最高3.2 |
| Vulkan支持 | 支持 | 部分支持 |
| 功耗优化 | 较好 | 优秀 |
13.4 双屏异显实现
双屏异显是座舱的标配需求。一个屏显示仪表,一个屏显示中控。这两个屏的内容完全不同,而且刷新率也可能不同。
13.4.1 QNX双屏实现
在QNX下,双屏实现相对直接。每个Display对应一个物理屏幕,每个Screen可以独立配置分辨率、刷新率。
// 创建两个Display
screen_display_t disp1, disp2;
screen_create_display(&disp1, screen_ctx, 0);
screen_create_display(&disp2, screen_ctx, 1);
// 配置Display1为仪表屏
int disp1_mode[] = {1920, 720, 60};
screen_set_display_property_iv(disp1, SCREEN_PROPERTY_MODE, disp1_mode);
// 配置Display2为中控屏
int disp2_mode[] = {1920, 1080, 60};
screen_set_display_property_iv(disp2, SCREEN_PROPERTY_MODE, disp2_mode);
// 为每个Display创建Window
screen_window_t win1, win2;
screen_create_window(&win1, screen_ctx);
screen_create_window(&win2, screen_ctx);
// 关联Window到对应的Display
screen_set_window_property_pv(win1, SCREEN_PROPERTY_DISPLAY, &disp1);
screen_set_window_property_pv(win2, SCREEN_PROPERTY_DISPLAY, &disp2);
经验之谈:我曾经在双屏场景下遇到VSync不同步的问题。仪表屏60Hz,中控屏也是60Hz,但两个屏的VSync相位不同。结果就是中控屏的动画会影响到仪表屏的刷新。解决办法是让两个Display共享同一个VSync源,或者用硬件同步信号。
13.4.2 Android双屏实现
Android的双屏实现要复杂得多。它通过DisplayManager管理多个Display,每个Display有自己的SurfaceFlinger实例。
嗯,这里要注意:Android的双屏有两种模式——镜像模式和扩展模式。座舱里一般用扩展模式,两个屏显示不同内容。
// Android双屏配置
DisplayManager dm = getSystemService(DisplayManager.class);
DisplayPresentation presentation = new DisplayPresentation(context, display);
// 创建虚拟Display
VirtualDisplay vd = dm.createVirtualDisplay(
"SecondaryDisplay",
1920, 720, 160, // 宽、高、DPI
surface,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC);
// 或者使用物理Display
Display[] displays = dm.getDisplays();
Display secondaryDisplay = displays[1]; // 第二个物理屏
13.4.3 双屏的GPU合成策略
双屏场景下,GPU的合成策略很关键。我建议:
- 每个屏独立合成,不要跨屏合成
- 仪表屏用GPU合成,保证低延迟
- 中控屏用HWC合成,节省GPU资源
- 如果GPU支持,开启硬件图层叠加
警告:双屏场景下,GPU的显存带宽很容易成为瓶颈。我曾经在一个项目里,两个屏都是1080p@60fps,结果GPU带宽不够,导致画面撕裂。解决办法是降低一个屏的刷新率到50Hz,或者减少图层数量。
好了,关于显示与GPU驱动开发,核心就是这些。从QNX的Screen子系统,到Android的SurfaceFlinger和HWC,再到GPU驱动的集成,最后是双屏异显的实现。每一步都有坑,但只要你理解了底层原理,调试起来就不难。
总结要点:
- Screen是QNX显示的核心,管理Window、Buffer和合成
- SurfaceFlinger + HWC是Android显示的双引擎
- Adreno和Mali的驱动集成方式不同,但都支持Screen
- 双屏异显要注意VSync同步和GPU带宽