2、Screen图形子系统:Screen架构与核心概念

好,咱们今天来聊聊QNX图形显示栈里最核心的一块——Screen图形子系统。说实话,我刚开始接触QNX的时候,也被Screen这套概念绕得有点晕。Session、Window、Stream、Buffer……这些词单独看都认识,组合在一起就懵了。别急,咱们一个一个拆开讲。

2.1 Screen的整体架构

Screen是什么?说白了,它是QNX原生的一套图形合成管理器。你想想看,一个嵌入式设备上可能有多个应用同时要往屏幕上画东西——导航要画地图,仪表要显示转速,多媒体要放视频。如果没有一个统一的管理者,这些画面就会打架。

Screen就是那个管理者。它负责把所有应用的绘制请求收集起来,合成一帧完整的画面,然后交给底层硬件去显示。我在做仪表盘项目时,经常把Screen比作一个舞台导演——每个应用是演员,Screen决定谁站在前面、谁站在后面、谁该出现、谁该消失。

核心要点:Screen是一个客户端-服务器架构的图形系统。应用作为客户端,通过libscreen库与Screen服务器通信。服务器负责管理所有图形资源,并协调硬件完成最终的显示输出。

2.2 核心概念详解

2.2.1 Session(会话)

Session是Screen里最高级别的容器。你可以把它理解成一个独立的图形环境。每个Session都有自己的窗口树、缓冲区池和显示配置。我记得在做一个双屏仪表项目时,就用了两个Session——一个给仪表盘,一个给中控娱乐。这样两个系统互不干扰,即使一个崩溃了,另一个还能正常工作。

创建Session的代码其实很简单:

#include <screen/screen.h>

screen_context_t screen_ctx;
screen_create_context(&screen_ctx, SCREEN_APPLICATION_CONTEXT);

// 每个应用通常只需要一个context
// 但一个进程可以创建多个session

嗯,这里要注意:Session的创建权限是受控的。普通应用只能创建应用级别的Session,只有系统服务才能创建系统级别的Session。我曾经踩过这个坑——在用户态程序里试图创建系统Session,结果返回了权限错误,查了半天文档才明白。

2.2.2 Window(窗口)

Window是实际显示内容的载体。每个Window可以理解成一块画布,应用把内容画到Window上,Screen再把所有Window合成到显示器上。

Window有几个关键属性:

  • 大小和位置:决定窗口在屏幕上的显示区域
  • Z序:决定窗口的堆叠顺序,数值越大越靠前
  • 透明度:支持alpha混合,可以实现半透明效果
  • 缓冲区数量:通常用双缓冲或三缓冲来避免撕裂

我个人习惯把Window分为三类:

窗口类型 用途 典型场景
应用窗口 普通应用的显示区域 导航地图、媒体播放器
背景窗口 桌面或仪表背景 仪表盘静态背景层
覆盖窗口 临时提示或警告 故障指示灯、弹窗提示

2.2.3 Stream(流)

Stream是Screen里一个比较抽象的概念。它代表了一组连续的图形操作序列。说白了,Stream就是你把一帧一帧的画面送出去的通道。

为什么需要Stream?因为Screen的渲染是异步的。你提交了一帧画面,Screen不会立刻把它显示出来,而是等一个合适的时机(比如垂直同步信号)再去合成。Stream就是用来管理这个异步流程的。

我的经验:在仪表盘项目中,Stream的同步策略非常关键。如果设置不当,会出现画面延迟或者撕裂。我一般建议使用双缓冲+垂直同步的模式,这样既能保证流畅度,又能避免撕裂。

2.2.4 Buffer(缓冲区)

Buffer是真正存储像素数据的地方。每个Window背后都有一组Buffer,应用往Buffer里写数据,Screen从Buffer里读数据去合成。

Buffer的管理方式有两种:

  • 应用管理Buffer:应用自己分配内存,然后注册到Screen。这种方式灵活,但容易出错。
  • Screen管理Buffer:Screen帮你分配和管理Buffer。应用只需要告诉Screen「我要画图」,Screen会给你一个可用的Buffer指针。

我推荐用第二种方式。为什么?因为Screen知道底层硬件的内存对齐要求,它分配的Buffer通常能获得更好的性能。我曾经自己分配Buffer,结果因为内存对齐不对,导致GPU访问时性能下降了一半。

2.3 Screen API基础

Screen的API设计得还算清晰,主要分为几大类:

  1. 上下文管理:创建/销毁screen_context_t
  2. 窗口操作:创建/配置/销毁Window
  3. 缓冲区操作:申请/填充/提交Buffer
  4. 事件处理:监听触摸、键盘等输入事件
  5. 显示管理:查询/配置显示器属性

一个典型的绘制流程是这样的:

// 1. 创建窗口
screen_window_t win;
screen_create_window(&win, screen_ctx);

// 2. 设置窗口属性
int size[2] = {800, 480};
screen_set_window_property_iv(win, SCREEN_PROPERTY_SIZE, size);
screen_set_window_property_iv(win, SCREEN_PROPERTY_POSITION, pos);

// 3. 获取缓冲区
screen_buffer_t buf;
screen_get_window_property_pv(win, SCREEN_PROPERTY_RENDER_BUFFERS, (void**)&buf);

// 4. 锁定缓冲区,获取像素指针
void *ptr;
screen_get_buffer_property_pv(buf, SCREEN_PROPERTY_POINTER, &ptr);

// 5. 绘制内容到ptr指向的内存
draw_my_content(ptr, width, height);

// 6. 提交缓冲区,通知Screen显示
screen_post_window(win, buf, 0, NULL, 0);

注意:screen_post_window是异步调用。它只是把Buffer提交到Screen的渲染队列,不会阻塞等待显示完成。如果你需要知道什么时候显示完毕,可以用screen_flush_blits或者注册回调。

2.4 多显示器管理

多显示器在仪表盘领域越来越常见。比如一个12.3寸的仪表盘加上一个10.25寸的中控屏,或者更夸张的——一整块贯穿屏。

Screen对多显示器的支持是通过display对象来实现的。每个物理显示器对应一个display对象。你可以查询显示器的属性:

// 获取显示器数量
int display_count;
screen_get_context_property_iv(screen_ctx, SCREEN_PROPERTY_DISPLAY_COUNT, &display_count);

// 遍历每个显示器
for (int i = 0; i < display_count; i++) {
    screen_display_t display;
    screen_get_display_property_pv(screen_ctx, i, SCREEN_PROPERTY_DISPLAY, &display);
    
    // 获取显示器分辨率
    int res[2];
    screen_get_display_property_iv(display, SCREEN_PROPERTY_SIZE, res);
    printf("Display %d: %dx%d\n", i, res[0], res[1]);
}

在多显示器环境下,每个Window需要指定它要显示在哪个显示器上:

// 将窗口绑定到指定显示器
screen_display_t display;
screen_get_display_property_pv(screen_ctx, target_display_index, 
                                SCREEN_PROPERTY_DISPLAY, &display);
screen_set_window_property_pv(win, SCREEN_PROPERTY_DISPLAY, (void*)display);

我在做双屏项目时遇到过一个坑:两个显示器的刷新率不同。仪表盘是60Hz,中控屏是30Hz。如果用一个全局的垂直同步信号,会导致画面不同步。解决方案是每个显示器独立管理自己的vsync,Screen本身支持这个特性,但需要你在创建窗口时指定正确的display对象。

多显示器最佳实践:

  • 每个显示器使用独立的渲染线程
  • 为每个显示器创建独立的窗口组
  • 注意显示器的EDID信息,不同显示器的色彩空间可能不同
  • 如果显示器分辨率不同,需要分别适配UI布局

2.5 小结

Screen图形子系统是QNX图形显示栈的基石。Session管理图形环境,Window承载显示内容,Stream控制渲染流程,Buffer存储像素数据。这四个概念搞清楚了,Screen的API用起来就顺手多了。

下一章我们会深入Screen的渲染管线,看看一帧画面从应用绘制到屏幕显示,中间经历了哪些步骤。到时候我会分享一些性能优化的实战经验,敬请期待。