2、Screen架构核心概念:Screen会话(Session)、窗口(Window)、流(Stream)与缓冲区(Buffer)的关系
好,咱们今天来聊聊Screen架构里最核心的几个概念。说实话,我刚接触QNX Screen时,也被Session、Window、Stream、Buffer这几个词绕得有点晕。它们到底怎么配合?谁管谁?谁又依赖谁?
我个人的理解是:这四者构成了一个从应用进程到显示硬件的完整管线。你想想看,一个应用要显示画面,总得有个“身份”去注册吧?这就是Session。有了身份,你得有个“画布”去画东西吧?这就是Window。画好的内容怎么送出去?靠Stream。数据存在哪儿?Buffer。
嗯,咱们一个一个拆开讲。
2.1 Screen会话(Session)——应用的“户口本”
Session是什么?说白了,它是应用与Screen服务之间建立的一个通信上下文。每个进程在调用Screen API之前,必须先创建一个Session。
核心要点:Session是Screen资源分配的根节点。没有Session,你连一个像素都画不出去。
我记得在做一个车载仪表盘项目时,有个同事直接把多个线程共用一个Session,结果窗口事件乱飞。后来我告诉他:每个线程最好有自己的Session,或者至少做好同步。为什么呢?因为Session内部维护了一个事件队列,多线程同时操作,队列就乱了。
创建Session的代码很简单:
#include <screen/screen.h>
screen_context_t ctx;
screen_create_context(&ctx, SCREEN_APPLICATION_CONTEXT);
// 现在你可以用ctx创建窗口、流等资源了
这里有个细节:SCREEN_APPLICATION_CONTEXT 表示这是一个应用级别的Session。还有一种 SCREEN_WINDOW_MANAGER_CONTEXT,那是给窗口管理器用的,普通应用别碰。
我的建议:每个进程只创建一个Session就够了。如果你非要创建多个,记得每个Session都是独立的,它们之间的窗口不能直接共享缓冲区。
2.2 窗口(Window)——应用的“画布”
Window是应用真正用来显示内容的地方。你可以把它想象成一块物理屏幕上的“子区域”。每个Window都属于某个Session,并且有自己的一组属性:位置、大小、透明度、Z序等等。
创建窗口的典型代码:
screen_window_t win;
screen_create_window(&win, ctx); // ctx是之前创建的Session
screen_set_window_property_iv(win, SCREEN_PROPERTY_SIZE, (int[]){800, 480});
screen_set_window_property_iv(win, SCREEN_PROPERTY_POSITION, (int[]){0, 0});
这里要注意:窗口本身不存储像素数据。它只是一个“框架”,真正存放图像数据的是Buffer。那Buffer怎么和Window关联?通过Stream。
我曾经踩过一个坑:在嵌入式设备上,窗口的 SCREEN_PROPERTY_FORMAT 如果设置成 SCREEN_FORMAT_RGBA8888,而底层硬件只支持 SCREEN_FORMAT_RGBX8888,那画面就会花掉。嗯,这个坑我帮大家踩过了。
2.3 流(Stream)——数据的“传送带”
Stream是Screen架构里最巧妙的设计。它连接了生产者(应用)和消费者(显示器或窗口管理器)。应用把渲染好的Buffer提交到Stream,Screen服务从Stream里取出Buffer去显示。
Stream有两种模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
SCREEN_PRODUCER |
应用作为生产者,往Stream里放Buffer | 普通应用渲染画面 |
SCREEN_CONSUMER |
窗口管理器作为消费者,从Stream里取Buffer | 窗口合成、截图、录屏 |
创建Stream的代码:
screen_stream_t stream;
screen_create_stream(&stream, ctx);
screen_attach_window_to_stream(win, stream); // 把窗口和流绑定
我个人习惯把Stream理解为“水管”。Buffer就是水管里的“水”。应用不断往水管里灌水(渲染新帧),Screen服务从水管另一头接水(显示到屏幕)。
注意:一个窗口可以绑定多个Stream吗?理论上可以,但实际项目中我几乎没见过这么用的。一个窗口一个Stream,简单清晰。
2.4 缓冲区(Buffer)——真正的“像素仓库”
Buffer是真正存放像素数据的内存区域。在Screen架构里,Buffer不是由应用直接malloc出来的,而是通过Stream分配的。
典型的Buffer操作流程:
// 从Stream请求一个Buffer
screen_buffer_t buf;
screen_get_stream_property_pv(stream, SCREEN_PROPERTY_BUFFERS, (void**)&buf);
// 锁定Buffer,获取像素指针
void *ptr;
screen_get_buffer_property_pv(buf, SCREEN_PROPERTY_POINTER, &ptr);
// 往ptr里画东西...(比如用OpenGL ES或CPU直接填充)
// 解锁并提交
screen_post_stream(stream, buf);
这里有个关键点:Buffer是双缓冲或多缓冲的。默认情况下,Screen会为每个Stream分配2个Buffer(双缓冲)。一个Buffer在显示,另一个Buffer在渲染。你调用 screen_post_stream 时,两个Buffer会交换角色。
我记得在做一个低延迟应用时,发现默认的双缓冲不够用。渲染一帧要20ms,但显示刷新率是60Hz(16.6ms一帧),导致画面卡顿。后来我把Buffer数量改成3个(三缓冲),问题就解决了。
// 设置三缓冲
int num_bufs = 3;
screen_set_stream_property_iv(stream, SCREEN_PROPERTY_BUFFER_COUNT, &num_bufs);
避坑指南:Buffer数量不是越多越好。我曾经在一个内存只有256MB的设备上设了5个Buffer,结果系统直接OOM了。一般来说,2~3个Buffer就够用。
2.5 四者的关系——一张图说清楚
咱们用文字描述一下这个关系链:
- Session 是应用的“身份”,它向Screen服务注册自己。
- 在Session下创建 Window,Window定义了显示区域和属性。
- 创建 Stream,并把Stream绑定到Window上。Stream是数据传输通道。
- 从Stream中获取 Buffer,往Buffer里填充像素数据。
- 提交Buffer到Stream,Screen服务把Buffer内容显示到Window对应的屏幕区域。
你想想看,这其实就是一个生产-消费模型。应用是生产者,Screen服务是消费者。Stream是队列,Buffer是产品。
一句话总结:Session是根,Window是壳,Stream是路,Buffer是货。四者缺一不可。
2.6 实际项目中的常见模式
在我参与的一个QNX智能座舱项目中,我们同时跑了三个应用:仪表盘、中控导航、空调控制。每个应用都有自己的Session、Window、Stream和Buffer。窗口管理器(Screen Compositor)负责把这些窗口合成到一起。
这里有个性能关键点:Buffer的分配策略。如果所有应用都用 SCREEN_PROPERTY_USAGE_OPENGL_ES2 来分配Buffer,那这些Buffer就会放在GPU可访问的内存区域,方便硬件加速合成。如果某个应用只用CPU渲染,那就用 SCREEN_PROPERTY_USAGE_READ 和 SCREEN_PROPERTY_USAGE_WRITE。
// 为GPU加速分配Buffer
int usage = SCREEN_PROPERTY_USAGE_OPENGL_ES2;
screen_set_stream_property_iv(stream, SCREEN_PROPERTY_USAGE, &usage);
曾经踩过的坑:有个应用用了CPU渲染,但Buffer却标记为GPU usage。结果每次渲染完,CPU要写数据到GPU内存,跨总线传输慢得要命。帧率直接掉到15fps。后来改成 SCREEN_PROPERTY_USAGE_READ | SCREEN_PROPERTY_USAGE_WRITE,帧率就回到60fps了。
2.7 小结
Session、Window、Stream、Buffer这四个概念,是理解Screen架构的基石。我个人觉得,只要搞懂了它们之间的关系,再看Screen的API文档就会豁然开朗。
下一章,咱们会深入Buffer的管理机制,包括Buffer的分配、锁定、同步,以及如何避免常见的性能陷阱。到时候我会分享一个我亲手调优过的案例,保证干货满满。
嗯,今天就到这儿。有问题欢迎随时交流。