weston input 概述
零、前言
本文描述了有关于 weston
(基于 wayland
协议一种显示服务器的实现) 中有关于输入设备管理的部分;为了聚焦于此,本文不会对 weston
整体或 wayland
协议进行过多的阐述.
考虑到读者可能存在不同的需求,采用分层次的描述方式,主要面向以下两类人群:
- 想了解通用显示服务器有关于输入设备管理
- 想了解
weston
有关于输入设备管理
对于前者,将会站在一个较为抽象的层次,以感性的方式讲述一个通用显示服务器的设计; 对于后者,则更多地会参考 weston
的实现细节,结合一些代码示例,帮助读者更好地理解 weston
, 为将来阅读 weston
源码做好一定的准备.
本文基于
weston
10.0.2 分支.
一、显示服务器与输入设备
不知道有没有人会与我一样疑惑,为什么 显示服务器 会与 输入设备 扯上关系呢?
有关于这点的答案实际上很多显示服务器官方都给出了自己的答案,目前主流的答案是: 现代 GUI
程序跟以往的(后台)程序不同,面向的是用户,强调的是交互;因此对于交互的反馈速度(50ms以内)十分看中,为此 GUI
程序一般采取的都是异步编程的方式进行,更为具体的描述是采用事件驱动的异步编程模式.
因此, GUI
程序的 UI 发生改变一定是受到了某些信号源驱动,信号驱动源从广义上可以分为硬件和软件两类:
- 硬件 : 鼠标、 键盘、 触摸 、蓝牙等
- 软件 : 定时器 、HTTP/IOT/WS 接口等
其中对于软件类的更多GUI
设计者会更加熟悉,例如我们会使用一个定时器去实现一些动态效果(比如说 gif
的播放),利用 IOT
去操作设备以及可能暴露一些 HTTP
接口去进行接口测试.
显示服务器管理的是硬件相关的信号源,而非软件信号源, 理解这点是至关重要的.对于软件信号源,其针对的是 GUI
程序这个进程本身;对于硬件信号源,其针对的是整个系统;两者的影响范围或者说作用域是不同的.
而对于一个正常的 GUI
程序,肯定是不希望其在最小化或者被其他 GUI
程序遮挡时仍然接收到诸如鼠标、触摸等事件,毫无疑问如果是这样的话,那将是一件相当奇怪的事情.
所以显示服务器才会代替 GUI
程序去接管系统设备的输入,并针对不同的 GUI
程序进行合适的滤波以及转换 (比如在 GUI
程序隐藏时就输入事件分发给它们),使得在 GUI
程序侧硬件输入的处理逻辑与软件输入没有差异.
二、输入设备种类
根据 weston
的实现,将 weston
的输入设备分为了三大类,具体如下图所示:
其中 seat
代表的是输入设备的抽象形式,而 pointer
、touch
、keyboard
则代表的是不同种类的输入设备; 这么划分实际上很大程度上是由输入设备硬件本身的特性,几种不同输入设备的特性如下:
- pointer : 常见的设备类型为鼠标,其特点是输入一般以相对坐标的方式给出,即给出是一个矢量
- keyboard : 常见的设备类型为键盘,其特点是输入一般以键值给出,例如
A
、B
、C
、D
等 - touch : 常见的设备类型为触摸屏,其特点是输入一般以绝对坐标的方式给出
上面各个输入设备的特性从日常使用角度理解起来应该是不难的; 值得一提的是
seat
这个作为其抽象的命名,其具体命名的意图我也不是特别理解,但seat
并非是weston
创造的名词,其命名依据来自于一个专门负责输入设备管理的库libinput
.
三、显示服务器输入管理设备总体设计
在描述 weston
整体输入管理之前,有必要先停下来,捋一捋weston
输入设备管理需要实现的目标:
- 获取(
linux
)系统输入事件捕获,以及监听输入设备 - 为
GUI
程序提供统一的输入事件逻辑 - 按照
GUI
程序的需求派发不同的输入事件
其中一点是为了获取完整的来自系统的输入事件;
第二点则更多的是标准化的过程,例如虽然 keyboard
类型的输入设备虽然大多数都是键盘,但是像是红外遥控器、蓝牙遥控器等也算是 keyboard
中的一种,在 GUI
程序看起来它们像是行为一致的,但是站在 linux
驱动层与应用层之间则不尽其然, weston
需要将各种输入设备进行分类(pointer
、kyeboard
、touch
),然后屏蔽各个输入设备之间具体的细节差异;
第三点而是需要根据应用的需求派发事件,例如有些应用只关注于触摸事件,而对键盘事件不敏感,那么 weston
就不应该将事件分发给它们,当程序不处于焦点状态(选中状态)时,也不应该将事件派发给它们.
基于以上描述, weston
给出的整体设计为:
其中 weston
处于第二级,向左对接 linux
系统, 向右则对接 wayland
显示服务器协议;然后 weston
(display server) 和 GUI
程序 (display client) 通过 wayland
(显示服务器)协议连接起来.
在第二级中,对于捕获的输入事件 weston
首先将其进行标准化,然后对其进行简单的滤波,最后按照需求进行分发;同时监听系统本身的输入设备,管理输入设备的声明周期.
到此为止,一个较为通用的显示服务器关于输入设备的管理就已经讲解完了,剩下的都是一些比较细节的实现,比如说滤波究竟做了些什么,标准化又做了什么,其实对于大多数应用层开发者都已经是非常的遥远的事情了,所以就此收手也是不错的选择.
如果想要对
weston
有一个整体性的理解,推荐阅读 Wayland与Weston简介,而对于wayland
协议的架构设计,则可以参考 wayland.
四、显示服务器细节设计
(1) 对接系统输入
在讲解之前,需要审视一下 weston
作为一个显示服务器的职责: 毫无疑问那将是渲染,而非输入.
基于此, weston
在对接系统输入这一块并没有做很多的事情,而是利用现在成熟的开源方案, libinput
以及 udev
.
- libinput : 获取输入事件以及设备状态
- udev : 管理输入设备
weston
对于 libinput
的封装主要存在于 libweston/libinput-device.c
, 而 udev
则是位于 libweston/libinput-seat.c
.
对于输入设备的监听,主要是基于 Reactor
模式进行; 通俗地讲就是在每个循环的周期内查询有无设备的移除以及添加相关的事件,具体的细节实现如下:
// file : libweston/libinput-seat.c
// 一次循环周期内调用
static int
udev_input_process_event(struct libinput_event *event)
{
struct libinput *libinput = libinput_event_get_context(event);
struct libinput_device *libinput_device =
libinput_event_get_device(event);
struct udev_input *input = libinput_get_user_data(libinput);
int ret = 0;
switch (libinput_event_get_type(event)) { // 获取输入事件类型类型 (libinput)
case LIBINPUT_EVENT_DEVICE_ADDED: // 设备添加
ret = device_added(input, libinput_device);
break;
case LIBINPUT_EVENT_DEVICE_REMOVED: // 设备移除
ret = device_removed(input, libinput_device);
break;
default:
evdev_device_process_event(event);
break;
}
return ret;
}
// 设备添加
static int
device_added(struct udev_input *input, struct libinput_device *libinput_device)
{
// ...
udev_seat = get_udev_seat(input, libinput_device); // udev, 获取 seat
if (!udev_seat) {
weston_log("Failed to get a seat\n");
return 1;
}
seat = &udev_seat->base;
device = evdev_device_create(libinput_device, seat); // udev, 创建设备节点
if (device == NULL) {
weston_log("Failed to create a device\n");
return 1;
}
// ...
return 0;
}
对于输入事件的监听,同样是位于上面代码片段 udev_input_process_event
中的 evdev_device_process_event
,具体实现如下:
// file : libweston/libinput-device.c
int
evdev_device_process_event(struct libinput_event *event)
{
struct libinput_device *libinput_device =
libinput_event_get_device(event);
struct evdev_device *device =
libinput_device_get_user_data(libinput_device);
int handled = 1;
bool need_frame = false;
if (!device)
return 0;
switch (libinput_event_get_type(event)) {
case LIBINPUT_EVENT_KEYBOARD_KEY:
handle_keyboard_key(libinput_device,
libinput_event_get_keyboard_event(event));
break;
case LIBINPUT_EVENT_POINTER_MOTION:
need_frame = handle_pointer_motion(libinput_device,
libinput_event_get_pointer_event(event));
break;
case LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE:
need_frame = handle_pointer_motion_absolute(
libinput_device,
libinput_event_get_pointer_event(event));
break;
case LIBINPUT_EVENT_POINTER_BUTTON:
need_frame = handle_pointer_button(libinput_device,
libinput_event_get_pointer_event(event));
break;
case LIBINPUT_EVENT_POINTER_AXIS:
need_frame = handle_pointer_axis(
libinput_device,
libinput_event_get_pointer_event(event));
break;
case LIBINPUT_EVENT_TOUCH_DOWN:
handle_touch_down(libinput_device,
libinput_event_get_touch_event(event));
break;
case LIBINPUT_EVENT_TOUCH_MOTION:
handle_touch_motion(libinput_device,
libinput_event_get_touch_event(event));
break;
case LIBINPUT_EVENT_TOUCH_UP:
handle_touch_up(libinput_device,
libinput_event_get_touch_event(event));
break;
case LIBINPUT_EVENT_TOUCH_FRAME:
handle_touch_frame(libinput_device,
libinput_event_get_touch_event(event));
break;
default:
handled = 0;
weston_log("unknown libinput event %d\n",
libinput_event_get_type(event));
}
if (need_frame)
notify_pointer_frame(device->seat);
return handled;
}
(2) 对接 wayland 协议
在阅读此之前,应当对
wayland
有足够的了解; 如果还不是很清楚, 可以参考 wayland.
wayland
定义了协议,那么 weston
对接 wayland
协议做的自然就是实现协议; 事实上, wayland
是 freedesktop
给出的标准规范, 而 weston
则是 freedesktop
给出的
参考实现.
那么回到代码上,又应该如何去做的;如果站在抽象语言的角度,wayland
是定义了一个接口类,而 weston
则是继承其并实现;在 C 语言的世界里,这通常使用函数指针来辅助完成,下面以 keyboard
作为一个示例进行描述:
// file : include/libweston/libweston.h
// 接口定义
struct weston_keyboard_grab;
struct weston_keyboard_grab_interface {
void (*key)(struct weston_keyboard_grab *grab,
const struct timespec *time, uint32_t key, uint32_t state);
void (*modifiers)(struct weston_keyboard_grab *grab, uint32_t serial,
uint32_t mods_depressed, uint32_t mods_latched,
uint32_t mods_locked, uint32_t group);
void (*cancel)(struct weston_keyboard_grab *grab);
};
// file : libweston/input.c
// 接口实现
static const struct weston_keyboard_grab_interface
default_keyboard_grab_interface = {
default_grab_keyboard_key,
default_grab_keyboard_modifiers,
default_grab_keyboard_cancel,
};
// 接口绑定
// 以 default_grab_keyboard_key 为例
WL_EXPORT void
weston_keyboard_send_key(struct weston_keyboard *keyboard,
const struct timespec *time, uint32_t key,
enum wl_keyboard_key_state state)
{
struct wl_resource *resource;
struct wl_display *display = keyboard->seat->compositor->wl_display;
uint32_t serial;
struct wl_list *resource_list;
uint32_t msecs;
if (!weston_keyboard_has_focus_resource(keyboard))
return;
resource_list = &keyboard->focus_resource_list;
serial = wl_display_next_serial(display);
msecs = timespec_to_msec(time);
wl_resource_for_each(resource, resource_list) {
send_timestamps_for_input_resource(resource,
&keyboard->timestamps_list,
time);
wl_keyboard_send_key(resource, serial, msecs, key, state); // 注意 wl 是 wayland 的缩写,这里已经完成了对 wayland 协议的对接了
}
};
static void
default_grab_keyboard_key(struct weston_keyboard_grab *grab,
const struct timespec *time, uint32_t key,
uint32_t state)
{
weston_keyboard_send_key(grab->keyboard, time, key, state);
}
四、后续
在第三大点中显示服务器输入管理设备总体设计,整体设计上整体由四部分组成.除去 display client, 在第四大点中也仅仅只讲述了linux
事件捕获和对接wayland
,也就是第一节点以及第三节点;而对于第二节点weston
自身的实现甚至于只字不提.
在第二节点中描述了 事件标准化 、事件滤波 以及 事件分发,实际上真实场景远复杂于此;第一部分 weston
(libweston/libinput-seat.c 、libweston/libinput-device.c) 合起来也只用来 1300 行左右; 但是第二部分光是 libweston/input.c
都超过了 5000 行,都还没计算 libweston/compositor.c
与此相关的代码.
以上现象说明 weston
对于输入设备的处理本质上是业务代码,维护在大量的上下文状态,根据实际业务场景添加了初看来奇奇怪怪的功能. 对于业务代码,绝非仅去描述具体的业务逻辑,更多的则是应当于去剖析作者的代码习惯以及风格,帮助读者去阅读理解理解对应的代码,让读者具备独立理解其对应业务的能力.
可能的话,对这部分内容的描述,可能从三个方面去切入;
一方面将会跟随 weston
变更记录(git),描述 weston
在添加某些功能时的具体做法,比如说添加了一个屏幕校准功能的实现,以增量的方式去讲解业务实现;
另一方面则会描述 weston
有关于此最为关键的业务流程,比如说窗口聚焦的实现,以最基础全量的方式去描述其业务实现;
最后则会讲述一下此部分对应代码提交者的代码风格,其编程手法、编程习惯以及对应的编程技巧.
实际上最后一点对于理解代码是非常重要的,理解作者的思维模式,对于代码的理解是具有非常重要的意义,远超于前两点.