4、Screen图形框架核心:Screen API、窗口系统、合成器与渲染管线
好,我们进入第四章。这一章是Screen图形框架的硬核部分。说实话,很多QNX开发者用了好几年Screen API,但对它底层的窗口系统和合成器机制还是一知半解。我个人觉得,不理解这些,你写出来的图形应用性能大概率不会太好。
4.1 Screen API:你与图形硬件的对话通道
Screen API说白了就是QNX提供的一套C语言接口,让你能直接操作显示硬件。它不像Linux那边搞个Wayland或者X11那么绕,QNX的思路很直接——给你一个libscreen,你调就完了。
我刚开始接触Screen API时,最不习惯的是它的资源管理模型。它把所有东西都抽象成"资源"(resource),比如窗口是一个资源,像素缓冲区是一个资源,显示屏幕也是一个资源。你通过screen_create_window()、screen_create_pixmap()这类函数创建资源,然后用screen_set_window_property_iv()设置属性。
核心概念:Screen API中,一切皆资源。每个资源有一个类型和一组属性。你通过属性来控制资源的行为。
举个例子,创建一个全屏窗口的典型流程:
screen_window_t win;
screen_create_window(&win, screen_ctx);
// 设置窗口大小为屏幕尺寸
int size[2] = {1920, 1080};
screen_set_window_property_iv(win, SCREEN_PROPERTY_SIZE, size);
// 设置窗口位置在(0,0)
int pos[2] = {0, 0};
screen_set_window_property_iv(win, SCREEN_PROPERTY_POSITION, pos);
// 设置缓冲区数量(双缓冲)
int nbuffers = 2;
screen_set_window_property_iv(win, SCREEN_PROPERTY_BUFFER_COUNT, &nbuffers);
嗯,这里要注意:SCREEN_PROPERTY_BUFFER_COUNT这个属性很多人会忽略。默认值是1,意味着单缓冲。如果你不做双缓冲,画面撕裂几乎是必然的。我曾经在一个车载仪表盘项目里吃过这个亏,客户反映指针转动时有撕裂感,查了半天发现就是缓冲区数量没设对。
4.2 窗口系统:不是你想的那样
QNX的窗口系统和桌面Linux的窗口管理器完全是两码事。Screen的窗口系统是轻量级的,它没有复杂的装饰、没有标题栏、没有窗口拖动逻辑。它只做一件事:管理窗口的Z序和可见区域。
为什么会这样?因为QNX主要跑在嵌入式设备上,比如仪表盘、HUD、中控屏。这些场景不需要用户拖拽窗口,只需要应用把自己的内容画出来,系统负责合成显示就行。
窗口的层级关系通过Z序(Z-order)控制。每个窗口有一个Z序值,值越大越靠上。我记得有个项目需要做倒车影像覆盖,要求影像窗口必须显示在所有UI之上。我当时就设了一个极高的Z序值:
int zorder = 1000;
screen_set_window_property_iv(win, SCREEN_PROPERTY_ZORDER, &zorder);
但这里有个坑:Z序只在同一个窗口组(window group)内有效。不同组的窗口,Z序不能跨组比较。我曾经被这个坑过——设了Z序但影像还是被其他窗口挡住,后来发现两个窗口不在同一个组里。
避坑指南:窗口组(window group)是Screen中一个重要的隔离机制。不同组的窗口互不干扰,但也不能通过Z序控制跨组的遮挡关系。如果你需要强制某个窗口在最前面,确保它在同一个组内,或者使用SCREEN_PROPERTY_GLOBAL_ZORDER属性。
4.3 合成器:幕后的图像拼图师
合成器(compositor)是Screen图形框架里最容易被忽视、但最重要的组件。它的工作很简单:把所有窗口的内容合成一张完整的画面,然后送到显示器。
但简单的事情往往藏着复杂的细节。合成器需要处理:
- 透明度混合:窗口可能有alpha通道,需要做alpha blending
- 裁剪:被遮挡的部分不需要渲染
- 变换:旋转、缩放、镜像等操作
- 缓冲区交换:vsync同步,避免撕裂
QNX的合成器默认是硬件加速的。它利用GPU的2D引擎来做合成,而不是CPU软合成。你想想看,如果每个像素都用CPU去算alpha混合,那4K分辨率下60fps根本不可能。
我个人习惯在性能调优时,先检查合成器是否真的走了硬件。方法很简单:看screen_get_display_property_pv()返回的SCREEN_PROPERTY_RENDERER值。如果是"hw",说明硬件加速生效;如果是"sw",那你得检查驱动或者配置了。
4.4 渲染管线:数据怎么变成像素
渲染管线描述了从应用程序生成图像到最终显示在屏幕上的完整流程。Screen的渲染管线大致分四步:
| 阶段 | 做什么 | 谁负责 |
|---|---|---|
| 1. 应用渲染 | 应用把内容画到离屏缓冲区 | 应用(使用OpenGL ES或Screen API) |
| 2. 缓冲区提交 | 应用通知Screen缓冲区已就绪 | Screen API(screen_post_window()) |
| 3. 合成 | 合成器把所有窗口的缓冲区合成一帧 | 合成器(硬件加速) |
| 4. 显示 | 将合成后的帧送到显示控制器 | 显示驱动 + 硬件 |
这里有个关键点:第2步和第3步是异步的。应用调用screen_post_window()后不会阻塞,合成器会在下一个vsync到来时取走缓冲区。这种设计让应用和合成器可以并行工作,提高整体吞吐量。
但异步也带来了问题。我曾经遇到一个情况:应用post窗口的速度比合成器消费的速度快,导致缓冲区队列堆积,最终内存耗尽。解决办法是限制post的频率,或者使用SCREEN_PROPERTY_WAIT_FOR_VSYNC属性让post和vsync同步。
性能调优小技巧:如果你发现画面卡顿,先检查screen_get_window_property_iv()的SCREEN_PROPERTY_DROPPED_FRAMES属性。这个值统计了被丢弃的帧数。如果这个值持续增长,说明你的渲染管线某个环节跟不上显示器的刷新率。
4.5 实战:一个简单的双缓冲渲染循环
说了这么多理论,我们来写点实际的。下面是一个典型的双缓冲渲染循环骨架:
// 获取当前可写的缓冲区
screen_buffer_t buf;
screen_get_window_property_pv(win, SCREEN_PROPERTY_RENDER_BUFFERS, (void**)&buf);
// 锁定缓冲区,获取像素指针
uint8_t *ptr;
int stride;
screen_get_buffer_property_pv(buf, SCREEN_PROPERTY_POINTER, (void**)&ptr);
screen_get_buffer_property_iv(buf, SCREEN_PROPERTY_STRIDE, &stride);
// 在这里画你的内容(比如填充红色)
for (int y = 0; y < 1080; y++) {
for (int x = 0; x < 1920; x++) {
ptr[y * stride + x * 4 + 0] = 0; // B
ptr[y * stride + x * 4 + 1] = 0; // G
ptr[y * stride + x * 4 + 2] = 255; // R
ptr[y * stride + x * 4 + 3] = 255; // A
}
}
// 提交缓冲区,通知合成器
screen_post_window(win, buf, 0, NULL, 0);
嗯,这里要注意stride和width的区别。stride是每行像素占用的实际字节数,它可能比width * bytes_per_pixel大,因为硬件有对齐要求。如果你直接用width去计算偏移,画出来的图像会歪掉。我刚开始做嵌入式图形时就被这个坑过,画了一条直线结果变成斜线,排查了半天才发现是stride的问题。
好了,这一章的内容就到这里。Screen API、窗口系统、合成器、渲染管线,这四个东西构成了QNX图形框架的骨架。理解了它们,你就能写出高效、稳定的图形应用。下一章我们会深入Screen的缓冲区管理机制,那才是真正考验功底的地方。