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 四者的关系——一张图说清楚

咱们用文字描述一下这个关系链:

  1. Session 是应用的“身份”,它向Screen服务注册自己。
  2. 在Session下创建 Window,Window定义了显示区域和属性。
  3. 创建 Stream,并把Stream绑定到Window上。Stream是数据传输通道。
  4. 从Stream中获取 Buffer,往Buffer里填充像素数据。
  5. 提交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_READSCREEN_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的分配、锁定、同步,以及如何避免常见的性能陷阱。到时候我会分享一个我亲手调优过的案例,保证干货满满。

嗯,今天就到这儿。有问题欢迎随时交流。