第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 实战经验总结
讲了这么多,我总结几条实战经验:
- 窗口类型选对:仪表盘用全屏窗口,普通应用用普通窗口。别混用。
- 缓冲区数量要合适:双缓冲是万金油,三缓冲留给高帧率场景。
- GC属性别频繁改:一次设置,多次使用。改一次GC属性,开销不小。
- 别忘了post:画完一定要调用
screen_post_window(),不然屏幕不会更新。 - 多线程加锁:GC不是线程安全的,多线程绘制时记得加互斥锁。
我曾经在一个项目中,为了追求极致性能,用了三缓冲+全屏窗口。结果内存吃紧,系统频繁OOM。后来改成双缓冲,帧率从60fps降到55fps,但系统稳定多了。嗯,有时候,平衡比极致更重要。
好了,这一章就到这里。下一章我们会深入Screen的绘制API,教你如何画圆、画弧、画文字。到时候,咱们再聊。