8 . 深入理解Surface

[TOC]
到目前为止,我们展示的Surface接口基本区域足以向用户呈现数据,但Surface接口提供了许多额外的请求和事件,以便更有效地使用。许多(即使不是大多数)应用程序不需要每帧都重绘整个Surface。即使决定何时绘制下一帧,最好也是在组合器的帮助下完成。在本章中,我们将深入探讨wl_surface的功能。

8.1 Surface生命周期

我们之前提到,Wayland被设计为原子性地更新所有内容,这样任何一帧都不会以无效或中间状态呈现。应用程序窗口和其他Surface的许多属性都可以配置,而背后的驱动机制就是wl_surface本身。

每个Surface都有待处理状态和已应用状态,创建时没有任何状态。待处理状态在来自客户端的任何数量的请求和来自服务器的任何事件过程中进行谈判,当双方都同意它代表一致的Surface状态时,Surface被提交-并且待处理状态应用于当前Surface状态。在此之前,组合器将继续渲染最后一个一致状态;一旦提交,将从下一帧开始使用新状态。

在原子更新的状态中包括:

  • 已附加的wl_buffer,或构成Surface内容的像素
  • 从上一帧开始“Damage”并需要重绘的区域
  • 接受输入事件的区域
  • 被认为是非透明的区域1
  • 附加的wl_buffer上的转换,以旋转或显示缓冲区的子集
  • 缓冲区的缩放因子,用于HiDPI显示器

除了这些Surface特性之外,Surface的角色可能还有其他双缓冲状态。所有这些状态以及与角色关联的任何状态,都将在发送wl_surface.commit时应用。如果您改变主意,可以多次发送这些请求,只有这些属性的最新值将在Surface最终提交时考虑。

当你第一次创建Surface时,初始状态是无效的。为了使其有效(或映射Surface),您需要提供必要的信息来为该Surface构建第一个一致的状态。这包括为其分配角色(例如xdg_toplevel),分配和附加缓冲区,以及为该Surface配置角色特定的状态。当您正确配置此状态并发送wl_surface.commit时,Surface变得有效(或映射),并将由组合器呈现。

下一个问题是:我应该什么时候准备新的一帧?

1这是由组合器用于优化目的的。

8.2 帧回调

更新Surface的最简单方法是,当需要更改时,简单地渲染并附加新的帧。这种方法适用于例如事件驱动的应用程序。用户按下键,文本框需要重新渲染,因此您可以立即重新渲染它,Damage相应的区域,并在下一个帧上附加一个新的缓冲区。

但是,有些应用程序可能希望连续渲染帧。您可能正在渲染视频游戏的帧、回放视频或渲染动画。您的显示器具有固有刷新率,或能够显示更新的最快速度(通常是一个数字,如60 Hz、144 Hz等)。以比这更快的速度渲染帧是没有意义的,这样做会浪费CPU、GPU甚至用户的电池资源。如果在每次显示刷新之间发送了几个帧,除了最后一个之外的所有帧都将被丢弃,并且已经白费了。

此外,组合器可能不想为您显示新帧。您的应用程序可能处于屏幕之外、最小化或隐藏在其他窗口后面;或者只显示您的应用程序的小缩略图,因此他们可能希望以较慢的帧速率渲染您以节省资源。因此,在Wayland客户端中持续渲染帧的最佳方法是让组合器告诉您何时准备好新的一帧:使用帧回调。

<interface name="wl_surface" version="4">
  <!-- ... -->

  <request name="frame">
    <arg name="callback" type="new_id" interface="wl_callback" />
  </request>

  <!-- ... -->
</interface>

这个请求将分配一个wl_callback对象,其接口非常简单:

<interface name="wl_callback" version="1">
  <event name="done">
    <arg name="callback_data" type="uint" />
  </event>
</interface>

当你请求一个Surface的帧回调时,一旦组合器为该Surface的新帧做好准备,它将向回调对象发送一个done事件。 在帧事件的情况下,callback_data被设置为当前时间(以毫秒为单位),从某个未指定的时间点开始。 你可以将其与上一帧进行比较,以计算动画的进度或缩放输入事件。1

有了帧回调功能,为什么我们不更新第7章的应用程序以实现每帧滚动一点呢?让我们从为我们的client_state结构添加一点状态开始:

--- a/client.c
+++ b/client.c
@@ -71,6 +71,8 @@ struct client_state {
    struct xdg_surface *xdg_surface;
    struct xdg_toplevel *xdg_toplevel;
+   /* State */
+   float offset;
+   uint32_t last_frame;
 };
 
 static void wl_buffer_release(void *data, struct wl_buffer *wl_buffer) {

然后我们将更新我们的draw_frame函数以考虑偏移量:

@@ -107,9 +109,10 @@ draw_frame(struct client_state *state)
    close(fd);
 
    /* Draw checkerboxed background */
+   int offset = (int)state->offset % 8;
    for (int y = 0; y < height; ++y) {
        for (int x = 0; x < width; ++x) {
-           if ((x + y / 8 * 8) % 16 < 8)
+           if (((x + offset) + (y + offset) / 8 * 8) % 16 < 8)
                data[y * width + x] = 0xFF666666;
            else
                data[y * width + x] = 0xFFEEEEEE;

在主函数中,让我们为我们的第一个新帧注册一个回调:

@@ -195,6 +230,9 @@ main(int argc, char *argv[])
    xdg_toplevel_set_title(state.xdg_toplevel, "Example client");
    wl_surface_commit(state.wl_surface);
 
+   struct wl_callback *cb = wl_surface_frame(state.wl_surface);
+   wl_callback_add_listener(cb, &wl_surface_frame_listener, &state);
+
    while (wl_display_dispatch(state.wl_display)) {
        /* This space deliberately left blank */
    }

然后像这样实现它:

@@ -147,6 +150,38 @@ static const struct xdg_wm_base_listener xdg_wm_base_listener = {
    .ping = xdg_wm_base_ping,
 };
 
+static const struct wl_callback_listener wl_surface_frame_listener;
+
+static void
+wl_surface_frame_done(void *data, struct wl_callback *cb, uint32_t time)
+{
+   /* Destroy this callback */
+   wl_callback_destroy(cb);
+
+   /* Request another frame */
+   struct client_state *state = data;
+   cb = wl_surface_frame(state->wl_surface);
+   wl_callback_add_listener(cb, &wl_surface_frame_listener, state);
+
+   /* Update scroll amount at 24 pixels per second */
+   if (state->last_frame != 0) {
+       int elapsed = time - state->last_frame;
+       state->offset += elapsed / 1000.0 * 24;
+   }
+
+   /* Submit a frame for this event */
+   struct wl_buffer *buffer = draw_frame(state);
+   wl_surface_attach(state->wl_surface, buffer, 0, 0);
+   wl_surface_damage_buffer(state->wl_surface, 0, 0, INT32_MAX, INT32_MAX);
+   wl_surface_commit(state->wl_surface);
+
+   state->last_frame = time;
+}
+
+static const struct wl_callback_listener wl_surface_frame_listener = {
+   .done = wl_surface_frame_done,
+};
+
 static void
 registry_global(void *data, struct wl_registry *wl_registry,
        uint32_t name, const char *interface, uint32_t version)

现在,对于每个帧,我们将:

  • 销毁现在使用的帧回调。
  • 请求下一帧的新回调。
  • 渲染并提交新帧。
  • 第三步,分解后是:

使用自上一帧以来的时间以恒定的速率更新我们的状态并产生新的偏移量。

  • 为新缓冲区准备一帧并为其进行渲染。
  • 将新缓冲区附加到我们的Surface上。
  • Damage整个Surface。
  • 提交Surface。

第3步和第4步更新了Surface的挂起状态,为其提供了新的缓冲区并指示整个Surface已更改。第5步提交了这个挂起的待处理状态,将其应用于Surface的当前状态,并在下一帧上使用它。原子地应用这个新的缓冲区意味着我们永远不会显示最后一半的帧,从而产生一个很好的无撕裂体验。编译并运行更新的客户端以尝试一下!

1 需要更准确的东西吗?在第12.1章中,我们谈论了一个协议扩展,它以纳秒级的分辨率准确地告诉您每个帧何时呈现给用户。

8.3 SurfaceDamage

在最后一个示例中,我们提交新帧时添加了这一行代码:

wl_surface_damage_buffer(state->wl_surface, 0, 0, INT32_MAX, INT32_MAX);

如果你注意到了,那么眼睛真尖! 这段代码Damage了我们的Surface,向组合器表明需要重新绘制。 在这里,我们Damage了整个Surface(以及超出它的一部分),但我们也可以只Damage它的一部分。

例如,假设您编写了一个GUI工具包,用户正在文本框中键入。 该文本框可能只占据窗口的一小部分,而每个新字符占据的更小部分。 当用户按下键时,您可以只渲染添加到他们正在编写的文本中的新字符,然后只Damage Surface的那一部分。 组合器可以复制Surface的很小一部分,这可以大大加快速度 - 尤其是对于嵌入式设备。 当您在字符之间闪烁光标时,您将需要提交Surface Damage的更新,而当用户更改视图时,您可能会Damage整个Surface。 通过这种方式,每个人都可以减少工作量,并且用户将感谢您改善了电池寿命。

注意:Wayland协议提供了两个请求来Damage Surface:damage和damage_buffer。 前者实际上已被弃用,您应该只使用后者。 它们之间的区别在于,前者考虑到了影响Surface的所有转换,例如旋转、比例因子、缓冲区位置和裁剪。 后者则将Damage相对于缓冲区应用,这通常更容易理解。

8.4 Surface区域

我们已经通过wl_compositor.create_surface接口使用过wl_compositor接口来创建wl_surface。 但是请注意,它还有第二个请求:create_region。

<interface name="wl_compositor" version="4">
  <request name="create_surface">
    <arg name="id" type="new_id" interface="wl_surface" />
  </request>

  <request name="create_region">
    <arg name="id" type="new_id" interface="wl_region" />
  </request>
</interface>

wl_region接口定义了一组矩形,它们共同构成一个任意形状的几何区域。其请求允许您对定义的几何进行位运算,通过从其添加或减去矩形来实现。

<interface name="wl_region" version="1">
  <request name="destroy" type="destructor" />

  <request name="add">
    <arg name="x" type="int" />
    <arg name="y" type="int" />
    <arg name="width" type="int" />
    <arg name="height" type="int" />
  </request>

  <request name="subtract">
    <arg name="x" type="int" />
    <arg name="y" type="int" />
    <arg name="width" type="int" />
    <arg name="height" type="int" />
  </request>
</interface>

例如,要创建一个带孔的矩形,您可以:

  • 发送wl_compositor.create_region以分配一个wl_region对象。
  • 发送wl_region.add(0, 0, 512, 512)创建一个512x512的矩形。
  • 发送wl_region.subtract(128, 128, 256, 256)从该区域的中间移除一个256x256的矩形。

这些区域也可以是不连接的;它不必是一个单一的连续多边形。 一旦您创建了这样的区域,您可以通过wl_surface接口将其传递给set_opaque_regionset_input_region请求。

<interface name="wl_surface" version="4">
  <request name="set_opaque_region">
    <arg name="region" type="object" interface="wl_region" allow-null="true" />
  </request>

  <request name="set_input_region">
    <arg name="region" type="object" interface="wl_region" allow-null="true" />
  </request>
</interface>

不透明区域是向组合器提供您Surface哪些部分被视为不透明的一种提示。基于这些信息,它们可以优化其渲染过程。例如,如果您的Surface是完全不透明的并遮挡了下面的另一个窗口,则组合器不会浪费任何时间在重绘您的窗口下面的窗口上。默认情况下,这是空的,这假定您Surface的任何部分都可能是透明的。这使得默认情况下的效果最差,但最正确。

输入区域表示您的Surface哪些部分接受指针和触摸输入事件。例如,您可以在您的Surface下方绘制阴影,但该区域中的输入事件应传递给下面的客户端。或者,如果您的窗口形状不寻常,则可以创建与该形状相同的输入区域。对于大多数Surface类型默认情况下,整个Surface都接受输入。

这两个请求都可以通过传递null而不是wl_region对象来设置空区域。它们也都是双缓冲的-因此发送wl_surface.commit以使更改生效。一旦使用它发送了set_opaque_regionset_input_region请求,就可以销毁wl_region对象以释放其资源。在这些请求发送后更新区域不会更新Surface的状态。

8.5 子Surface

在核心Wayland协议中只定义了一个与Surface相关的角色:子Surface。它们相对于父Surface的X、Y位置——不必受其父Surface边界的限制——以及相对于其兄弟和父Surface的Z顺序。

此功能的一些用例包括以本地像素格式播放带有RGBA用户界面的视频Surface或显示在顶部的字幕,使用OpenGLSurface作为主应用程序界面,并使用子Surface以软件方式呈现窗口装饰,或者无需重新绘制即可移动UI的各个部分。借助硬件平面,组合器甚至可能无需为更新子Surface而重新绘制任何内容。特别是在嵌入式系统中,当它适合您的用例时,这特别有用。巧妙设计的应用程序可以利用子Surface实现很高的效率。

管理这些的接口是wl_subcompositor接口。get_subsurface请求是子Surface管理器的主要入口点。

<request name="get_subsurface">
  <arg name="id" type="new_id" interface="wl_subsurface" />
  <arg name="surface" type="object" interface="wl_surface" />
  <arg name="parent" type="object" interface="wl_surface" />
</request>

一旦将wl_subsurface对象与wl_surface关联起来,它就成为该Surface的子对象。子Surface本身可以具有子Surface,从而在任何顶级Surface之下形成有序的Surface树。通过wl_subsurface接口可以操纵这些子对象:

*set_position(x, y):设置子Surface相对于其父Surface的位置。
*place_above(sibling, x, y):将子Surface放置在其同级Surface的上方,并相对于该同级Surface设置位置。

  • place_below(sibling, x, y):将子Surface放置在其同级Surface的下方,并相对于该同级Surface设置位置。
  • set_sync()和set_desync():控制子Surface是否与其父Surface同步。当同步时,子Surface的绘制将在父Surface的绘制之后进行。

注意,这些操作不会立即生效,而是需要发送wl_surface.commit请求以使更改生效。此外,当不再需要子Surface时,应使用wl_subsurface.destroy()来释放相关资源。

通过使用子Surface,应用程序可以更有效地组织和管理其界面组件,从而提高渲染性能并简化代码逻辑。子Surface的使用尤其适用于具有复杂用户界面的应用程序,例如视频播放器、图形编辑器和多窗口工作环境等。

<request name="set_position">
  <arg name="x" type="int" summary="x coordinate in the parent surface"/>
  <arg name="y" type="int" summary="y coordinate in the parent surface"/>
</request>

<request name="place_above">
  <arg name="sibling" type="object" interface="wl_surface" />
</request>

<request name="place_below">
  <arg name="sibling" type="object" interface="wl_surface" />
</request>

<request name="set_sync" />
<request name="set_desync" />

子Surface的Z顺序可以通过将其放置在与其共享相同父级Surface的任何兄弟Surface之上或之下,或放置在父级Surface本身之上或之下进行更改。

需要解释一下wl_subsurface的各种属性的同步。这些位置和Z顺序属性与父级Surface的生命周期同步。当向主Surface发送wl_surface.commit请求时,其所有子Surface的位置和Z顺序将随之应用更改。

然而,与此子Surface关联的wl_surface状态,例如缓冲区的附加和Damage的累积,不需要与父级Surface的生命周期关联。这是set_sync和set_desync请求的目的。与父级Surface同步的子Surface将在父级Surface提交时提交其所有状态。脱节的Surface将像任何其他Surface一样管理自己的提交生命周期。

简而言之,同步和异步请求是非缓冲的,并立即应用。位置和Z顺序请求是缓冲的,并且不受到Surface的同步/异步属性的影响-它们始终与父级Surface一起提交。相关wl_surface上的其余Surface状态根据子Surface的同步/异步状态提交。

1 忽略已弃用的wl_shell接口。

8.6 高密度Surface(HiDPI)

在过去几年中,高端显示器的像素密度有了巨大的飞跃,新的显示器在相同的物理区域内塞入了过去几年两倍的像素。我们将这些显示器称为“HiDPI”,这是“每英寸高点”的简称。然而,这些显示器比它们的“LoDPI”同类领先很多,需要进行应用程序级别的更改才能正确使用它们。如果在相同空间内将屏幕分辨率加倍,我们的用户界面的大小将是原来的一半。对于大多数显示器来说,这会使文本无法阅读,并且交互元素会变得非常小。

然而,作为交换,我们获得了更多的图形保真度与我们的矢量图形,特别是在文本渲染方面。Wayland通过为每个输出添加“缩放因子”来解决这个问题,并且客户端被期望将此缩放因子应用于其界面。此外,不知情的HiDPI客户端通过不采取行动来表明其局限性,允许组合器通过放大其缓冲区来弥补这一点。组合器通过相应的事件为每个输出发送缩放因子信号:

<interface name="wl_output" version="3">
  <!-- ... -->
  <event name="scale" since="2">
    <arg name="factor" type="int" />
  </event>
</interface>

请注意,这是在版本2中添加的,因此当绑定到wl_output全局时,您必须将版本设置为至少2才能接收到这些事件。然而,这不足以决定在你的客户端中使用HiDPI。为了做出这个决定,组合器还必须为你的wl_surface发送进入事件,以表明它已经“进入”(正在显示在)特定的输出或多个输出上:

<interface name="wl_surface" version="4">
  <!-- ... -->
  <event name="enter">
    <arg name="output" type="object" interface="wl_output" />
  </event>
</interface>

一旦你知道客户端显示的输出集合,它应该取缩放因子的最大值,将其缓冲区的大小(以像素为单位)乘以该值,然后以2倍或3倍(或N倍)的缩放比例呈现UI。然后,像这样指示缓冲区准备的缩放比例:

<interface name="wl_surface" version="4">
  <!-- ... -->
  <request name="set_buffer_scale" since="3">
    <arg name="scale" type="int" />
  </request>
</interface>

注意:这需要版本3或更新版本的wl_surface。这是您应该在绑定到wl_compositor时传递给wl_registry的版本号。

在接下来的wl_surface.commit中,你的Surface将采用这个缩放因子。如果它大于显示该Surface的输出的缩放因子,组合器将将其缩小。如果它小于输出缩放因子,组合器将将其放大。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容