banner

介绍

Wayland 是一个为了替代 Xorg 服务而设计和构建的,用于类 Unix 系统的下一代显示服务。并自称是将应用程序窗口显示在用户屏幕上的最佳方法。过去曾经使用过 X11 的读者会对 Wayland 的改进感到惊喜,而 Unix 上的图形新手将发现它是一个灵活而强大的系统,可用于构建图形应用程序和桌面。

这本书将会帮助您深入理解 Wayland 的概念、设计和实现,并为您提供构建自行构建 Wayland 客户端和服务端所需的工具。在阅读过程中,我们将构建 Wayland 的理想模型,并建立对其原理的认知。在这本书中,你能发现许多令你恍然大悟的时刻,Wayland 直观的设计让选择变得更加明确,有利于保持顺畅的阅读体验。欢迎来到开源图形的未来!

注意: 这还只是草案。第一到第十章基本已完成,可能后续会有所更新。第十一章及后续内容大部分有待撰写。

TODO

  • Expand on resource lifetimes and avoiding race conditions in chapter 2.4
  • Move linux-dmabuf details to the appendix, add note about wl_drm & Mesa
  • Rewrite the introduction text
  • Add example code for interactive move, to demonstrate the use of serials
  • Use — instead of - where appropriate
  • Prepare PDFs and EPUBs

关于这本书

本书采用 mdbook 构建,译者翻译水平有限,疑问请自寻原书解答,许可同源。

自译项目地址:repo

原书:https://wayland-book.com

许可:Creative Commons Attribution-ShareAlike 4.0 International License

源码:repo

关于作者

用 Drew 紧密合作者 Preston Carpenter 的话来说:

Drew DeVault 从 sway(一个对广受欢迎的平铺式窗口管理器 i3wm 的克隆) 开启了自己的 Wayland 之路。 目前它俨然成为 Wayland 下最受欢迎的平铺式窗口管理器,无论是用户、提交数量还是影响力。随着它的成功,Drew 回到 Wayland 社区并开始 wlroots 的工作:一个用于构建 Wayland 混成器的灵活可组合的模块。如今它已经成为数十个混成器的基础,并且在 Wayland 领域 Drew 成为最重要的专家之一。

Wayland 的上层设计

你的电脑有输入和输出设备,它们各自负责接收你的信息并将其显示给你。输入设备例如:

  • 键盘
  • 麦克风
  • 触摸板
  • 触摸屏
  • 数位板

而输出设备通常是桌面上的显示器、笔记本或其他移动设备的屏幕。这些显示资源在你的应用程序之间共用,而 Wayland 混成器在其中起到给客户端分派输入事件并在合适位置显示程序窗口的作用。将所有应用程序窗口组合在一起显示在屏幕上的过程被称为“混成”,因此我们将执行此操作的软件称作混成器。

现状

有许多不同的软件组成了桌面生态系统。诸如用于渲染的 Mesa 及其Linux KMS/DRM 子系统,负责缓存分配的 GBM (Generic Buffer Management 通用缓冲区管理),用户空间库 libdrm, libinput, evdev 等。但是不用担心,理解 Wayland 几乎不需要具备这些体系的专业知识,并且这些内容都大大超出了本书范围。事实上,Wayland 协议是相当保守和抽象的,很容易就能构建出一个基于 Wayland 的桌面并且大多数软件无需额外实现什么就能运行。话虽如此,但从表面上理解这些部分是什么,以及它们是如何工作,仍是十分有用的。让我们自底向上逐步展开。

硬件部分

一台典型的计算机配备了一些重要的硬件。在机箱外面,我们还有显示器、键盘、鼠标,或许还有麦克风和一个可爱的 USB 保温杯。机箱内部有一系列与这些设备接口相连的组件。例如可能你的键盘和鼠标正在使用由系统专有控制器负责的 USB 接口,你的显示器正连接着 GPU。

这些系统有他们自己的任务和状态。例如,GPU 有以显存形式提供的像素缓冲区,并将这些像素扫描输出到显示器上。GPU 还提供经过特调整的处理器,它们虽然在其他方面有所不如,但可以很好地处理高度并行的任务(例如为 1080P 显示器上的 2,073,600 个像素计算正确的颜色)。USB 控制器的工作同样复杂的令人称奇,它要实现枯燥的 USB 规范以接受来自键盘的输入事件,或精心调控杯垫的温度,从而避免诉讼和让你感到不快的冷咖啡。

在这个层面上,硬件几乎不了解系统上正在运行哪些应用程序。硬件提供了执行工作的命令接口,并告知相应的操作——而不在乎是谁发出的。因此,只允许一个组件与之对话......

内核部分

这一责任落到了内核身上。内核是一头复杂的“野兽”,因此我们只关注与 Wayland 相关的部分。Linux 内核的任务是提供一个抽象的硬件,因此可以在用户态安全的访问它们,我们的 Wayland 混成器也同样运行在用户态。对于图形(称为 DRM 或直接渲染管理器 direct rendering manager)来说,可以在用户态有效地为 GPU 分配任务。DRM 另一个重要的子系统是 KMS(kernel mode setting),其用于枚举显示设备并为其设置属性,例如其选定的分辨率(也称为“模式”)。输入设备通过名为 evdev 的接口进行抽象。

大多数内核接口都可以以特殊的设备文件形式存在于 /dev 供用户态调用。以 DRM 为例,这些文件位于 /dev/dri/ 中,通常以主要节点 primary node(例如 card0)的形式进行特权操作(如模式设置),且以渲染节点 render node(如 renderD128)的形式进行非特权操作(如渲染或视频解码),而对于设备节点 device nodes 则为 /dev/input/event*

$ ls /dev/dri/
by-path  card0  renderD128

用户态

现在我们来看用户态。在这里,应用程序于硬件隔离,必须通过内核提供的设备节点才能运行。

libdrm

大多数 Linux 内核接口都有一个对应的用户态,它为使用这些设备节点提供了令人满意的 C 语言 API。libdrm 库是其中之一,它是 DRM 子系统的用户态部分。Wayland 混成器使用它进行模式设置和其他 DRM 操作,但 Wayland 客户端通常不直接使用 libdrm。

Mesa

Mesa 是 Linux 图形栈中最为重要的部分之一。它除了为 Linux 提供 OpenGL(和 Vulkan)的厂家优化实现之外,还提供了 GBM(Generic Buffer Management)库,这是一种在 libdrm 之上的抽象层,用于在 GPU 上分配缓冲区。大多数 Wayland 混成器将通过 Mesa 同时使用 GBM 和 OpenGL,且多数客户端至少使用 OpenGL 或 Vulkan 其中一种实现。

libinput

如同 libdrm 是 DRM 子系统的抽象那样,libinput 提供了 evdev 用户态的抽象。它负责从内核接收输入设备的输入事件,将其解码为可用的形式,并传递给 Wayland 混成器。混成器需要特殊的权限才能使用 evdev 设备文件,从而迫使 Wayland 客户端通过混成器接收输入事件,这样可以防止键盘被记录等。

(e)udev

用户空间负责初步处理来自内核的新设备,在 /dev 中配置目标设备节点的权限,并将这些更改的信息发送给系统上正在运行的程序。大多数系统使用 udev(或 eudev)。Wayland 混成器使用 udev 枚举输入设备和 GPU,并在出现新设备或者拔出旧设备的时候接收通知。

xkbcommon

XKB(X Keyboard 的缩写)是 Xorg 服务的原始键盘处理子系统。几年前,开发者把它从 Xorg 代码树中提取出来,并做成了一个独立的键盘处理库且不再与 X 有任何实际的联系。Libinput(以及 Wayland 混成器)以扫描码的形式提供键盘事件,其准确含义因键盘而异。xkbcommon 负责将这些扫描到的码转化为有意义的通用键盘符号,如 65 转化为 XKB_KEY_Space。它还包含了一个状态机,该状态机知道在按住 shift 键的同时按下 “1” 会变成 “!”。

pixman

这是一个客户端和混成器都使用的简单库,其可以有效的处理像素缓冲区,使用相交的矩形进行数学运算,以及执行其他类似的像素操作任务。

libwayland

libwayland 是 Wayland 协议最常用的 C 语言实现,它处理许多底层的线协议。同时也提供了一个从 Wayland 协议定义(XML 文件)生成高级代码的工具。我们将在第 1.3 章以及整本书中详细讨论 libwayland。

其他

到目前为止,提到的每个部分在整个 Linux 桌面生态系统中都是一致的。而除此以外还有更多的组件。许多图形应用程序根本不了解 Wayland,而是选择诸如 GTK+、QT、SDL 和 GLFW 之类的库来进行处理。许多混成器选择像 wlroots 这样的软件来抽象它们所负责的部分,而其它的一类混成器则在内部实现所有功能。

目标和受众

我们的目标是让您从本书中了解到 Wayland 协议及其高级用法。您应该对 Wayland 核心协议中的所有内容有扎实的了解,并具有评估和实现生产中使用的各种扩展协议的必备知识。首先,这本书通过着重于 Wayland 的客户端来介绍其架构。此外,它也应该为 Wayland 混成器开发者提供一些实用的工具。

自由桌面生态系统十分复杂,并且由许多分离的部分所组成。我们将很少讨论这些部分——在这里你将不会找到关于在 Wayland 混成器中利用 libdrm 或者 libinput 处理 evdev 事件的信息。因此这本书不是构建 Wayland 混成器的综合指南。我们也不会讨论到图像绘制技术,如 Cairo、Pango、GTK+ 等,尽管它们对于 Wayland 客户端来说十分有用。因此,对于实际的 Wayland 客户端实现而言,这也不是一个可靠的指南。相反,我们只专注于 Wayland 的细节。

这本书只涵盖协议和 libwayland。如果你正在编写一个客户端,并且已经熟悉自己喜欢的 UI 渲染库,那么请带上自己的“像素”,我们将帮助您在 Wayland 上显示它们。如果你已经对操作混成器和输入设备所需的技术有所了解,那么本书将帮助您学习如何与客户端进行通信。

Wayland 软件包内容

当你在遵循 freedesktop.org 规范的 Linux 发行版中安装 "wayland" 的时候,很可能最后得到 libwayland-clientlibwayland-serverwayland-scannerwayland.xml 这些文件。它们或分别位于 /usr/lib/usr/include/usr/bin/usr/share/wayland 中。该软件包代表了 Wayland 协议最主流的实现,但这并不是唯一的。第三章详细介绍了 Wayland 的实现;这本书其余部分同样适用于任何实现。

wayland.xml

Wayland 协议通过 XML 文件进行定义。如果定位到并在编辑器中打开了 "wayland.xml" 文件,你将会发现 Wayland 核心协议的 XML 规范。这是一个高级协议,它建立在我们在下一章将要讨论的线协议之上。本书的大部分内容致力于解释该文件。

wayland-scanner

"wayland-scanner" 工具被用于处理这些 XML 文件并生成对应代码,其最常用的实现正如你现在所见的 wayland-scanner,它可以用于从诸如 wayland.xml 之类的文件生成 C 头文件和上下文代码。在其它的编程语言中有对应的 scanner,如 wayland-rs (Rust)、waymonad-scanner (Haskell) 等。

libwayland

libwayland-clientlibwayland-server 这两个库包含了一个双端通信线协议的实现,提供了一些常用的实用工具来处理 Wayland 的数据结构、简单的事件循环等。此外,这些库还包含 wayland-scanner 生成的 Wayland 核心协议的预编译副本。

协议设计

Wayland 协议是由多层抽象结构所构建的。它从一个基本的线协议格式开始(该格式是可用事先约定好的接口解码的消息流),然后用更高级别的程序来枚举接口,创建符合这些接口的资源,以及交换相关信息,这便是 Wayland 协议及其扩展的内容。且最重要的是,我们拥有了一些更宽泛的模式,这些模式在 Wayland 协议设计中经常用到。我们将在本章节中介绍所有相关内容。

让我们继续自下而上地学习。

基础线协议

注意: 如果你正打算使用 libwayland,那么本章节选读,可自由转跳到第 2.2 章节。

线协议是由 32 位值所组成的流,使用主机的字节顺序进行编码(例如 x86 系列 CPU 上的小端序)。如下表示原始类型的值:

  • int, uint: 32 位有符号或无符号整型
  • fixed: 24 位整数 + 8 位小数的定点数
  • object: 32 位对象 ID
  • new_id: 32 位对象 ID,收到对象时分配

除了上面这些基本类型之外,下面还有一些常用的类型:

  • string: 字符串,以 32 位整数为前缀,指定其长度(以字节为单位),而后是字符串内容和 NUL 终止符,并用未定义的数据填充满 32 位。没有指定编码,但实际上是使用 UTF-8。

  • array: 任意的数据块,以 32 位整形作为前缀按位指定其长度,随后是数组逐字内容和 NUL 结束符,用未定义的数据填充不满 32 位长度的部分。

  • fd: 主传输上的 0-bit 值,在 Unix 域套接字信息(msg_control)中使用辅助数据,将文件描述符从一段传输到另一端。

  • enum: 一个单独的值(或者 bitmap)用于枚举已知常量,编码为 32 位整形。

消息

线协议是使用这些原语构建而成的消息流。每个消息都是对 object 起作用的一个事(服务端到客户端)件或者请求(客户端到服务端)。

消息头由两个部分组成。第一个是起作用的对象 ID。第二个是两个 16 位值;高 16 位读音消息的大小(包括头本身),低 16 位事件或请求的操作码。其参数基于双方事先约定的消息签名。接收方查找对象 ID 以及由其操作码定义的事件或请求,以确定消息的签名和性质。

msg header
object IDmsg size (upper 16bits)code (lower 16bits)

为了解析消息,客户端和服务端必须先创建对象。作为 Wayland 的显示单例已预先分配对象 ID,可用于引导其他对象。我们将在第四章中对此进行讨论,而下一章将讨论什么是接口,以及假设您已经协商了对象 ID 的情况下请求和事件是如何工作,下见后文。

对象 ID

当消息带着 new_id 参数来的时候,发送方将为其分配一个对象 ID(用于该对象的接口是通过其他参数建立的,或者是针对该请求或事件预先约定的)。此对象 ID 可以用在后续消息中使用,作为信息头的第一个字或者 object_id 参数。客户端从 10xFEFFFFFF 范围内分配 ID,服务端在 0xFF0000000xFFFFFFFF 内分配 ID。ID 从下限开始,并随着每次新对象的分配而递增。

对象 ID 为 0 表示一个空对象;也就是说不存在对象或缺少显式声明。

传输

迄今为止,所有已知的 Wayland 实现都工作在 Unix 域套接字上。尤其是出于一个原因而使用它:文件描述符消息。Unix 套接字是能够在进程之间传输文件描述符的最为实用的传输方式,并且对于大量数据传输而言是十分必要的(主要如键映射、像素缓冲和剪贴板内容)。从理论上讲,可以使用其他传输方式(如 TCP),但需要有人来找出另一种传输块数据的方式。

要找到连接的 Unix 套接字,大多数实现只需要执行 libwayland 的操作:

  1. 如果已经设置了 WAYLAND_SOCKET,则说明已经在其上建立了文件描述符编号,并假设父进程为我们配置了连接。
  2. 如果已经设置了 WAYLAND_DISPLAY,则与 XDG_RUNTIME_DIR 路径连接以建立 Unix 套接字。
  3. 假定套接字名称为 wayland-0,并与 XDG_RUNTIME_DIR 路径连接以建立 Unix 套接字。
  4. 建议放弃。

接口、请求和事件

Wayland 协议通过发出作用于对象的请求和事件来工作。每个对象都有一个接口,定义了可能的请求和事件以及对应的签名。让我们来考虑一个简单的示例接口:wl_surface

wl_surface

请求

表层是可以在屏幕上显示的像素区域。这是我们构建诸如窗口应用程序的原始方法之一。它的请求之一是“销毁”(damage),客户端使用它来表示表层的某些部分已经更改并需要重新绘制。下面是一个销毁信息的注释示例:

0000000A    Object ID (10)
00180002    Message length (24) and request opcode (2)
00000000    X coordinate (int): 0
00000000    Y coordinate (int): 0
00000100    Width        (int): 256
00000100    Height       (int): 256

这是会话的一小部分——表层是较早分配的,并且分配的 ID 为 10。当服务端接收到这一消息时,它会查找 ID 为 10 的对象,并发现它是一个 wl_surface 的实例。知道这一点后,它将使用操作码 2 查找请求的签名。然后知道它需要以四个整数作为参数,且可以解码该消息并将其分派以进行内部处理。

事件

请求是对于客户端到服务端而言的,反之服务端可以给客户端发回送事件消息。服务端可以发送 wl_surface 的“输入”事件,当该表层在特定输出上显示时,服务端将发送该事件(客户端可以通过如调整 HiDPI 显示的比例系数来对此作出响应)

0000000A    Object ID (10)
000C0000    Message length (12) and event opcode (0)
00000005    Output (object ID): 5

该消息通过其 ID 引用了另一个对象:wl_output,该对象正用于表层显示。客户端收到此消息后与服务端行为类似:查找 ID 为 10 的对象,将其与 wl_surface 接口关联,并查找与操作码 0 对应事件的签名。它相应地解码其余信息(还查找 ID 为 5wl_output),然后将其分派给内部处理。

接口

接口用于事先定义请求和事件的列表,与每个请求相关联的操作码和解码消息所需的签名也都来自于其。我敢肯定你渴望知道如何实现接口——请看后文。

上层协议

在第 1.3 章节我们提到:wayland.xml 可能与 Wayland 软件包一同被安装到你的操作系统上。立即找到并用你偏好的编辑器打开该文件。通过这样的文件,我们定义了 Wayland 客户端或服务端所支持的接口。

$ pacman -F wayland.xml
    usr/share/wayland/wayland.xml

文件中定义了每个接口,以及对应的请求、事件和各自的签名。为此,我们使用了 XML 格式。让我们看看上一章中讨论过的 wl_surface 示例:

<interface name="wl_surface" version="4">
  <request name="damage">
    <arg name="x" type="int" />
    <arg name="y" type="int" />
    <arg name="width" type="int" />
    <arg name="height" type="int" />
  </request>

  <event name="enter">
    <arg name="output" type="object" interface="wl_output" />
  </event>
</interface>

注意: 为了简单起见,我已经对该代码进行了删减,但是如果你之前有找到 wayland.xml 文件(实际文件和上述示例略有不同),建议查找此接口并亲自检查一下——其中有额外的文档,这些文档解释了每个请求和事件的目的和精确的语义。

在处理此 XML 文件时,我们为每个请求和事件分配一个操作码(按照它们出现的顺序,零号开始,依次递增)结合参数列表,你可以解码来自线协议中的请求和事件,并且基于 XML 文件中的文档,你可以决定如何对软件进行编程以对应相应的行为。这一部分代码通常来自于代码生成器,我们将在第 3 章中讨论 libwayland 如何实现这一点。

而从第四章开始,本书其余大部分内容专门用于解释该文件以及一些补充的扩展协议。

协议设计准则

在大多数 Wayland 的协议设计中已经应用了一些关键性的概念,我们将在此简要介绍它们。在整个上层 Wayland 协议及其扩展协议中都可以找到这些准则(至少在 Wayland 协议中)。如果您正在编写自己当协议扩展,那么聪明的做法是借鉴这些准则。

原子性

Wayland 协议设计中最重要的准则就是原子性。Wayland 的既定目标是“每一帧都是完美的”。为此,大多数接口都允许以事务的方式进行更新,使用多个请求来创建一个新的表示状态,然后一次性提交所有请求。例如,可以在 wl_surface 上配置几个属性:

  • 附加的像素缓冲区
  • 需要重新绘制的变更区域
  • 出于优化而不透明的区域
  • 可接受输入事件的区域
  • 变形,例如旋转 90 度
  • 缓冲规模,用于 HiDPI

该接口为这些请求提供了独立的配置,但它们都处于挂起状态(pending)。仅当提交请求的时候,挂起状态才会合并到当前状态(current)。从而使您可以在单帧内原子地更新所有这些属性。结合其他一些关键性设计决策,Wayland 混成器可以在每一帧中完美地呈现所有内容——没有画面撕裂和窗口部分更新,每个像素都恰如其分地显示。

资源生命周期

另一个重要的设计准则是:避免服务端和客户端向无效对象发送相关事件或请求。于此,通过接口定义的事件和请求具有有限的生命周期,服务端和客户端也可以通过接口对其进行显式释放。只有当两边都先后同意才能释放对象所分配的资源。

Wayland 是一个完全异步的协议。它将保证消息按发送顺序送达,但仅仅针对一个发送者的情况。例如,当客户端决定销毁其键盘设备时,服务端可能会有多个输入事件正在排队。客户端必须正确处理不再需要的对象事件,直到服务端同步。同理,如果客户端在销毁对象前将一些不再需要的请求入队,则服务端必须以正确的顺序发送这些请求,以便在客户端同意销毁该对象后不再对其进行使用。

版本控制

关于 libwayland 的实现

我们在 1.3 节中介绍了 libwayland 这一最受欢迎的 Wayland 实现。本书的大部分内容适用于任何实现,但是接下来的两章将使你熟悉 libwayland。

Wayland 软件包包括用于 Wayland-Client 和 Wayland-Server 的 pkg-config 规范——请参阅构建系统的文档以获取有关它们的链接介绍。当然,大多数应用程序只会连接到其中一个。该库包含一些简单的原语(如链表)和 Wayland 的核心协议 ——Wayland.xml 的预编译版本。

我们将从原语开始介绍。

Wayland-util 原语

wayland-util.h 是客户端和服务端共同使用的库,它定义了许多结构、实用函数和宏,建立了一些用于 Wayland 应用程序的原语。其中包括:

  • 生成用于 Wayland 协议消息结构化重组和解构的代码
  • 链表 wl_list 的实现
  • 数组 wl_array 的实现(装配到相应的 Wayland 原语)
  • 用于 Wayland 标量(如定点小数)和 C 类型转化的实用程序
  • 调试记录工具,收集来自 libwayland 内部传出的消息

头文件包含了许多非常好的注解,您应该自己阅读该文件。在接下来的几页中我们将详细介绍如何运用这些原语。

wayland-scanner

Wayland 包中含有一个二进制文件:wayland-scanner。该工具基于 2.3 节中所提及的定义 Wayland 协议的 XML 文件来生成 C 头文件和对应的胶水代码。生成的头文件为 wayland-client-protocol.hwayland-server-protocol.h,此外通常还包括对协议进行封装的头文件 wayland-client.hwayland-server.h ,而不是直接使用它们。

该工具的用法非常简单(并可以在 wayland-scanner -h 看到用法总结),但仍可概述如下:

  • 生成客户端头文件
$ wayland-scanner client-header < protocol.xml > protocol_client.h
  • 生成服务端头文件
$ wayland-scanner server-header < protocol.xml > protocol_server.h
  • 生成胶水代码
$ wayland-scanner private-code < protocol.xml > protocol.c

不同的构建系统将使用不同的方法来自定义命令——请查阅构建系统的文档。一般来说,您将需要在构建时运行 wayland-scanner,然后编译并将你的应用程序链接到胶水代码。

如果你方便的话可以立即尝试任意的 Wayland 协议(示例 wayland.xml 可能在 /usr/share/wayland)。打开胶水代码和头文件,并在阅读下面的章节时进行参考,以了解 libwayland 提供的原语在生成的代码中如何实际应用。

代理与资源

对象是客户端和服务端都知道的具有某种状态的实体,通过线协议协商来对其进行更改。在客户端,libwayland 通过 wl_proxy 接口引用这些对象。这些接口是对 C 语言友好的,是抽象对象的具体“代理”,并为客户端提供了间接函数,以将请求编组转化为线协议格式。如果你查看 wayland-client.core.h 文件,会发现一些实现该目的的底层函数。而通常你不会直接使用它们。

// /usr/include/wayland-client-core.h
// ...
void wl_proxy_marshal(struct wl_proxy *p, uint32_t opcode, ...);

struct wl_proxy * wl_proxy_create(struct wl_proxy *factory, const struct wl_interface *interface);
// ...

在服务端,对象是通过 wl_resource 抽象的,这与客户端非常相似,但另有更复杂的内容——服务端必须跟踪哪个对象属于哪个客户端。每个 wl_resource 均由单个客户端所有。除此之外,该这两个接口基本相同,并且为编组事件发送到关联的客户端提供了底层的抽象。与直接在客户端上使用 wl_proxy 相比,直接在服务端上使用 wl_resource 的频率更高。这种用法的一个例子是获取对 wl_client 的引用,该 wl_client 拥有你所在上下文之外的操作资源,或者在客户端尝试无效操作的时候发送协议错误。

此外还有另一组高级接口,大多数 Wayland 客户端和服务端的代码都与之交互以完成其大部分任务。