Wayland合成器相关概念(Wlroots)

2025-08-20 22:08:29

# 合成

在 X11 中,窗口管理器和合成器是分开的。窗口管理器,顾名思义,负责窗口的管理,包括窗口布局(平铺或是堆叠),以及新建、最大化、最小化、全屏、浮动等等。合成器则是一个可选项,主要负责窗口特效,包括圆角(裁剪)、透明甚至自定义 shader。合成一般作为一个独立的阶段,在 X11 中,它使用 xcomposite 扩展将窗口重定向到屏幕外,并利用其 buffer 在屏幕上进行重绘,因此可以看作是后处理。

在 Wayland 上,由于渲染窗口的主体变成了客户端,而非 Xorg 服务器,因此 Wayland 服务器所做的工作变得更加简单:它只是通过 Wayland 协议和客户端对话,从应用获取它们的窗口 Surface,并将它按照一定位置和尺寸绘制到屏幕上。因此,窗口管理和合成的工作变得统一起来。Wayland Server、Wayland 合成器、Wayland 窗口管理器可以认为指向的是同样的东西。

# Wlroots

由于 Wayland 只是一个协议,因此需要合成器实现很多在 X11 下由 Xorg Server 实现的基本功能。但是这样一来,实现一个 Wayland 合成器成本就相当高。因此,很多 Wayland 合成器会选择借助一些现有的合成器库进行编写。

Wlroots 是一个相对成熟的 Wayland 合成器 Library,使用 C 编写,不支持 CPP(但能通过一些手段支持)。它原本是 Sway 的一部分,后来分割出来作为一个独立的库。一些有名的合成器,如 sway、river,以及早期的 hyprland 都使用了 wlroots。虽然 hyprland 后面重写了自己的渲染器,并剥离了 wlroots,但它仍然实现了很多 wlroots 的扩展协议。

wlroots 缺点很明显。由于它比较基础,因此即使依赖它,实现一个 wayland 合成器的代价依然很高。与此同时,它的渲染器过于简单,因此难以实现一些复杂的视觉效果,比如圆角、阴影。因此你可以看到大部分基于 wlroots 的合成器都没有圆角选项。作为一个现代的桌面没有圆角窗口是不应该的。另外一些 eye candy 的合成器,如 wayfire,在 wlroots 的渲染器基础上,使用自己的渲染器进行渲染。至于 sway 的 eye candy fork——swayfx 则选择利用 wlr_renderer_impl 从头(其实并非从头,因为它 copy 了很多 wlroots 的渲染器代码)实现自己的渲染器以及 scene API,这就是 scenefx。scenefx 在 wlroots 的 gles2 渲染器的基础上增加了部分自己的 shader,实现了圆角、阴影等效果;但似乎没有推出 vulkan 渲染器的计划。

虽说如此,它仍然是从头编写自己的合成器的最好选择之一。相比其他合成器库,wlroots 也属于最成熟的一个,它被许多成熟的合成器所使用,因此很少有严重 BUG(与之相比,由于 hyprland 主要由一个人维护,BUG 较多)。另外,相比自己从头开始写渲染器和实现 Wayland 协议,wlroots 做了很多必要的工作,能够让整个工作简单不少。相比一些更高级的合成器库,wlroots 虽然需要更多代码,但也提供了必要的灵活性,从而允许合成器作者自己实现想要的功能。

# 渲染

在渲染方面,从下到上,wlroots 大概做了如下工作:

另外,它还建立了一些抽象:

# Scene

Scene 在逻辑结构中是一个树,它其中有由客户端表面组成的各个结点。在渲染时,从根节点出发,按照一种类似先序遍历的方式渲染每个被启用的节点。

在 scene 中,可以使用 wlr_scene_tree_create 建立一个子树:

wlr_scene_tree* scene_tree = wlr_scene_tree_create(&this->scene->tree);

并且可以在同级子树/节点之间调整次序,从而决定渲染的顺序:

for (auto& layer : this->layers)
  wlr_scene_node_raise_to_top(&layer->tree.node);

# Toplevel

Toplevel(下面称为顶层窗口)的管理是窗口管理器的主要工作。合成器可以做以下几件事来管理窗口:

平铺窗口管理器一般会在顶层窗口创建/销毁时根据布局去主动调整窗口的位置和大小,从而将窗口平铺在屏幕上。另外,根据实现,它可能还会实现浮动、栈布局,以及全屏、最大化、最小化等等(后两者在平铺窗口管理器上并不算常见)。

堆叠管理器相对简单些,它一般无需主动调整窗口位置/大小,但大多需要处理窗口与鼠标的交互(对于平铺并非一个硬性要求),如拖动标题栏以调整位置,拖动窗口边缘/角落以调整大小。

# LayerShell

LayerShell 是根据 layershell 协议创建的一类表面,主要用于桌面组件。相比普通窗口,它会被创建在几个由协议确定的固定的层之上,从上到下依次是 Overlay、Top、Bottom、Background。实现了 layershell 的客户端程序会将它们的窗口放在这几个层上,从而方便合成器对其进行管理。

一个 LayerShell 有一个属性决定它是否独占。独占是指不允许有其它表面覆盖在它之上。因此在布局其他窗口之前,应当先确定 LayerShell 独占的显示区域,并将顶层窗口布局在这之外的可用区域中。

弹窗可以由 Toplevel、LayerShell 或是 Popup 创建,它通常是一个比较小的窗口,用来临时显示一些信息。常见的弹窗有:浏览器的右键菜单、Bar(如 waybar)的 tooltip 等。一级菜单可能还会创建一个二级菜单,它就是由另一个弹窗(一级菜单)创建的弹窗。

弹窗相比其他窗口的特殊性在于,它是用来提示信息的,因此不应该处在屏幕之外。因此合成器需要额外工作,以对其位置做出限制,这被称为 constrain,比如对下部或右部超出屏幕的弹窗进行移动或反转。

# 工作区/标签

工作区/标签虽然确实有相关协议,但很少有合成器实现。它通常只是一系列窗口的集合。对于工作区管理一般有两种方式:一种是将窗口移到屏幕之外,另一种是将其 unmap。

# 输入

在输入方面,Wayland 主要管理鼠标、键盘(或是数位板)设备,不管理手柄的输入。

wlroots 提供了一些抽象:

# 光标

在 wayland 中,认为只有一个光标,因此所有鼠标设备都是对这一个光标进行操作。

光标所支持的事件有:

光标的图像一般由客户端提供。客户端会在请求中附上光标的像素图,并由合成器渲染到屏幕上。由于客户端所提供的 buffer 分辨率不一,因此可能会出现在不同应用(GUI 框架)中光标大小不一的情况。为此,有 cursor shape 协议,允许客户端只提供一个具有光标名字的字符串,并由合成器根据这个字符串自行渲染光标(这也是 Wayland 经常被人用来抨击的一点,因为为了统一光标大小,最终又回到了 X11 的服务端渲染模式)。

为了在服务端渲染光标,需要知道需要绘制的光标主题和大小。对此,wlroots 提供了一个名为 wlr_xcursor_manager 的对象,它使用以下 API 进行创建:

wlr_xcursor_manager_create(cursor_theme, cursor_size);

对于光标,还有一些其他协议:

光标分为硬件光标和软件光标两种,前者由显卡进行渲染,因此对性能影响较小。后者使用软件渲染,在每次移动光标时都会造成底下表面的重绘,因此可能导致一些应用的卡顿。如果硬件(显卡)支持,尽可能使用硬件光标。对于 wlroots,这可以通过环境变量来控制:

export WLR_NO_HARDWARE_CURSORS=0

除了光标渲染和部分场景下使用的协议,另外光标加速度、focus_follow_mouse(焦点跟随鼠标)等常见功能都是由合成器自行实现。

# 键盘

键盘一般有以下事件:

由于 X11 中使用的 libxkbcommon 是一个很好的通用按键处理库,因此 wayland 合成器一般使用它进行按键的解析——从 libinput 的 keycode 根据 keymap layout 转换为实际的 keysym。下面是一个示例:

uint32_t keycode = event->keycode + 8; // xkeycode = libinput keycode + 8
auto sym = xkb_state_key_get_one_sym(this->keyboard->xkb_state, keycode);
auto modifiers = this->get_modifiers();

在收到按键事件后,合成器可以决定自行消耗,或是将它传递给客户端。自行消耗是指:执行快捷键,切换 tty,处理输入法捕获,忽略客户端消息,或是执行其它自定义逻辑。

键盘分为物理键盘(即真实的键盘设备)和虚拟键盘。其中虚拟键盘需要合成器实现 virtual-keyboard-protocol。它本来是用来实现屏幕键盘功能:当用户点击屏幕键盘应用表面时,它会模拟一个按键事件并发送给合成器。IME(如 fcitx5)在客户端不支持 text-input 协议的场合,也可能会使用该协议与客户端通信。

# 输入法

输入法就比较麻烦了,可能开一篇文章来单独讨论这件事会比较好。

在 X11 中,所有客户端都可以直接接收到用户的输入事件,即使它没有获取焦点。虽然这方便了应用作者,但也带来了一定安全隐患。恶意客户端可以在后台一刻不停地监听用户输入,从而获取他们的密码和其他隐私信息。

Wayalnd 为了解决这一问题,同一时间只允许一个应用(焦点所在的窗口)接收输入事件。其中光标(pointer)和键盘焦点是独立的。合成器决定输入事件如何被处理。

但这也为输入法的实现带来麻烦。在 X11 中,输入法只需要监听键盘输入,然后将结果发送到接收输入的程序中去。Wayland 中,输入法需要先获取焦点,从合成器获取到用户输入,处理并将结果返回给合成器。另一方面,合成器需要和要接收文本的应用通信,从而告知其输入的结果。因此,合成器在输入法实现中扮演着一个中间人的角色,它既要和输入法通信,又要和应用通信。在输入法侧,合成器要实现 input-method 协议;在应用侧,合成器要实现 text-input 协议。

但是事情并不只是这样。对于 input-method 协议,有 v1v2 两个版本;对于 text-input 协议,有 v1v2v3v4 四个版本,每个版本不兼容。而应用对协议的支持并不相同:gtk 支持 v3,qt 支持 v2v4,winit 支持 v3,Chrome 支持 v1。对此合成器支持情况也不尽相同:kwin(KDE 的合成器)实现了 v2v3,mutter 和 wlroots 只实现了 v3,weston 只实现了 v1

具体可见这篇文章:Chrome/Chromium 今日 Wayland 输入法支持现状

好在 fcitx5 做了一些工作以在不能使用 wayland 原生输入法模块的场合利用自己的模块。合成器编写者至少应该实现 input-method-v2text-input-v3,才能保证基本的输入法功能。但在用户这边,仍然需要为 fcitx5 配置正确的环境变量,并针对部分应用(如 electron)单独设置标志。如:

some-electron-app --enable-features=UseOzonePlatform --ozone-platform=wayland --enable-wayland-ime --wayland-text-input-version=3