[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_region
和set_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_region
或set_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_syn
c和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的輸出的縮放因子,組合器將將其縮小。如果它小于輸出縮放因子,組合器將將其放大。