
Premier pas sur Wayland
Lorem ipsum at iaculis vitae vehicula in nunc. Hac nisl molestie posuere vulputate nibh cras.At iaculis vitae vehicula in nunc. Hac nisl molestie posuere vulputate nibh cras.At iaculis vitae vehicula in nunc. Hac nisl molestie posuere vulputate nibh cras.
Article écrit par Willow Barraco
Aujourd’hui j’aimerais traiter d’un sujet très différent de ceux de d’habitude sur ce blog. On ne va pas parler de Web ou de Ruby ou de design pattern. Nous allons parler de Wayland.
Je préfère écrire sur le sujet qui m’anime en ces temps puisque je développe beaucoup autour de Wayland pour Sxmo, projet open-source pour lequel je suis co-mainteneuse. Nous sommes en effet en train de transitionner de windows manager par défaut vers Sway qui est basé sur le protocole Wayland.
J’ai donc fait évoluer bon nombre de programmes Wayland pour nos besoins, par exemple :
- bemenu pour inclure les fonctionnalités présentes dans dmenu sous X11 et desquelles nous dépendons
- wvkbd pour l’améliorer déjà graphiquement mais aussi pour y ajouter plus de possibilités autour des couches qui nous offrent le loisir de construire des layouts complexes.
- ou même directement Sway où j’ai dû améliorer (bug fixer ?
>_<
) le support des inputs de type touch dans certains cas précis.
Je vais vous présenter succinctement Wayland puis vous montrer comment débuter simplement une application et afficher quelque chose à l’écran.
Pourquoi Wayland ?
Depuis presque 40 ans maintenant, X11 est l’un des principaux protocoles permettant la création d’environnement graphique sous Unix puis Linux. Sa principale implémentation est Xorg et a permis de construire une grande variété d’environnements fenêtrés sous Linux.
Pourtant cette solution est loin, très loin d’être idéale. Déjà à mon humble niveau de connaissance voilà ce que je peux en redire :
- Développer autour de X est pénible ;
- Il n’y a aucune gestion de la sécurité. N’importe quelle application peut observer les autres et interagir avec les dispositifs d’entrée/sortie (claviers, souris, écran) ;
- Les performances sont médiocres ;
- Il y a plein de trucs qui ne sont juste pas gérés au bon niveau ; aujourd’hui par exemple, la plupart des écrans sont assez petits pour une très haute définition (on parle d’HiDPI pour haute densité de pixel) ; Il faut donc configurer chaque application pour agrandir les interfaces ou aller bidouiller les configurations des écrans en suivant des conseils avisés sur StackOverflow (ne faites pas ça).
Mais lorsque des gens un peu plus avisés que moi donnent leurs avis sur la question, voilà ce que ça donne :
En guise de tl;dr
:
Les problèmes adressés par X11 sont bien plus facilement résolubles en repartant de zéro et en ayant en tête les contraintes et les besoins d’aujourd’hui, avec l’atout de l’expérience en bonus.
Du coup, c’est quoi Wayland ?
Wayland est un protocole de l’initiative de volontaire de l’écosystème Open Source. Il date de la fin des années 2000. Il a pour objectif direct de remplacer X11 et traiter les problèmes qui lui incombent par un design plus avisé. Il doit offrir un écosystème sécurisé et fournir des solutions pour un affichage plus précis.
Notamment, une application ne peut recevoir les événements des périphériques d’entrée (clavier, souris, etc) qui sont destinés a une autre application. Il est donc impossible au sein de cet écosystème de mettre en place un keylogger.
Wayland offre des solutions pour un affichage frame perfect et double buffered par design. Cela signifie qu’il est bien plus facile de ne pas avoir de cisaillement sur l’écran. L’application va indiquer lorsque la surface sur laquelle elle écrit est prête à être affichée. L’application peut également indiquer quelle partie de cette surface a évolué. On parle de damage tracking. Wayland ne va alors actualiser que cette partie.
Par ce design plus mature, les environnements graphiques sous Wayland semblent plus fluides, plus propres. Dans le monde du jeux vidéo on parle de synchronisation verticale (le terme est imprécis ici mais le sens qu’on lui donne s’en rapproche). Le compositeur Wayland est un membre très actif contrairement a un compositeur X11. Il a par exemple le contrôle direct du frame rate. Il indique aux applications à quel moment il serait le plus intéressant de redessiner sur la surface. L’application n’a alors même plus besoin de calculer un frame rate.
Pour couronner le tout, XWayland est un client Wayland permettant de faire fonctionner des applications compatibles uniquement X11 sous Wayland. Et le comble, c’est que les applications ont l’air plus propres sous XWayland que sous Xorg sans coût additionnel…
Pour clore cette longue présentation, et pour couper court à tout débat stérile (j’en ai déjà trop eu, je vous en conjure). À la question « Faut-il créer Wayland pour remplacer X11 ? », notons que la réponse a déjà été donnée il y a bientôt 15 ans. Aujourd’hui, la seule question que l’on devrait se poser selon moi est «Faut-il préférer Wayland à X11 ?». Sur celle-ci, tous les windows managers actuels s’accordent : tous sans exception transitionnent ou ont déjà transitionné vers Wayland. Aujourd’hui, l’immense majorité des applications sont soit compatibles Wayland, soit ont des équivalents. Firefox (cette année), OBS (il y a quelques mois), Steam (dans quelque temps, pitié Valve), mais surtout les frameworks majeurs SDL, Gtk, Qt qui font tourner une grande partie des logiciels. Bref, il n’y a, en 2021, plus aucune raison de s’accrocher à X11.
Tu parles trop, c’est quand qu’on code ?
C’est la récré, on va jouer dans la cours ! On va donc ici s’amuser en C. Si vous y êtes allergiques, bah déjà c’est très dommage ! Vous ratez l’occasion d’apprendre plein de chouettes choses. Si vous ne connaissez pas trop, pas d’inquiétudes ! L’idée ici est de présenter le flow d’une application Wayland pas de faire de vous des experts.
Je vais ouvrir un dépôt Git pour cet article. Chaque étape sera commitée. Vous pourrez donc facilement retrouver vos petits ici.
La base pour nos travaux
On va commencer par un bon Hello world
comme on les aime.
// main.c #include <stdio.h> int main(int argc, char *argv[]) { fprintf(stderr, "Hello World !n"); return 0; }
# Makefile CFLAGS += -I. -DWLR_USE_UNSTABLE -std=c99 SOURCES += $(wildcard ./*.c) HEADERS += $(wildcard ./*.h) OBJECTS = $(SOURCES:.c=.o) all: run $(OBJECTS): $(HEADERS) run: $(OBJECTS) $(CC) -o run $(OBJECTS) $(CFLAGS) clean: rm -f $(OBJECTS) run
Pour ceux qui voudront reproduire chez eux : Je ne vais pas forcément recopier tout le code à chaque fois. Je vais principalement coller et commenter le diff du commit. Pour les besoins de l’article je vais probablement omettre des détails. Rendez-vous sur le dépôt GitLab pour retrouver l’ensemble du code !
Les dépendances seront seulement wayland-devel
, pango-devel
et cairo-devel
.
Comment recevoir les éléments de base de notre application ?
Pour commencer on va voir comment récupérer les éléments de base que va nous donner le compositeur Wayland. On parle ici d’éléments globaux. Dans mon cas c’est avec Sway que je vais communiquer.
@@ -1,6 +1,10 @@ CFLAGS += -I. -DWLR_USE_UNSTABLE -std=c99 +PKGS = wayland-client + +CFLAGS += $(foreach p,$(PKGS),$(shell pkg-config --cflags $(p))) +LDLIBS += $(foreach p,$(PKGS),$(shell pkg-config --libs $(p))) SOURCES += $(wildcard . } static const struct wl_registry_listener wl_registry_listener = { .global = registry_global, .global_remove = registry_global_remove, };
On définit ici notre wl_registry_listener
. C’est une API qui vient répondre à deux méthodes : global
et global_remove
. On y revient dans un instant.
int main(int argc, char *argv[]) { struct client_state state = { 0 }; struct client_app app = { 0 }; app.state = &state; state.wl_display = wl_display_connect(NULL); state.wl_registry = wl_display_get_registry(state.wl_display); wl_registry_add_listener(state.wl_registry, &wl_registry_listener, &app); wl_display_roundtrip(state.wl_display); while (wl_display_dispatch(state.wl_display)) { } return 0; }
On commence par initier nos structures d’état. Ensuite on récupère notre wl_display
et wl_registry
.
On vient ensuite brancher notre wl_registry
à notre API wl_registry_listener
. Le dernier argument &app
sera donné en argument void *data
aux différentes méthodes.
Cette dernière boucle while
va nous servir d’event loop. Pour notre cas ce sera bien suffisant.
Notons une chose importante : Avec Wayland rien n’est asynchrone. Nos méthodes global
et global_remove
vont être appelées à des moments clefs. Ici global
est appelé au wl_display_roundtrip(state.wl_display)
.
Nous brancher ainsi au registre global permet au compositeur Wayland de fournir toutes les clefs à notre application. Ici on cherche à récupérer le wl_compositor
. On utilise la méthode wl_registry_bind
pour le récupérer. On stocke ensuite son pointeur dans notre state
.
Dernier point sur lequel j’aimerais m’étendre. Il est possible de débugger tous les échanges Wayland de notre application.
$ export WAYLAND_DEBUG=1 $ make && ./run [1269718.544] -> wl_display@1.get_registry(new id wl_registry@2) [1269718.566] -> wl_display@1.sync(new id wl_callback@3) [1269718.625] wl_display@1.delete_id(3) [1269718.636] wl_registry@2.global(1, "wl_shm", 1) [1269718.643] wl_registry@2.global(2, "wl_drm", 2) [1269718.649] wl_registry@2.global(3, "zwp_linux_dmabuf_v1", 3) [1269718.653] wl_registry@2.global(4, "wl_compositor", 4) [1269718.658] -> wl_registry@2.bind(4, "wl_compositor", 4, new id [unknown]@4) [1269718.663] wl_registry@2.global(5, "wl_subcompositor", 1) [1269718.667] wl_registry@2.global(6, "wl_data_device_manager", 3) [1269718.671] wl_registry@2.global(7, "zwlr_gamma_control_manager_v1", 1) [1269718.674] wl_registry@2.global(8, "zxdg_output_manager_v1", 3) [1269718.678] wl_registry@2.global(9, "org_kde_kwin_idle", 1) [1269718.682] wl_registry@2.global(10, "zwp_idle_inhibit_manager_v1", 1) [1269718.685] wl_registry@2.global(11, "zwlr_layer_shell_v1", 4) [1269718.689] wl_registry@2.global(12, "xdg_wm_base", 2) [1269718.693] wl_registry@2.global(13, "zwp_tablet_manager_v2", 1) [1269718.696] wl_registry@2.global(14, "org_kde_kwin_server_decoration_manager", 1) [1269718.700] wl_registry@2.global(15, "zxdg_decoration_manager_v1", 1) [1269718.704] wl_registry@2.global(16, "zwp_relative_pointer_manager_v1", 1) [1269718.707] wl_registry@2.global(17, "zwp_pointer_constraints_v1", 1) [1269718.711] wl_registry@2.global(18, "wp_presentation", 1) [1269718.715] wl_registry@2.global(19, "zwlr_output_manager_v1", 2) [1269718.718] wl_registry@2.global(20, "zwlr_output_power_manager_v1", 1) [1269718.722] wl_registry@2.global(21, "zwp_input_method_manager_v2", 1) [1269718.725] wl_registry@2.global(22, "zwp_text_input_manager_v3", 1) [1269718.729] wl_registry@2.global(23, "zwlr_foreign_toplevel_manager_v1", 3) [1269718.732] wl_registry@2.global(24, "zwlr_export_dmabuf_manager_v1", 1) [1269718.736] wl_registry@2.global(25, "zwlr_screencopy_manager_v1", 3) [1269718.740] wl_registry@2.global(26, "zwlr_data_control_manager_v1", 2) [1269718.743] wl_registry@2.global(27, "zwp_primary_selection_device_manager_v1", 1) [1269718.747] wl_registry@2.global(28, "wp_viewporter", 1) [1269718.751] wl_registry@2.global(29, "zxdg_exporter_v1", 1) [1269718.754] wl_registry@2.global(30, "zxdg_importer_v1", 1) [1269718.758] wl_registry@2.global(31, "zxdg_exporter_v2", 1) [1269718.761] wl_registry@2.global(32, "zxdg_importer_v2", 1) [1269718.765] wl_registry@2.global(33, "zwp_virtual_keyboard_manager_v1", 1) [1269718.768] wl_registry@2.global(34, "zwlr_virtual_pointer_manager_v1", 2) [1269718.772] wl_registry@2.global(35, "zwlr_input_inhibit_manager_v1", 1) [1269718.776] wl_registry@2.global(36, "zwp_keyboard_shortcuts_inhibit_manager_v1", 1) [1269718.779] wl_registry@2.global(37, "wl_seat", 7) [1269718.783] wl_registry@2.global(38, "zwp_pointer_gestures_v1", 1) [1269718.786] wl_registry@2.global(39, "wl_output", 3) [1269718.790] wl_registry@2.global(40, "wl_output", 3) [1269718.794] wl_callback@3.done(116250)
On liste ici tous les événements global
de notre wl_registry
. D’ailleurs on y trouve l’endroit où on bind
notre wl_compositor
(si si, cherchez un peu)
Pour connaitre les définitions précises de tout ce que je viens d’énoncer intéressons-nous à la documentation de Wayland. Ce qui suit va dépendre de votre système mais chez moi elle se trouve ici : /usr/share/wayland/wayland.xml
À la ligne 130 nous y retrouvons :
<interface name="wl_registry" version="1"> <description summary="global registry object"> The singleton global registry object. The server has a number of global objects that are available to all clients. These objects typically represent an actual object in the server (for example, an input device) or they are singleton objects that provide extension functionality. When a client creates a registry object, the registry object will emit a global event for each global currently in the registry. Globals come and go as a result of device or monitor hotplugs, reconfiguration or other events, and the registry will send out global and global_remove events to keep the client up to date with the changes. To mark the end of the initial burst of events, the client can use the wl_display.sync request immediately after calling wl_display.get_registry. A client can bind to a global object by using the bind request. This creates a client-side handle that lets the object emit events to the client and lets the client invoke requests on the object. </description>
Un peu plus bas nous y retrouvons nos deux événements sur lesquels nous avons branché notre API d’écoute :
<event name="global"> <description summary="announce global object"> Notify the client of global objects. The event notifies the client that a global object with the given name is now available, and it implements the given version of the given interface. </description> <arg name="name" type="uint" summary="numeric name of the global object"/> <arg name="interface" type="string" summary="interface implemented by the object"/> <arg name="version" type="uint" summary="interface version"/> </event> <event name="global_remove"> <description summary="announce removal of global object"> Notify the client of removed global objects. This event notifies the client that the global identified by name is no longer available. If the client bound to the global using the bind request, the client should now destroy that object. The object remains valid and requests to the object will be ignored until the client destroys it, to avoid races between the global going away and a client sending a request to it. </description> <arg name="name" type="uint" summary="numeric name of the global object"/> </event>
Je ne vais pas m’étendre plus longuement sur cette source d’information. Vous irez lire cette documentation si vous vous intéressez aux méthodes que nous utiliserons par la suite.
Une fois que vous avez compris comment passer de la doc au code, Wayland devient un jeu d’enfant.
Comment afficher quelque chose ?
Bon, c’est bien beau tout ça, mais on veut au moins afficher des trucs. Comment on fait ?
Tout d’abord il va nous falloir créer une surface sur laquelle dessiner. Cette surface va se présenter comme un espace en mémoire d’une certaine taille qui correspondra aux différents pixels de la surface.
Pour ne pas nous embourber dans les détails, on va prendre ces deux fichiers qui vont abstraire une partie inintéressante du travail.
// shm_open.c #define _POSIX_C_SOURCE 200112L #include <errno.h> #include <fcntl.h> #include <sys/mman.h> #include <time.h> #include <unistd.h> static void randname(char *buf) { struct timespec ts; long r; clock_gettime(CLOCK_REALTIME, &ts); r = ts.tv_nsec; for (int i = 0; i < 6; ++i) { buf[i] = 'A'+(r&15)+(r&16)*2; r >>= 5; } } static int create_shm_file(void) { int retries = 100; int fd; do { char name[] = "/wl_shm-XXXXXX"; randname(name + sizeof(name) - 7); --retries; fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); if (fd >= 0) { shm_unlink(name); return fd; } } while (retries > 0 && errno == EEXIST); return -1; } int allocate_shm_file(size_t size) { int fd = create_shm_file(); int ret; if (fd < 0) return -1; do { ret = ftruncate(fd, size); } while (ret < 0 && errno == EINTR); if (ret < 0) { close(fd); return -1; } return fd; }
// shm_open.h #ifndef shm_open_h_INCLUDED #define shm_open_h_INCLUDED void randname(char *buf); int create_shm_file(void); int allocate_shm_file(size_t size); #endif
Je vais passer sous silence cette partie, car elle ne nous intéresse pas vraiment. Retenez que ce code nous permet d’allouer un shm
pour shared memory, c’est-à-dire un espace de mémoire partagé. On va en avoir besoin pour créer notre wl_buffer
final.
De retour sur notre main.c
. Il va nous falloir nous lier (bind
) au wl_shm
fournit par le compositeur. Il nous servira à créer le wl_buffer
en partant du shm
. Vous suivez ?
Toujours en utilisant notre registre global
.
@@ -20,6 +30,9 @@ registry_global(void *data, struct wl_registry *wl_registry, if (strcmp(interface, wl_compositor_interface.name) == 0) { app->state->wl_compositor = wl_registry_bind( wl_registry, name, &wl_compositor_interface, 4); + } else if (strcmp(interface, wl_shm_interface.name) == 0) { + app->state->wl_shm = wl_registry_bind( + wl_registry, name, &wl_shm_interface, 1); } }
C’est ici que l’on récupère le wl_shm
.
uint32_t setup_buffer(struct client_app *app) { int stride = app->width * 4; app->size = stride * app->height; int fd = allocate_shm_file(app->size); if (fd == -1) { return 1; } app->pool_data = mmap(NULL, app->size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (app->pool_data == MAP_FAILED) { close(fd); return 1; } struct wl_shm_pool *pool = wl_shm_create_pool(app->state->wl_shm, fd, app->size); app->wl_buffer = wl_shm_pool_create_buffer(pool, 0, app->width, app->height, stride, WL_SHM_FORMAT_XRGB8888); wl_shm_pool_destroy(pool); close(fd); return 0; }
Cette nouvelle méthode setup_buffer
nous permet d’allouer un espace de mémoire de la taille souhaitée (ici width
* height
) à partir du wl_shm
. On y trouve l’usage de notre allocate_shm_file(app->size)
.
On vient ensuite créer notre wl_buffer
à partir de la pool
de donnée crée grâce à wl_shm_create_pool
(RTFM).
@@ -48,6 +88,14 @@ main(int argc, char *argv[]) wl_registry_add_listener(state.wl_registry, &wl_registry_listener, &app); wl_display_roundtrip(state.wl_display); + app.width = 500; + app.height = 500; + setup_buffer(&app); + + app.wl_surface = wl_compositor_create_surface(state.wl_compositor); + wl_surface_attach(app.wl_surface, app.wl_buffer, 0, 0); + wl_surface_commit(app.wl_surface); + while (wl_display_dispatch(state.wl_display)) { }
Voilà, nous avons créé notre wl_buffer
grâce à setup_buffer(&app)
. Maintenant on peut créer notre wl_surface
et venir lui attacher notre wl_buffer
. Et lorsqu’enfin on vient commit
la surface, rien ne se produit…
Non mais restez ! On y est presque ! C’est quand même pas une sinécure !
Pour pouvoir afficher quelque chose encore faut-il avoir une fenêtre ! Notre surface est prête, mais elle n’est pas encore rattachée à notre environnement graphique. On parle ici du rôle que nous allons donner à notre wl_surface
. En effet elle pourrait très bien servir à une pop up, à un curseur, ou à une interface fixe. Nous ici ce qu’on veut c’est une fenêtre !
Pour ce faire nous allons nous reposer sur un protocole additionnel à Wayland. Il ne fait pas partie du cœur de Wayland mais d’une liste d’autres protocoles qui viennent l’enrichir. Et oui, afficher une fenêtre ne fait pas partie de Wayland.
Le protocole pour les fenêtres c’est xdg-shell. Il est bien sûr implémenté par Sway. Nous ajoutons la définition de ce protocole dans proto/xdg-shell.xml
. Nous devons juste en générer les sources que notre client va utiliser. Heureusement nous avons des outils pour ça. Pour ce faire :
@@ -6,6 +6,10 @@ PKGS = wayland-client CFLAGS += $(foreach p,$(PKGS),$(shell pkg-config --cflags $(p))) LDLIBS += $(foreach p,$(PKGS),$(shell pkg-config --libs $(p))) +WAYLAND_HEADERS = $(wildcard proto }
Ici on vient effectivement indiquer au compositeur qu’on a redessiné sur la surface via wl_surface_damage
au moment où Cairo peint dessus. Par contre on ne va attacher notre wl_buffer
que dans la méthode redraw_flip
. Cette méthode va être appelée à chaque événement surface_frame_callback
émis par la surface.
Cet événement est émis par le compositeur et indique que c’est un bon moment pour commencer à écrire sur la surface.
Request a notification when it is a good time to start drawing a new frame, by creating a frame callback. This is useful for throttling redrawing operations, and driving animations.
Voilà ! Vous pouvez maintenant cliquer frénétiquement sur votre souris ! Et si vous cliquez super, mais alors super vite ! Il est même possible que votre écran ne voit jamais la couleur du premier coup de peinture. Et en retour, si vous ne cliquez pas, rien ne se redessine.
C’est propre, c’est beau.
Conclusion, bon sang !
Si vous souhaitez approfondir le sujet, je ne peux que vous conseiller le Wayland Book de Drew DeVault.
Pour le coup j’ai plus grand-chose à raconter. Et puis comme je pense que j’en ai déjà fait pas mal, bah je vous souhaite une bonne journée.
Au revoir.