2. Screen图形框架入门:Screen API体系结构、连接与会话管理、窗口与缓冲区概念
好,咱们开始聊Screen图形框架。说实话,我第一次接触QNX的Screen时,第一反应是——这玩意儿跟Linux上的Wayland有点像,但又不完全一样。它有一套自己的哲学,你得顺着它的思路来。
我个人习惯把Screen理解成一个“图形资源的大管家”。你作为应用程序,想画东西?可以,但你得先跟Screen建立连接,申请窗口,再弄个缓冲区。每一步都有规矩,乱来是不行的。
2.1 Screen API体系结构
Screen API其实是一套C语言接口,它封装了底层显示硬件的操作。你想想看,如果没有Screen,你要直接操作帧缓冲,那得跟具体的硬件寄存器打交道,换个显示器就得重写驱动——太痛苦了。
Screen帮你做了这件事。它把显示设备抽象成几个核心概念:
- Context(上下文):你跟Screen对话的“通道”
- Window(窗口):你画图的地方
- Buffer(缓冲区):存放像素数据的内存区域
- Display(显示器):物理或虚拟的显示设备
这些概念之间的关系,我画个简单的图给你看:
应用程序
↓ (screen_create_context)
Screen Context
↓ (screen_create_window)
Window(s)
↓ (screen_create_buffer)
Buffer(s)
↓ (screen_post_window)
Display
嗯,这里要注意:每个Context可以管理多个Window,每个Window又可以关联多个Buffer。这种层次结构,说白了就是为了支持复杂的UI场景——比如一个窗口做视频播放,另一个窗口做控制面板。
核心要点:Screen API是事件驱动的。你创建了窗口,并不代表立刻就能看到东西。你得先往缓冲区里画内容,然后调用screen_post_window()把缓冲区提交给显示系统。这个“提交”动作,才是真正让画面显示出来的关键一步。
2.2 连接与会话管理
咱们来聊聊怎么跟Screen“握手”。
第一步,你得创建一个Context。这就像你走进一家公司,先得在前台登记一样。代码很简单:
screen_context_t ctx;
screen_create_context(&ctx, SCREEN_APPLICATION_CONTEXT);
这里有个参数SCREEN_APPLICATION_CONTEXT,它告诉Screen:我是一个普通应用,不是系统服务。如果你写的是系统级的合成器,那得用SCREEN_WINDOW_MANAGER_CONTEXT。我在项目中遇到过有人用错了这个参数,结果窗口死活创建不出来——嗯,权限不够。
创建完Context,你就可以查询显示设备了:
int num_displays;
screen_get_context_property_iv(ctx, SCREEN_PROPERTY_DISPLAY_COUNT, &num_displays);
screen_display_t *displays = malloc(num_displays * sizeof(screen_display_t));
screen_get_context_property_pv(ctx, SCREEN_PROPERTY_DISPLAYS, (void**)displays);
为什么要有这一步?因为嵌入式设备可能接了好几个屏幕——一个主屏,一个副屏,甚至还有个抬头显示。你得知道哪个是哪个。
避坑指南:我曾经在调试一个双屏设备时,发现画面总是显示在错误的屏幕上。后来才发现,screen_get_context_property_pv返回的display数组顺序,并不一定跟硬件接口编号一致。正确的做法是用screen_get_display_property_iv去查每个display的物理ID。
会话管理这块,说白了就是“谁可以访问什么”。Screen内部维护了一个访问控制列表。默认情况下,同一个进程内的多个线程可以共享同一个Context。但如果你想让两个不同的进程共享一个窗口?那就得用screen_create_window_type()指定窗口类型为SCREEN_CHILD_WINDOW,并传入父窗口的ID。
2.3 窗口与缓冲区概念
窗口,是你在屏幕上看到的那个矩形区域。缓冲区,是你在内存里画图的地方。这两个概念,是Screen图形框架的基石。
窗口的属性
每个窗口都有很多属性,我挑几个最重要的说:
| 属性 | 说明 | 我的经验 |
|---|---|---|
| SCREEN_PROPERTY_SIZE | 窗口的宽高 | 单位是像素,别跟屏幕坐标搞混 |
| SCREEN_PROPERTY_POSITION | 窗口在屏幕上的位置 | 原点在左上角,x向右,y向下 |
| SCREEN_PROPERTY_ZORDER | 窗口的叠放顺序 | 数值越大越靠上,我习惯用100的倍数 |
| SCREEN_PROPERTY_ALPHA | 窗口透明度 | 0.0完全透明,1.0完全不透明 |
创建窗口的代码长这样:
screen_window_t win;
screen_create_window(&win, ctx);
int size[2] = {800, 480};
screen_set_window_property_iv(win, SCREEN_PROPERTY_SIZE, size);
int pos[2] = {0, 0};
screen_set_window_property_iv(win, SCREEN_PROPERTY_POSITION, pos);
int zorder = 100;
screen_set_window_property_iv(win, SCREEN_PROPERTY_ZORDER, &zorder);
缓冲区的管理
缓冲区,说白了就是一块内存。但Screen对这块内存有特殊要求——它必须是连续的物理内存,因为显示控制器需要直接访问它。
我建议你使用双缓冲机制:
screen_buffer_t buffers[2];
screen_create_buffer(buffers[0], win);
screen_create_buffer(buffers[1], win);
// 在前台缓冲区画图
screen_buffer_t front;
screen_get_window_property_pv(win, SCREEN_PROPERTY_RENDER_BUFFER, (void**)&front);
// 在后台缓冲区画图
screen_buffer_t back;
screen_get_window_property_pv(win, SCREEN_PROPERTY_RENDER_BUFFER, (void**)&back);
// 交换缓冲区
screen_post_window(win, back, 0, NULL, 0);
为什么用双缓冲?你想想看,如果你只有一个缓冲区,画图的时候用户会看到“一半是旧画面,一半是新画面”的撕裂效果。双缓冲就是让你在后台画完,然后一次性切换到前台——用户看到的永远是完整的画面。
重要提醒:screen_post_window()是异步的。它只是把缓冲区提交给显示系统,并不保证立刻显示。如果你紧接着就去修改这个缓冲区的内容,可能会出问题。我曾经犯过这个错——画面闪烁得像迪斯科舞厅。正确的做法是等一个SCREEN_EVENT_POST事件,确认缓冲区已经显示完毕,再开始画下一帧。
缓冲区的格式
Screen支持多种像素格式,最常见的是:
- SCREEN_FORMAT_RGBA8888:32位,每个像素4字节,R/G/B/A各8位。质量最好,但占内存
- SCREEN_FORMAT_RGB565:16位,每个像素2字节。省内存,但颜色精度差一些
- SCREEN_FORMAT_NV12:YUV格式,视频解码常用
我个人习惯用RGBA8888,除非内存实在紧张。因为很多图形效果(比如透明、抗锯齿)在32位下表现更好。
设置缓冲区格式:
int format = SCREEN_FORMAT_RGBA8888;
screen_set_buffer_property_iv(buffer, SCREEN_PROPERTY_FORMAT, &format);
嗯,到这里你应该对Screen的框架有个整体认识了。说白了就是三步走:建立连接、创建窗口、管理缓冲区。下一节我们会深入讲怎么往缓冲区里画东西——那才是真正好玩的部分。