3. Wayland协议基础
Wayland 并不是一个图形库,也不是一套渲染 API,而是一份显示服务器与客户端之间的通信协议。它规定了客户端如何把自己的窗口内容、输入事件、缓冲区提交等意图,通过一条本地套接字传递给合成器(Compositor),由合成器统一完成合成、显示与输入分发。理解 Wayland,本质上就是理解“谁拥有什么对象、谁向谁发什么请求、谁在什么时候收到什么事件”。
一、整体架构:客户端、合成器与协议对象
在 Weston 嵌入式桌面环境中,Wayland 架构通常由三层构成。最上层是客户端应用,例如 weston-terminal、weston-simple-egl 或基于 Qt/GTK 的应用;中间层是Wayland 协议,它定义了接口、请求、事件和对象生命周期;最下层是合成器,在 Weston 中即 weston 进程本身,它同时承担显示服务器、输入管理器、窗口管理器和渲染合成器的角色。
客户端与合成器之间通过 Unix 域套接字建立连接。连接建立后,双方并不是直接传递“窗口”或“按钮”这样的高级概念,而是围绕一组协议对象进行交互。每个对象都有唯一的数字 ID,客户端通过请求(request)操作对象,合成器通过事件(event)通知客户端状态变化。这种“对象 + 请求 + 事件”的模型,是 Wayland 协议最基础的抽象。
二、客户端—合成器通信模型
Wayland 的通信模型是双向异步的。客户端向合成器发送请求,合成器向客户端发送事件。两者都通过同一条连接传输,但方向相反。客户端不能直接调用合成器的函数,也不能直接读取合成器的内部状态;它只能通过协议规定的请求和事件来间接交互。
一个典型的交互过程如下:客户端首先连接 wl_display,然后通过 wl_registry 获取合成器暴露的全局对象,例如 wl_compositor、wl_shm、wl_seat、xdg_wm_base 等。客户端绑定这些全局对象后,创建自己的 wl_surface,并将 wl_buffer 附加到 surface 上。当客户端完成绘制并调用 wl_surface_commit 时,合成器收到提交请求,随后在合适的时机进行合成,并通过 frame callback 或 buffer release 事件通知客户端“可以继续画下一帧”。
输入方向则相反。当用户移动鼠标或按下键盘时,Weston 从 evdev 读取输入事件,经过 seat 和 pointer/keyboard 对象,将事件发送给当前拥有输入焦点的客户端。客户端收到 wl_pointer.motion 或 wl_keyboard.key 事件后,更新自己的界面状态,再提交新的表面内容。整个过程中,客户端不需要知道屏幕分辨率、不需要知道其他窗口的位置,也不需要直接访问输入设备。
三、核心接口与对象
Wayland 协议由一组接口组成,每个接口对应一类对象。下表列出了嵌入式 Weston 开发中最常接触的核心对象及其职责。
| 接口/对象 | 方向 | 核心职责 | 典型使用场景 |
|---|---|---|---|
wl_display |
客户端 ↔ 合成器 | 连接入口,管理协议对象 ID 与事件队列 | 连接 Weston、分发事件、同步请求 |
wl_registry |
合成器 → 客户端 | 枚举全局对象,通知客户端可用能力 | 发现 wl_compositor、wl_seat、xdg_wm_base |
wl_compositor |
客户端 → 合成器 | 创建 surface 和 region | 创建窗口表面、裁剪区域 |
wl_surface |
客户端 → 合成器 | 表示一块可被合成的像素区域 | 窗口内容、光标、子表面 |
wl_buffer |
客户端 → 合成器 | 承载像素数据的内存缓冲区 | 共享内存 wl_shm 或 EGL 缓冲区 |
wl_seat |
合成器 → 客户端 | 输入设备集合,管理 pointer/keyboard/touch | 鼠标、键盘、触摸输入 |
xdg_wm_base |
客户端 → 合成器 | 桌面窗口语义,管理窗口状态 | 最大化、最小化、标题、移动 |
四、案例:一个最小 Wayland 客户端的连接流程
下面这段伪代码展示了客户端从连接 Weston 到提交第一帧的典型顺序。它省略了错误处理和缓冲区分配细节,但保留了 Wayland 协议交互的关键步骤。
display = wl_display_connect(NULL);
registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, ®istry_listener, NULL);
wl_display_roundtrip(display); // 获取全局对象
surface = wl_compositor_create_surface(compositor);
xdg_surface = xdg_wm_base_get_xdg_surface(xdg_wm_base, surface);
xdg_toplevel = xdg_surface_get_toplevel(xdg_surface);
xdg_toplevel_set_title(xdg_toplevel, "weston-demo");
wl_surface_commit(surface); // 提交初始表面
buffer = create_shm_buffer(width, height);
wl_surface_attach(surface, buffer, 0, 0);
wl_surface_damage(surface, 0, 0, width, height);
wl_surface_commit(surface); // 提交第一帧
while (wl_display_dispatch(display) != -1) {
// 处理事件,等待 frame callback 或输入事件
}
在这个流程中,wl_display_roundtrip 非常关键。它让客户端发送请求后等待合成器处理完并返回事件,从而确保 registry 中的全局对象已经全部到达。如果没有 roundtrip,客户端可能在 compositor 或 seat 尚未绑定时就尝试创建 surface,导致协议错误。
WAYLAND_DEBUG=1 环境变量。它会把客户端与合成器之间的所有请求和事件打印到终端,是理解协议交互顺序最直接的工具。
五、常见误区与复盘
- 误区一:把 Wayland 当成 X11 的替代品。Wayland 不是 X Server 的重新实现,它没有全局坐标、没有窗口树、没有 X 属性。客户端不能查询其他窗口的位置,也不能直接抓取屏幕。
- 误区二:认为客户端可以直接画到屏幕。客户端只能画到自己的 wl_surface 和 wl_buffer 中,最终显示由合成器决定。合成器可以缩放、旋转、裁剪甚至丢弃某一帧。
- 误区三:忽略对象生命周期。Wayland 对象有明确的创建和销毁顺序。例如 xdg_toplevel 必须在 xdg_surface 之后创建,surface 销毁前必须先销毁其角色对象,否则会触发协议错误。
- 误区四:在错误线程中调用 wl_display_dispatch。Wayland 连接不是线程安全的。多线程客户端需要把事件分发集中在一个线程,或者使用 wl_display_prepare_read 等机制进行同步。
XDG_RUNTIME_DIR 是否设置正确、WAYLAND_DISPLAY 是否指向 Weston 的 socket、以及客户端是否成功绑定了 xdg_wm_base。很多“黑屏”问题并不是渲染错误,而是协议对象没有正确创建。
总结来看,Wayland 协议基础可以浓缩为一条主线:客户端通过 wl_display 连接合成器,通过 wl_registry 发现全局对象,通过 wl_surface 和 wl_buffer 提交内容,通过 wl_seat 接收输入,通过 xdg_wm_base 获得桌面窗口语义。掌握这些核心对象及其请求/事件关系,就掌握了 Weston 嵌入式桌面开发中所有上层框架(Qt、GTK、EFL、SDL)与合成器交互的底层逻辑。