Évaluons votre projet

Premier pas sur Wayland

Publié le 24 septembre 2021 par Eddie Barraco | open source - système

Cet article est publié sous licence CC BY-NC-SA

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 ./*.c)
 HEADERS += $(wildcard ./*.h)
@@ -11,7 +15,7 @@ all: run
 $(OBJECTS): $(HEADERS)

 run: $(OBJECTS)
-	$(CC) -o run $(OBJECTS) $(CFLAGS)
+	$(CC) -o run $(OBJECTS) $(CFLAGS) $(LDLIBS)

 clean:
 	rm -f $(OBJECTS) run
struct client_state {
	struct wl_display *wl_display;
	struct wl_registry *wl_registry;
	struct wl_compositor *wl_compositor;
};

struct client_app {
	struct client_state *state;
};

Ces deux client_state et client_app vont contenir l’ensemble des choses dont on va avoir besoin au cours du programme.


static void
registry_global(void *data, struct wl_registry *wl_registry,
		uint32_t name, const char *interface, uint32_t version)
{
	struct client_app *app = data;
	if (strcmp(interface, wl_compositor_interface.name) == 0) {
		app->state->wl_compositor = wl_registry_bind(
			wl_registry, name, &wl_compositor_interface, 4);
	}
}

static void
registry_global_remove(void *data,
		struct wl_registry *wl_registry, uint32_t name)
{
	/* This space deliberately left blank */
}

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)) {
		/* This space deliberately left blank */
	}

	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 // shm_open_h_INCLUDED

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)) {
 		/* This space deliberately left blank */
 	}

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/*.xml)
+HEADERS += $(WAYLAND_HEADERS:.xml=-client-protocol.h)
+SOURCES += $(WAYLAND_HEADERS:.xml=-client-protocol.c)
+
 SOURCES += $(wildcard ./*.c)
 HEADERS += $(wildcard ./*.h)
 OBJECTS = $(SOURCES:.c=.o)
@@ -14,6 +18,12 @@ all: run

 $(OBJECTS): $(HEADERS)

+proto/%-client-protocol.c: proto/%.xml
+	wayland-scanner code < $? > $@
+
+proto/%-client-protocol.h: proto/%.xml
+	wayland-scanner client-header < $? > $@
+
 run: $(OBJECTS)
 	$(CC) -o run $(OBJECTS) $(CFLAGS) $(LDLIBS)

wayland-scanner nous permet de générer les fichiers headers ainsi que le code glu dont va se servir notre client. Notre application peut donc maintenant utiliser les protocoles additionnels s’ils sont implémentés par le compositeur.

Bon, revenons à nos moutons ! On veut afficher notre wl_buffer dans une fenêtre !

static void
xdg_wm_base_ping(void *data, struct xdg_wm_base *xdg_wm_base, uint32_t serial)
{
	xdg_wm_base_pong(xdg_wm_base, serial);
}

static const struct xdg_wm_base_listener xdg_wm_base_listener = {
	.ping = xdg_wm_base_ping,
};
@@ -33,6 +47,11 @@ registry_global(void *data, struct wl_registry *wl_registry,
 	} else if (strcmp(interface, wl_shm_interface.name) == 0) {
 		app->state->wl_shm = wl_registry_bind(
 				wl_registry, name, &wl_shm_interface, 1);
+	} else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
+		app->state->xdg_wm_base = wl_registry_bind(
+				wl_registry, name, &xdg_wm_base_interface, 1);
+		xdg_wm_base_add_listener(app->state->xdg_wm_base,
+				&xdg_wm_base_listener, app->state);
 	}
 }

On commence à connaitre la chanson. On utilise global pour bind le xdg_wm_base. Notez au passage que j’implémente le listener ping pour y répondre pong. Sans quoi ça va se voir que notre programme ne sert à rien !

static void
xdg_surface_configure(void *data,
		struct xdg_surface *xdg_surface, uint32_t serial)
{
	struct client_app *app = data;
	xdg_surface_ack_configure(xdg_surface, serial);

	wl_surface_attach(app->wl_surface, app->wl_buffer, 0, 0);
	wl_surface_commit(app->wl_surface);
}

static const struct xdg_surface_listener xdg_surface_listener = {
	.configure = xdg_surface_configure,
};
@@ -93,7 +127,11 @@ main(int argc, char *argv[])
 	setup_buffer(&app);

 	app.wl_surface = wl_compositor_create_surface(state.wl_compositor);
-	wl_surface_attach(app.wl_surface, app.wl_buffer, 0, 0);
+	app.xdg_surface = xdg_wm_base_get_xdg_surface(
+		state.xdg_wm_base, app.wl_surface);
+	xdg_surface_add_listener(app.xdg_surface, &xdg_surface_listener, &app);
+	state.xdg_toplevel = xdg_surface_get_toplevel(app.xdg_surface);
+	xdg_toplevel_set_title(state.xdg_toplevel, "Example client");
 	wl_surface_commit(app.wl_surface);

 	while (wl_display_dispatch(state.wl_display)) {

Ensuite on s’en sert pour créer un xdg_surface. Notez l’usage de xdg_wm_base_get_xdg_surface. Ici on passe notre wl_surface pour lui donner le rôle d’un xdg_surface.

On vient accrocher ses événements à une API (ici : xdg_surface_listener). Et c’est enfin lors de l’événement configure que l’on vient attacher notre wl_buffer à notre wl_surface. Pourquoi pas avant me demanderez-vous ? Parce que c’est le protocole ! Vous n’avez pas lu le protocole…

  <interface name="xdg_surface" version="2">
    <description summary="desktop user interface surface base interface">
      ...

      Creating an xdg_surface from a wl_surface which has a buffer attached or
      committed is a client error, and any attempts by a client to attach or
      manipulate a buffer prior to the first xdg_surface.configure call must
      also be treated as errors.

      ...
    </description>

Ça y est, enfin… Notre fenêtre apparait sous nos yeux ébahis ! C’est beau quand même… Bon, elle est toute noire. Y’a rien dessus ! C’est un peu normal, on a rien mis dans nos pixels.

Et du coup, on dessine comment avec ton machin là ?

On pourrait s’amuser à aller modifier la valeur de chacun de nos pixels un à un. Pour les besoins de la démonstration on va directement ajouter Cairo. Et tant qu’à faire on utilise Pangocairo. Comme ça si vous voulez afficher du texte dans votre application, vous aurez déjà ce qu’il faut.

@@ -1,7 +1,7 @@

 CFLAGS += -I. -DWLR_USE_UNSTABLE -std=c99 -lrt

-PKGS = wayland-client
+PKGS = wayland-client pangocairo

 CFLAGS += $(foreach p,$(PKGS),$(shell pkg-config --cflags $(p)))
 LDLIBS += $(foreach p,$(PKGS),$(shell pkg-config --libs $(p)))
@@ -87,10 +89,28 @@ setup_buffer(struct client_app *app)

 	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);
+			app->width, app->height, stride, WL_SHM_FORMAT_ARGB8888);
 	wl_shm_pool_destroy(pool);
 	close(fd);

+	cairo_surface_t *s = cairo_image_surface_create_for_data(app->pool_data,
+		CAIRO_FORMAT_ARGB32,
+		app->width, app->height, stride);
+
+	app->cairo = cairo_create(s);
+	cairo_save(app->cairo);
+
+	cairo_rectangle(app->cairo, 0, 0, app->width, app->height);
+	cairo_set_source_rgba(
+		app->cairo,
+		0.5,
+		0.5,
+		0.5,
+		1
+	);
+	cairo_fill(app->cairo);
+
 	return 0;
 }

On initie notre cairo en lui fournissant simplement notre pool_data, un format de pixel et les dimensions de notre surface.

Bien sûr ne venez pas ajouter toute votre logique d’affichage dans ce setup_buffer ! Ici c’est pour illustrer ! Retenez juste que vous pouvez maintenant faire appel à votre instance de Cairo app->cairo comme bon vous semblera.

Comment recevoir les inputs ?

Super mais maintenant comment on interagit avec notre application ? Moi ce que je veux c’est cliquer dessus !

On va commencer par faire les choses dans le bon ordre. En premier lieu il va nous falloir récupérer le global wl_seat. Grâce à celui-ci, on va pouvoir tester les différentes capacités qu’a l’utilisateur dans son seat pour utiliser l’application. Il peut avoir un clavier et une souris. Il peut aussi s’agir d’un écran tactile ou d’une tablette graphique. Chacun de ces cas est différent et va donner lieu a des événements dédiés.

Récupérons déjà le wl_seat.

void
seat_capabilities(void *data, struct wl_seat *wl_seat,
		 enum wl_seat_capability caps)
{
	struct client_app *app = data;
}

void
seat_name(void *data, struct wl_seat *wl_seat, const char *name)
{}

static const struct wl_seat_listener wl_seat_listener = {
	.capabilities = seat_capabilities,
	.name = seat_name,
};
@@ -138,6 +138,7 @@ struct registry_global{
 static void
 registry_global(void *data, struct wl_registry *wl_registry,
 		uint32_t name, const char *interface, uint32_t version)
@@ -49,6 +66,9 @@ registry_global(void *data, struct wl_registry *wl_registry,
 	} else if (strcmp(interface, wl_shm_interface.name) == 0) {
 		app->state->wl_shm = wl_registry_bind(
 				wl_registry, name, &wl_shm_interface, 1);
+	} else if (strcmp(interface, wl_seat_interface.name) == 0) {
+		app->state->wl_seat = wl_registry_bind(wl_registry, name, &wl_seat_interface, 1);
+		wl_seat_add_listener(app->state->wl_seat, &wl_seat_listener, app);
 	} else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
 		app->state->xdg_wm_base = wl_registry_bind(
 				wl_registry, name, &xdg_wm_base_interface, 1);

Super maintenant gérons le cas où le wl_seat est capable d’interagir à la souris. On parle ici de pointer.

void
wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, uint32_t serial,
		struct wl_surface *surface, wl_fixed_t surface_x,
		wl_fixed_t surface_y)
{
	fprintf(stderr, "Pointer enter !\n");
}

void
wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, uint32_t serial,
		struct wl_surface *surface)
{
	fprintf(stderr, "Pointer leave !\n");
}

void
wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, uint32_t time,
		wl_fixed_t surface_x, wl_fixed_t surface_y)
{
	fprintf(stderr, "Pointer motion ! %ix%i\n",
		wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y));
}

void
wl_pointer_button(void *data, struct wl_pointer *wl_pointer, uint32_t serial,
		uint32_t time, uint32_t button, uint32_t state)
{
	fprintf(stderr, "Pointer button !\n");
}

static const struct wl_pointer_listener wl_pointer_listener = {
	.enter = wl_pointer_enter,
	.leave = wl_pointer_leave,
	.motion = wl_pointer_motion,
	.button = wl_pointer_button,
};
@@ -138,6 +138,7 @@ struct registry_global{
 void
 seat_capabilities(void *data, struct wl_seat *wl_seat,
 		 enum wl_seat_capability caps)
 {
 	struct client_app *app = data;
+
+	if (caps & WL_SEAT_CAPABILITY_POINTER) {
+		app->state->wl_pointer = wl_seat_get_pointer(wl_seat);
+		wl_pointer_add_listener(app->state->wl_pointer, &wl_pointer_listener, app);
+	}
 }

 void

Bonjour, bonsoir à tous j’aimerais ouvrir une parenthèse !

Auriez-vous une minute à m’accorder pour parler de bitmask ? Cette jolie chose que l’on utilise finalement que dans les langages bas niveau et qui offre pourtant tellement de possibilités.

Ici c’est bien du caps & WL_SEAT_CAPABILITY_POINTER dont on va parler.

Le bitmasking c’est le fait d’utiliser les bits d’une variable pour le comparer avec un masque.

Prenons deux minutes pour présenter un cas concret. Imaginons, à tout hasard, que l’on énumère les capacités d’un utilisateur.

enum wl_seat_capabilities {
    WL_SEAT_CAPABILITY_POINTER = 1 << 1,  // -> 1 -> 00000001
    WL_SEAT_CAPABILITY_TOUCH = 1 << 2,    // -> 2 -> 00000010
    WL_SEAT_CAPABILITY_TABLET = 1 << 3,   // -> 4 -> 00000100
    WL_SEAT_CAPABILITY_KEYBOARD = 1 << 4, // -> 8 -> 00001000
    
};

Et à quoi ça sert ? Eh bien si l’on veut savoir si notre caps est une combinaison de deux d’entre elles, on peut tout simplement effectuer une comparaison par masque :

                    caps & (WL_SEAT_CAPABILITY_POINTER | WL_SEAT_CAPABILITY_KEYBOARD)
                    caps & (00000001 | 00001000)
                    caps & 00001001
  par exemple : 00000001 & 00001001
  -> Faux ! Y peut pas.
  par contre :  01101001 & 00001001
  -> Vrai ! Y peut.

Si vous vous envisagez d’approfondir le sujet avec Wayland, cette connaissance vous fera gagner beaucoup de temps. En effet les événements que va vous envoyer votre compositeur auront souvent des combinaisons d’états. Si vous cliquez sur votre téléphone pour faire défiler l’écran, votre wl_touch aura l’état DOWN et MOTION par exemple.

Pour terminer ce long blog post, amusons-nous à changer la couleur de la surface au click. Parce que !

void
wl_pointer_button(void *data, struct wl_pointer *wl_pointer, uint32_t serial,
		uint32_t time, uint32_t button, uint32_t state)
{
	struct client_app *app = data;
	fprintf(stderr, "Pointer button !\n");

	cairo_rectangle(app->cairo, 0, 0, app->width, app->height);
	cairo_set_source_rgba(
		app->cairo,
		rand() % 100 / (double)100,
		rand() % 100 / (double)100,
		rand() % 100 / (double)100,
		1
	);
	cairo_fill(app->cairo);

	wl_surface_damage(app->wl_surface, 0, 0, app->width, app->height);
	wl_surface_attach(app->wl_surface, app->wl_buffer, 0, 0);
	wl_surface_commit(app->wl_surface);
}

Remarquons ici notre usage de wl_surface_damage(). Cette méthode permet d’indiquer au compositeur quelle partie de la surface a évolué. Dans vos applications essayez, au moins un petit peu, d’optimiser vos rendus ! Merci, je vous embrasse !

Bien entendu ici on n’a pas du tout fait les choses comme il faut. Ici on redessine la surface à chaque événement que veut bien nous donner le compositeur. Cela ne correspond à rien de concret ou même pratique en termes de frame rate ou même de cycle de vie.

Voilà une bien meilleure façon de gérer le rendu de notre application ! Vous ne serez pas venus ici pour rien !

void
wl_pointer_button(void *data, struct wl_pointer *wl_pointer, uint32_t serial,
		uint32_t time, uint32_t button, uint32_t state)
{
	struct client_app *app = data;
	fprintf(stderr, "Pointer button !\n");

	cairo_rectangle(app->cairo, 0, 0, app->width, app->height);
	cairo_set_source_rgba(
		app->cairo,
		rand() % 100 / (double)100,
		rand() % 100 / (double)100,
		rand() % 100 / (double)100,
		1
	);
	cairo_fill(app->cairo);

	wl_surface_damage(app->wl_surface, 0, 0, app->width, app->height);
}

void redraw_flip(struct client_app *app); // ne faites pas ça, ayez un .h

void
surface_frame_callback(void *data, struct wl_callback *cb, uint32_t time) {
	struct client_app *app = data;
	wl_callback_destroy(cb);

	redraw_flip(app);
}

static struct wl_callback_listener frame_listener = {
    .done = surface_frame_callback
};

void
redraw_flip(struct client_app *app) {
	struct wl_callback *cb = wl_surface_frame(app->wl_surface);
	wl_callback_add_listener(cb, &frame_listener, app);

	wl_surface_attach(app->wl_surface, app->wl_buffer, 0, 0);
	wl_surface_commit(app->wl_surface);
}
@@ -232,6 +253,9 @@ main(int argc, char *argv[])
       xdg_toplevel_set_title(state.xdg_toplevel, "Example client");
       wl_surface_commit(app.wl_surface);

+      wl_display_roundtrip(state.wl_display);
+      redraw_flip(&app);
+
       while (wl_display_dispatch(state.wl_display)) {
               /* This space deliberately left blank */
       }

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.


L’équipe Synbioz.
Libres d’être ensemble.