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合成。

QNX Screen Graphics Subsystem 架构图 应用1 (导航) 应用2 (仪表) 应用3 (空调) Screen Graphics Service Window管理 | Buffer分配 | VSync同步 | 合成策略 GPU驱动 (Adreno/Mali) OpenGL ES | Vulkan | 合成加速 显示控制器驱动 DSI | HDMI | DP 接口 硬件层:GPU芯片 + 显示控制器 + 物理屏幕

13.2 Android SurfaceFlinger与HWC

Android的显示架构比QNX复杂得多。SurfaceFlinger负责合成所有应用的Surface,HWC(Hardware Composer)则负责把合成结果送到显示硬件。

13.2.1 SurfaceFlinger的工作流程

SurfaceFlinger本质上是一个合成引擎。它接收每个应用的Surface,然后决定怎么合成。我把它总结为三步:

  1. 收集:从每个应用获取Surface的Buffer
  2. 决策:判断哪些图层用GPU合成,哪些交给HWC
  3. 输出:把最终结果送到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带宽
公众号:蓝海资料掘金营,微信deep3321