第3章:Screen图形框架入门

好,咱们今天聊聊Screen图形框架。说实话,很多刚接触QNX的工程师,一上来就被Screen的架构搞懵了。我当年也是这样,看了三天文档,脑子里还是一团浆糊。后来亲手调了几个项目,才慢慢摸清门道。

这一章,我会把Screen图形子系统的架构、窗口与缓冲区管理、还有图形上下文(Graphics Context)这三个核心概念,用我自己的理解讲给你听。保证不绕弯子。

3.1 Screen图形子系统架构

Screen是什么?说白了,它是QNX系统里负责图形显示的“大管家”。你写的应用程序,不管是仪表盘、中控屏还是HUD,最终都要通过Screen把画面送到屏幕上。

它的架构其实不复杂,我画个简图给你看:

应用程序 (App)
    ↓
Screen API (libscreen.so)
    ↓
Screen Compositor (合成器)
    ↓
显示驱动 (Display Driver)
    ↓
硬件 (GPU/Display Controller)

嗯,就这么几层。但每一层都有讲究。

应用程序层:你写的代码,调用Screen API创建窗口、绘制图形。

Screen API层:这是核心。它提供了一整套函数,比如screen_create_window()screen_post_window()。我建议你把这套API当成你的“画笔”和“画布”。

Screen Compositor层:这是QNX的合成器。它负责把多个应用的窗口合成到一起。比如仪表盘显示车速,中控显示导航,合成器把它们叠在一起输出到屏幕。

显示驱动层:直接跟硬件打交道。QNX的驱动模型很成熟,你一般不需要碰这层。

重要概念:Screen是一个客户端-服务端架构。你的应用是客户端,Screen Compositor是服务端。客户端创建窗口,服务端负责合成和显示。

我在项目中遇到过一个问题:两个应用同时写同一个屏幕区域,结果画面闪烁。后来发现是没理解合成器的缓冲策略。这个后面会细说。

3.2 窗口与缓冲区管理

窗口,就是你的应用在屏幕上占的一块矩形区域。缓冲区,是窗口背后存储像素数据的内存块。

这两个概念,你想想看,就像你写字的纸和墨水。窗口是纸,缓冲区是墨水。没有缓冲区,窗口就是空的。

3.2.1 窗口类型

Screen支持几种窗口类型,我列个表给你:

窗口类型 用途 特点
普通窗口 常规应用界面 支持合成、透明度
全屏窗口 仪表盘、视频播放 独占显示层,性能高
子窗口 控件、弹出菜单 依附于父窗口
虚拟窗口 离屏渲染 不直接显示,用于缓存

我个人习惯,仪表盘开发用全屏窗口。为什么?因为仪表盘需要高帧率、低延迟。全屏窗口绕过合成器的一些开销,直接跟显示驱动交互。

3.2.2 缓冲区管理

缓冲区管理是Screen里最容易踩坑的地方。我见过不少同事,程序跑着跑着就卡了,或者画面撕裂,十有八九是缓冲区没处理好。

Screen支持两种缓冲模式:

  • 单缓冲:只有一个缓冲区。你画完,直接送显。简单,但容易撕裂。
  • 双缓冲:两个缓冲区,一个前台显示,一个后台绘制。画完再交换。这是主流做法。
  • 三缓冲:三个缓冲区,进一步减少等待。适合高帧率场景。

我建议你默认用双缓冲。代码示例:

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

// 设置缓冲模式为双缓冲
int format = SCREEN_FORMAT_RGBA8888;
int usage = SCREEN_USAGE_OPENGL_ES2;
int nbuffers = 2;  // 双缓冲

screen_set_window_property_iv(win, SCREEN_PROPERTY_FORMAT, &format);
screen_set_window_property_iv(win, SCREEN_PROPERTY_USAGE, &usage);
screen_set_window_property_iv(win, SCREEN_PROPERTY_BUFFER_COUNT, &nbuffers);

// 获取缓冲区
screen_buffer_t buffers[2];
screen_get_window_property_pv(win, SCREEN_PROPERTY_BUFFERS, (void **)buffers);

小技巧:缓冲区数量不是越多越好。三缓冲虽然流畅,但会多占一份内存。嵌入式设备内存金贵,我一般只在仪表盘这种高要求场景用三缓冲。

我曾经踩过一个坑:双缓冲模式下,忘记调用screen_post_window(),结果画面一直不更新。排查了半天,才发现是post函数没调。嗯,这种低级错误,犯过一次就记住了。

3.3 图形上下文(Graphics Context)

图形上下文,英文叫Graphics Context,简称GC。这个概念,很多初学者觉得抽象。我换个说法:GC就是你的“绘画状态”。

你想想看,画画的时候,你需要知道:

  • 用什么颜色?
  • 线条多粗?
  • 填充还是描边?
  • 坐标原点在哪?

这些信息,在Screen里就存在GC里。每个窗口都有一个关联的GC,你通过GC来设置绘制参数。

3.3.1 GC的核心属性

我常用的GC属性有这么几个:

属性 含义 常用值
前景色 绘制图形的主色 0xFFFF0000 (红色)
背景色 填充背景的颜色 0xFF000000 (黑色)
线宽 线条的像素宽度 1, 2, 3
填充模式 是否填充图形内部 SOLID, EMPTY
裁剪区域 只绘制指定区域 矩形区域

设置GC属性的代码很简单:

// 获取窗口的GC
screen_gc_t gc;
screen_get_window_property_pv(win, SCREEN_PROPERTY_GC, (void **)&gc);

// 设置前景色为红色
int color = 0xFFFF0000;
screen_set_gc_property_iv(gc, SCREEN_PROPERTY_FG_COLOR, &color);

// 设置线宽为2像素
int line_width = 2;
screen_set_gc_property_iv(gc, SCREEN_PROPERTY_LINE_WIDTH, &line_width);

// 绘制一条线
int start_x = 100, start_y = 100;
int end_x = 300, end_y = 100;
screen_draw_line(gc, start_x, start_y, end_x, end_y);

注意:GC属性是状态性的。你设置一次,后续所有绘制操作都沿用这个状态,直到你再次修改。我见过有人每画一条线就设置一次颜色,性能白白浪费了。

3.3.2 GC的生命周期

GC的生命周期跟窗口绑定。窗口创建时,Screen自动分配一个GC。窗口销毁时,GC也跟着销毁。你不需要手动创建或释放GC。

但有一点要注意:GC不是线程安全的。如果你在多线程里操作同一个窗口的GC,一定要加锁。我有个项目,多线程同时绘制,结果画面花屏,查了半天才发现是GC竞争。

3.4 实战经验总结

讲了这么多,我总结几条实战经验:

  1. 窗口类型选对:仪表盘用全屏窗口,普通应用用普通窗口。别混用。
  2. 缓冲区数量要合适:双缓冲是万金油,三缓冲留给高帧率场景。
  3. GC属性别频繁改:一次设置,多次使用。改一次GC属性,开销不小。
  4. 别忘了post:画完一定要调用screen_post_window(),不然屏幕不会更新。
  5. 多线程加锁:GC不是线程安全的,多线程绘制时记得加互斥锁。

我曾经在一个项目中,为了追求极致性能,用了三缓冲+全屏窗口。结果内存吃紧,系统频繁OOM。后来改成双缓冲,帧率从60fps降到55fps,但系统稳定多了。嗯,有时候,平衡比极致更重要。

好了,这一章就到这里。下一章我们会深入Screen的绘制API,教你如何画圆、画弧、画文字。到时候,咱们再聊。