À propos de Wayland, X11, DWM et Quartz

Dans les années 1980, des chercheurs du Xerox PARC (le centre de recherche de la firme en Californie) inventent un concept aujourd'hui si banal qu'on en oublie à quel point il révolutionna l'informatique.

Cette idée géniale porte le nom de "métaphore de bureau", et consiste à représenter le fonctionnement de l'ordinateur de manière graphique, en le rapprochant d'un environnement de travail physique. Enterrant la vénérable ligne de commande, le "bureau" permet ainsi d'effectuer des opérations simples comme trier des fichiers dans des dossiers, les jetter dans la corbeille, etc.

Un jour, Steve Jobs rend visite aux chercheurs de Xerox, et perçoit tout de suite le potentiel du concept. Deux inventions en particulier suscitent son intérêt : l'interface graphique et la souris. Ces éléments vont inspirer le premier bureau made in Apple, qui consiste en un système multi-tâches novateur, où les applications sont contenues dans des fenêtres manipulables à la souris.

Plus tard, Bill Gates rendra visite aux employés de Apple, et s'en trouvera lui aussi très inspiré pour la mise au point du premier Windows. Ces différentes visions de l'informatique moderne reposent sur un composant logiciel central, dont la nature réelle varie d'une plateforme à une autre mais que nous pouvons appeller de manière générale un "système d'affichage". Ils sont aujourd'hui incontournables pour réaliser un programme graphique : l'ère des systèmes mono-tâche comme DOS est révolue.

Des bibliothèques graphiques vont alors faire leur apparition, avec pour mission de faciliter la création d'applications tout en assurant une certaine portabilité entre les différents systèmes d'affichage. GTK, Qt et WxWidgets sont des exemples de tels outils. Ils se concentrent généralement sur le contenu de l'application, et n'exposent donc pas le fonctionnement du système d'affichage sous-jacent.

Cet article sera l'occasion de découvrir les principaux systèmes d'affichage disponibles sous Linux, Windows et OS X. Nous en profiterons pour souligner leurs différences et donner une idée des défis relevés par les abstractions décrites ci-dessus.

X11

Présentation

X est le système d'affichage historique des systèmes d'exploitation Unix. Il est très marqué par le design pattern client/serveur :

Schéma simplifié des liens du protocole X11 dans le système et l'application

C'est d'ici que vient le célèbre X11, protocole de communication encadrant les échanges entre serveur graphique et applications clientes.

Implémentations

Serveur

Peut-être avez-vous entendu parler de X.org. Il s'agit tout simplement de l'implémentation de référence d'un serveur acceptant le protocole X11.

Clients

Deux bibliothèques bas-niveau permettent de concevoir des clients, elles aussi développées par le projet à l'origine du protocole X11 et du serveur X.org :

Schéma simplifié des liens du protocole X11 avec plus de détail sur l'application

Freins à l'adoption de XCB

Tout d'abord, XCB est moins bien documentée que Xlib, dans les commentaires accompagnant son implémentation comme dans la littérature spécialisée. Les bibliothèques graphiques de plus haut niveau (SDL, GTK, etc.) n'ont donc pas opéré de transition, or ce sont elles qui servent les plus souvent à la création d'applications graphiques.

De plus, XCB ne permet pas l'utilisation d'OpenGL. Le lien entre le contexte OpenGL et le système d'affichage X est assuré par la bibliothèque GLX. Curieusement, sa standardisation ne s'appuie pas sur celle du protocole X11, mais sur les fonctions disponibles dans libX11. Il est donc impossible d'utiliser OpenGL dans une application X sans faire appel à libX11.

Schéma détaillé des blocs logiciels composant une application OpenGL X11

Il est toutefois possible de limiter l'utilisation de cette dernière à la seule initialisation du contexte OpenGL et d'utiliser XCB pour tout le reste.

Les spécificités de X11

X profite avantageusement de l'architecture "client-serveur" pour rendre possible le partage de fenêtres par le réseau (X-forwarding). Il s'agit d'une fonctionnalité historique très importante dans la conception du protocole.

Schéma simplifié des liens du protocole X11 à travers une connexion réseau

Elle permet par exemple de se connecter à une machine où s'exécute l'application tandis que les requêtes X11 sont transmises à un serveur X local pour gérer son seul affichage.

Gestion de la mémoire

Mémoire séparée

Par défaut l'espace de mémoire stockant le contenu des fenêtres n'est accessible que du serveur. Comme cela a pour effet de multiplier inutilement les échanges de données, une autre technique a été développée.

Schéma simplifié des échanges de données lors de l'utilisation de buffers séparés

Mémoire partagée

Partager l'espace mémoire utilisé pour faire le rendu de la fenêtre est en effet possible dans certains cas, mais il incombe au programmeur de le vérifier.

Schéma simplifié des échanges de données lors de l'utilisation d'un buffer partagé

Comme la chose n'est pas encadrée par le protocole X11, certaines subtilités peuvent mettre en péril la robustesse de l'opération. Dans le cadre du X-forwarding en particulier, il n'est pas possible de mettre en place ce mécanisme. Il faut donc être en mesure de détecter cela pour, le cas échéant, gérer la mémoire de manière traditionnelle.

Gestion des évènements

XCB propose ses propres outils de gestion des évènements. Ils permettent bien sûr de réagir aux manipulations des fenêtres, mais aussi aux entrées effectuées au clavier et à la souris.

Pour créer une boucle d'évènements, XCB propose deux options :

la fonction bloquante xcb_wait_for_event, qui aura pour effet d'interrompre l'exécution du programme jusqu'à la réception d'un évènement la fonction non-bloquante xcb_poll_for_event, qui permet de traiter la file d'évènements sans toutefois interrompre le programme si elle est vide Cette dernière option s'incrit dans la logique de performance de XCB, qui permet un accès asynchrone au protocole X. Il s'agit d'un outil intéressant car il est possible de l'utiliser pour mettre en place une boucle d'évènements générique.

D'ordinaire, Les évènements sont en effet collectés par des bibliothèques comme epoll, qui scrutent des descripteurs de fichiers à la recherche d'un signal. Pour utiliser un tel mécanisme avec X, il suffit de renseigner le descripteur de fichier du contexte XCB dans epoll.

Lors de la réception d'un évènement X, il faut alors exécuter la fonction non-blocante et traiter la file.

Mise à jour des régions exposées

Les évènements d'exposition permettent de mettre à jour une partie de la fenêtre sans transférer la totalité de l'image. Mal réagir aux évènements d'exposition peut créer des erreurs d'affichage : les rectangles de couleur unie obstruant parfois les éléments d'interface en font partie.

Transition vers Wayland

Nous nous sommes concentrés jusqu'à présent sur les points critiques de l'intégration du système d'affichage X. Cependant, le protocole X11 et le serveur X.org sont aujourd'hui vivement critiqués, et un nouveau système d'affichage prend progressivement son envol. Voici quelques raisons pouvant expliquer ce changement.

Un bouleversement inattendu

Quand l'écosystème X est conçu au début des années 80, l'accélération matérielle est loin d'être accessible au commun des mortels. Lorsque les premiers GPU commerciaux apparaissent, il faut donc adapter des années de travail sur le serveur X avant d'obtenir une quelconque compatibilité Unix.

Ce bouleversement eut clairement un impact négatif sur l'architecture du système d'affichage, et l'ensemble de ses contributeurs a fini par s'accorder sur l'urgence d'en développer un nouveau, plus moderne.

Des cas d'utilisation obsolètes

Le partage par réseau (X-forwarding) a été déterminant dans la conception du protocole X, or cette fonctionnalité est clairement obsolète, et n'est plus aujourd'hui qu'une contrainte limitant la modernisation du projet.

Une conception monolithique critiquée

Le serveur X.org a aussi la caractéristique d'être pronfondément monolithique. Nous avons vu par exemple qu'il intégrait un mécanisme de gestion d'évènements mais il gère de surcroît les entrées utilisateur au clavier et à la souris. Une autre approche était nécessaire, la plus convaincante étant Wayland.

Wayland

Présentation

Les systèmes d'affichage se reposent aujourd'hui sur l'accélération matérielle offerte par les GPU, en utilisant les API disponibles. Wayland aurait donc pu être conçu pour utiliser OpenGL, mais tel n'est pas le cas.

En effet, une subtilité de conception des drivers OpenGL pour Linux fait qu'ils incluent des références à GLX, la bibliothèque faisant le lien avec X11. Comme nous l'avons vu, GLX n'est cependant pas définie directement en termes de requêtes X11, son standard s'appuyant sur la bilbiothèque abstraite libX11.

nullgemm@au486 ~ > ldd /usr/lib/libGL.so
	linux-vdso.so.1
	libGLdispatch.so.0 => /usr/lib/libGLdispatch.so.0
	libGLX.so.0 => /usr/lib/libGLX.so.0
	[...]
nullgemm@au486 ~ > ldd /usr/lib/libGLX.so.0
	linux-vdso.so.1
	libGLdispatch.so.0 => /usr/lib/libGLdispatch.so.0
	libdl.so.2 => /usr/lib/libdl.so.2
	libX11.so.6 => /usr/lib/libX11.so.6
	[...]

En réalité, les contraintes d'utilisation d'OpenGL ne se limitent donc pas à l'obligation de passer par libX11 dans le cadre d'une application X : c'est bien l'ensemble des programmes OpenGL fonctionnant sous Linux qui est condamné à dépendre de ce système d'affichage, ce qui serait un comble pour Wayland qui se veut moderne et en rupture totale avec X11 !

Schéma détaillé des liens inaltérables entre libGL, GLX et libX11

Pour pallier ce type de problème et séparer les pilotes graphiques des systèmes d'affichage, le consortium Khronos a développé le standard EGL. Utilisé astucieusement au sein de la jungle graphique de Linux, il a permis à Wayland d'accéder à une autre API (OpenGL ES) tout en conservant une totale indépendance vis-à-vis de X.org.

L'utilisation d'OpenGL reste bien sûr possible depuis une application Wayland, cela a simplement pour effet de la faire dépendre de X.

Schéma détaillé des blocs logiciels composant une application OpenGL Wayland

À noter qu'il est théoriquement possible pour un serveur Wayland de se reposer sur Vulkan, même s'il n'en existe pas encore de démonstration à ce jour.

Gestion des évènements

Wayland est en opposition totale à X concernant la gestion des évèvements. En effet, X n'empêche absolument pas les clients de capter la moindre entrée et laisse donc le champ libre aux keyloggers et autres programmes malveillants. Pour les contrer, le projet Wayland a initié libinput, une bibliothèque de gestion des périphériques d'entrée isolant les informations au mieux.

Cependant Wayland rejoint X en ce qu'il propose son propre système d'évènements permettant de gérer les actions liées aux fenêtres et intégrant libinput pour donner accès aux périphériques d'entrée.

Un accès externe à ce système via un descripteur de fichier est toutefois prévu et permet de l'utiliser uniquement pour les évènements liés aux fenêtres. Comme libinput peut tout à fait être utilisée indépendemment, il est possible de mettre en place le même fonctionnement générique que pour X (avec epoll).

Cas particulier du frame callback

Le "frame callback" fait partie des fonctionnalités ajoutées à Wayland mais non présentes dans X. Il s'agit de permettre au compositeur lui-même d'exécuter une fonction "callback" lorsque l'écran est prêt à être rafraîchi. Ce mécanisme est un excellent moyen de limiter la charge CPU en ne produisant que des images qui seront affichées (le callback n'est appellé que si la fenêtre est visible et si le rafraichissement de l'écran est terminé).

Cas particulier de la manipulation logicielle des fenêtres

Contrairement à X11, qui permet un contrôle total des fenêtres par le programme, le protocole Wayland principal ne prévoit pas une grande liberté sur ce point. Il n'est pas possible, par exemple, de redimmensionner ou déplacer les fenêtres de manière logicielle.

Cas particulier de la minimisation

Pour changer l'état des fenêtres (maximisé/plein écran/normal) Wayland expose des fonctions très similaires à celles de XCB. La différence se situe donc au niveau de la minimisation ; étrangement, Wayland prévoit une fonction dédiée (contrairement à XCB) mais pas sa contrepartie qui permettrait de faire ré-apparaître la fenêtre. Il faut donc ruser et supprimer les surfaces pour les recréer aussitôt. Comme l'ensemble des compositeurs classiques affiche les fenêtre lors de leur création, cela permet d'outrepasser les limites du protocole Wayland. À noter qu'il est parfaitement possible de conserver le buffer graphique, une réinitialisation n'est donc pas nécessaire et le processus reste rapide.

Quartz

Présentation

Sous OS X, la création de fenêtres se fait en général via l'API Cocoa. Il s'agit en fait d'une abstraction reposant sur trois bibliothèques :

D'emblée, il est évident qu'une bibliothèque de fenêtrage bas-niveau n'a pas de sens au sein de l'écosystème Apple : le langage Objective-C a même été conçu spécialement pour cet environnement où tout est manipulé par abstraction.

Apple exploitant au maximum la métaphore objet, il en résulte un grand avantage fonctionnel pour l'utilisateur. Cependant, la contrainte de développement est à la mesure de l'expérience quand il s'agit de proposer différentes versions d'un programme, pour plusieurs plateformes.

Au lieu d'ajouter une couche d'abstraction aux éléments d'interface natifs nous allons cependant tenter de communiquer le plus directement possible avec Quartz, le système d'affichage en présence.

nullgemm@au486 ~/code/globox > otool -L bin/globox
globox:
	/System/Library/Frameworks/[...]/AppKit
	/usr/lib/libSystem.B.dylib
	/System/Library/Frameworks/[...]/CoreFoundation
	/System/Library/Frameworks/[...]/CoreGraphics
	/usr/lib/libobjc.A.dylib

En listant les liens dynamiques de l'exemple d'implémentation dans Globox, nous pouvons confirmer que l'exécutable ne dépend que de AppKit, Core Foundation, Core Graphics et du runtime Objective-C.

Schéma de l'architecture choisie pour Globox afin de contourner les limites d'Apple

Ci-dessus l'architecture simplifiée de l'implémentation de Globox pour Quartz. La fonction d'initialisation principale ne crée que NSApplication et NSApplicationDelegate. Les classes suivantes sont gérées depuis un callback de NSApplicationDelegate nommé applicationDidFinishLaunching.

Lasagnes

Ainsi, le système d'affichage lui-même n'est pas accessible directement. Il est nécessaire de passer par un accesseur abstrait (appellé "framework" par Apple), et qui permet de caractériser une application.

Plusieurs de ces interfaces existent, et permettent de définir différents types de contenu : les fenêtres sont un effet de bord de cette caractérisation. Comme nous l'avons vu, Cocoa (le plus utilisé) cache en réalité AppKit, qui est le véritable "framework". Comme il s'agit également d'un composant historique d'OS X, AppKit est le plus bas-niveau et le plus rétro-compatible, c'est donc celui qui nous intéresse.

AppKit

L'écriture directe de pixels est possible dans le cadre d'un objet "rectangle", défini par Apple mais supportant de nombreux modes d'affichage. Une fenêtre bas-niveau sous OS X est donc la conséquence de la définition d'une application Objective-C contenant un rectangle.

Schéma simplifié des échanges de données lors de la copie du buffer via drawRect

Malgré son élégance, ce système est très restrictif : le pointeur permettant la modification directe de la mémoire n'est valide qu'au sein d'un callback graphique similaire à celui présent sous Wayland. Il est donc obligatoire de copier les données dans la fenêtre au sein de cette fonction.

Schéma simplifié des échanges de données lors de la copie du buffer avec cache GPU

Il est intéressant de noter qu'Apple a intégré un système de cache GPU visant à accélérer le rendu des fenêtres. Il est accessible via une abstraction, sous la forme de "calques". Le callback permettant de vraiment mettre à jour la fenêtre n'est alors plus le même.

Gestion des évènements

Sous OS X comme sous BSD, le noyau gère les évènements non pas via epoll mais avec kqueue. Il n'est cependant pas possible de récupérer les évènements liés aux fenêtres sans passer par les fonctions Objective-C dédiées. Il est intéressant de voir que sous Wayland, la fonction wl_display_dispatch doit être appellée avant de traiter la file d'évènements. Dans une bibliothèque graphique, une fonction commune pourrait donc servir à son exécution pour Wayland et à la récupération des évènements graphiques pour Quartz.

Win32

Présentation

Sous Windows, le développement d'application fait généralement intervenir l'API propriétaire Win32. Tout comme Cocoa, Win32 repose sur une certaine abstraction (son fonctionnement interne est par contre nettement moins évident).

Au fil des décennies, Microsoft a développé plusieurs mécanismes d'affichage. GDI (l'un des plus anciens) se présente sous la forme d'une bibliothèque. Un de ses objectifs est d'abstraire la gestion des modes d'affichage. Pour cela, elle permet de sélectionner un format d'image différent de celui du matériel et fait alors une conversion à la volée. Une telle chose n'est pas possible directement avec Quartz ou dans les solutions d'affichage existant sous Linux.

Pour moderniser l'environnement Windows, Microsoft a développé par la suite les bibliothèques GDI+ et Direct2D, supposées remplacer GDI. Plus orientée vers le dessin, GDI+ rend moins évident l'accès direct à la mémoire contenant les images affichées. Direct2D affirme cette nouvelle orientation en supportant le rendu sur GPU de manière transparente. Nous nous intéresserons donc à GDI, qui expose mieux le fonctionnement interne du système d'affichage.

Mise en place de l'accès mémoire direct

Pour écrire directement dans la mémoire utilisée par GDI, il faut d'abord créer un Display-Independant Bitmap, le seul type d'image accessible par pointeur en vue d'une écriture externe.

Il est nécessaire de la créer depuis un Display Context réellement compatible avec le matériel de l'utilisateur, afin de permettre la conversion automatique si elle est nécessaire.

L'allocation de la mémoire est donc totalement abstraite sous Windows : il nous suffit d'appeler CreateCompatibleDC puis CreateDIBSection. Pour transmettre l'image à la fenêtre nous prenons un Display Context optimisé via la fonction BeginPaint, puis utilisons la célèbre fonction BitBlt.

La délectable anecdote historique

Les premières versions de GDI, qui accompagnaient un Windows encore balbutiant, ne permettaient pas cet accès direct à la mémoire. Pour y remédier et assurer l'indispensable portabilité des jeux DOS les plus demandeurs, Chris Hecker (alors employé chez Microsoft) écrit WinG.

En plus d'amener la création de DirectX son travail permet de porter Doom sur Windows 95. Le responsable de ce projet se trouve être Gabe Newell, assisté des autres fondateurs de Valve. La fonction CreateDIBSection remplacera finalement WinG sous Windows 98.

Les évènements made in Redmond

l'API Win32 défini un mécanisme de gestion des évènements logiquement différent des techniques traditionnellement employées dans les systèmes Unix. Baptisés Messages, les évènements sont accessibles de manière blocante et non-blocante, via les fonctions GetMessage et PeekMessage.

Comme pour Wayland, la distribution des évènements est limitée pour des raisons de sécurité, mais il est possible d'obtenir un accès global par d'autres canaux (pour communiquer directement avec le matériel, limiter les latences...).

Cette deuxième méthode étant décorrélée de la gestion des fenêtres nous ne nous intéresserons qu'au système traditionnel. Dans ce cas, une sorte de descripteur de fichier appelé HANDLE est nécessaire. Il s'agit en réalité d'un void*, à l'usage des fonctions mentionnées plus haut.

Comportement non spécifié

Contrairement à de nombreuses affirmations de la documentation de Microsoft, certaines opérations ne génèrent pas les erreurs prévues : il faut donc être vigilant, un programme cache parfois des incompatibilités à retardement alors qu'il semble parfaitement fonctionnel au premier abord. La gestion des évènements en particulier est très permissive, mais provoque des divergences importantes entre Wine et Windows si manipulée sans précautions.