Linux Embedded

Le blog des technologies libres et embarquées

Créer un compositeur avec QtWayland en QML

Considérons un système relié à un écran d’affichage sur lequel plusieurs applications graphiques sont présentes : lorsqu'une application est lancée, une fenêtre contenant l’interface graphique de l’application s’affiche à l'écran.

Avec Wayland, ce fonctionnement est assuré par un logiciel compositeur.
Cet article aura pour but de décrypter les étapes à effectuer pour développer un compositeur QtWayland en QML. Le module QtWayland étant encore en développement et sujet à modifications, nous aborderons le sujet de manière générique.

Principe général d’un compositeur

A chaque instant T, une image est affichée à l'écran : cette image est la composition des images générées au même instant par chaque application graphique exécutée sur le système.
Chaque application fournit les images de son interface graphique, que nous appellerons écrans, à travers un espace de mémoire tampon qui lui est alloué. Cette mémoire tampon lui est attribuée par le compositeur suite à une requête d'affichage de l’application.
Pour permettre la communication entre le compositeur et les applications à afficher, le compositeur ouvre une socket à laquelle les applications viendront se connecter.
Ainsi, le compositeur assure en réalité 3 fonctions :

  • Serveur d'affichage auquel des applications clientes se connectent pour utiliser les services qu'il met à disposition à travers un protocole de communication qui lui est propre.
  • Compositeur graphique chargé de composer des effets visuels ou de nouvelles images à partir des écrans préalablement stockés dans une mémoire tampon.
  • Gestionnaire de fenêtre chargé de l'affichage et du placement des fenêtres des applications présentes sur un système.

De Wayland à QtWayland

Wayland est un protocole de communication sur lequel se base le compositeur du même nom.
Le framework Qt met à disposition le module QtWayland qui englobe les fonctionnalités du compositeur Wayland et permet de créer un environnement graphique personnalisé, de la gestion des connexions clientes à l'affichage en sortie.
A l'époque où nous nous sommes soumis à l'exercice, le module QtWayland était moyennement stable et la documentation moyennement généreuse. A ce jour, il est encore en développement.

La documentation officielle côté Wayland se trouve ici : https://wayland.freedesktop.org/qt5.html
La documentation officielle côté Qt se trouve là : https://doc-snapshots.qt.io/qt5-dev/qtwaylandcompositor-index.html
et là : https://wiki.qt.io/QtWayland
Le dépôt QtWayland est séparé du dépôt Qt. Pour l'utiliser correctement, il suffit de suivre les instructions du lien ci-dessus.

Une fois les étapes suivies, vous devriez être en mesure de compiler et exécuter le projet exemple de compositeur.

Fonctionnement global d’un compositeur basé sur QtWayland

La version actuelle du module QtWayland (5.9) permet de créer un compositeur en QML pur. Ce n'était pas encore le cas dans la version 5.6.

Quelque soit le mode de développement du compositeur, il y a deux composantes indispensables à gérer :

  • la composante serveur qui accueille les connexions clientes et émet les signaux de gestion des clients et de leurs ressources
  • la composante graphique qui gère l'affichage et la composition des écrans en une image finale

En terme d'objets Qt, si les noms de classes peuvent sensiblement varier avec les versions, les étapes de fonctionnement restent globalement les mêmes:

  1. A sa création, un objet compositeur (WaylandCompositor) crée une socket, dont le nom peut être précisé dans l'attribut socketName et l'emplacement dans la variable d'environnement XDG_RUNTIME_DIR, et spécifie son périmètre d'affichage (ex: dimensions de l'écran de sortie du système). Par défaut, une socket Wayland-0 est créée dans le répertoire /tmp.
  2. Une application cliente qui doit être affichée en sortie se connecte via la socket ouverte et lance la création d'un objet de type surface (WaylandSurface). La surface servira à gérer une mémoire tampon contenant les écrans de l'application cliente. Lorsque la surface est créée, la méthode ou le signal surfaceCreated() de l'objet compositeur est appelé. La redéfinition de cette méthode ou la connexion du signal à un slot permet de prendre la main sur la surface créée.
  3. Lorsque les écrans de l'application sont effectivement chargés dans la mémoire tampon, la surface est dite mapped. Selon les versions du module, l'API mettra a disposition un signal (v5.7: mapped(), v5.9: mappedChanged() ) ou un booléen (v5.9: isMapped() ) pour notifier de cet état ou y accéder.
  4. Les écrans de l'application cliente chargés dans la mémoire tampon sont accessibles à travers des objets de type vue (WaylandView). Une vue est une représentation de l'écran géré par la surface pour un objet de type sortie (WaylandOutput). Un objet de type sortie correspond à une zone d'affichage à l'intérieur du périmètre d'affichage du compositeur. Une surface peut donc être associée à plusieurs vues si le système possède plusieurs objets de type sortie. La liste de ces vues est accessible par l'attribut views de la surface. Autrement dit, une vue correspond à l’image affichable de l’application avant qu’elle ne soit intégrée à l’image composée finale.
  5. Le compositeur aura préalablement défini des règles d'affichage (position, aspect des fenêtres d'affichage) grâce à des objets graphiques auxquels les vues de l'application cliente pourront être intégrées.

schéma récapitulatif architecture Wayland

Développement

A présent, voici un exemple de développement basé sur la version 5.7 du module QtWayland.
A cette époque, seule la composante graphique pouvait être développée en QML. La composante serveur a donc été assurée par un objet C++ héritant de la classe QWaylandQuickCompositor, ainsi que de la classe QQuickView pour embarquer les objets QML de l'interface.

Ce compositeur simple se contente de détecter les connexions d’applications clientes et de les afficher telles quelles dans le coin haut gauche de la sortie d’affichage.

Coté C++

On commence par créer une classe que nous appellerons Compositeur, qui hérite de QWaylandQuickCompositor et QquickView et dont nous détaillerons les membres plus bas.
Compositeur.h

 class Compositeur : public QQuickView, public QWaylandQuickCompositor
 {
         Q_OBJECT
 public:
     /*!
     * \brief constructor
     */
     Compositeur ( );
     /*!
     * \brief Gives QML access to the item of the surface
     * \param xkpSurf Surface to get the item from
     * \return item of the corresponding surface
     */
     Q_INVOKABLE QWaylandSurfaceItem *item ( QWaylandSurface* xkpSurf ) const;
 signals:
     /*!
     * \brief Signal to notify that a client window was connected
     * \param xWindow Client window object
     */
     void clientAdded ( QVariant xWindow );
 private slots:
     /*!
     * \brief Slot to handle client connections
     */
     void surfaceMapped();
     /*!
     * \brief Slot that calls sendFrameCallbacks function from qwaylandcompositor
     */
     void sendCallbacks();
 protected:
     /*!
     * \brief Called when a client has connected
     * \param xkpSurface Surface of the connected client
     */
     void surfaceCreated ( QWaylandSurface * const xkpSurface );

Du côté du constructeur de la classe, les étapes de l’initialisation du compositeur sont explicitées dans les commentaires du code :

ihm::Compositeur::Compositeur(): QQuickView(), QWaylandQuickCompositor( NULL, DefaultExtensions | SubSurfaceExtension )
 {
     //on indique au compositeur le chemin vers le fichier QML principal
     setSource ( QUrl( "main_compositeur.qml" ) );
     //on lui indique ses dimensions:
     //il aura les même que celle de l'objet QML principal
     setResizeMode ( QQuickView::SizeRootObjectToView );
     setColor ( Qt::black );
     //on lui indique que l'apparence de ses fenêtres sera celle utilisée
     //par défaut par le système
     addDefaultShell ( );
     //on crée un objet de type sortie sur laquelle afficher l'image finale
     if( QWaylandQuickCompositor::createOutput( this, "", "" ) == NULL )
     {
         qWarning("failed to create compositor output ");
     }
     //et on crée la connexion qui permettra de synchroniser l'affichage
     //et la gestion des mémoires tampon
     bool isConnected = static_cast<bool>( connect(this, SIGNAL( afterRendering ( ) ), this, SLOT ( sendCallbacks( ) ) ) );
     if(!isConnected)
     {
         qWarning("failed to connect afterRendering to sendCallbacks.\n"
         "this will cause client display issues");
     }
     ...
 }

Le signal afterRendering() est émis par le QquickView lorsqu'une image est construite et prête à être affichée.
Le slot sendCallbacks appelle la méthode sendFrameCallbacks() de QWaylandQuickCompositor qui supprime la première image de la mémoire tampon.
Ainsi, à chaque fois qu'une image est affichée en sortie, elle est supprimée de la file de la mémoire tampon pour laisser place à la prochaine.

Communication QML-C++

La partie C++, qui gère les connexions, doit pouvoir communiquer avec la partie QML, qui gère l'aspect graphique.
Premièrement, la partie QML du code doit être en mesure de récupérer la vue de l'application cliente pour pouvoir l'intégrer à l'image finale.
Pour cela, on crée une propriété dans le contexte du QML qui correspondra à l'objet C++ du compositeur :

rootContext ( ) -> setContextProperty ( "compositor", this );

Ainsi, tous les membres de la classe Compositeur déclarés comme Q_INVOCABLE seront accessibles depuis le code QML en utilisant le mot “compositor”.

On déclare et l'on définit une méthode item qui renvoie la première vue de la surface créée par l'application cliente (cf. Compositeur.h) à laquelle on accèdera comme ci-dessus en QML. La première vue est en réalité la seule, puisque nous n’avons qu’une seule sortie:

compositor.item

Ensuite, lorsqu'une application demande à être affichée, le QML doit être notifié de manière à prendre la main sur l'écran en mémoire.
Pour cela, on redéfinit la méthode surfaceCreated. Son rôle est de connecter le signal mapped de la surface créée au slot surfaceMapped qui commencera le traitement de l’écran :

void ihm::CGestionnaireIHM::surfaceCreated ( QWaylandSurface * const xkpSurface )
 {
     bool isConnected = static_cast<bool> ( connect ( xkpSurface, SIGNAL( mapped ( ) ), this, SLOT ( surfaceMapped ( ) ) ) );
     if ( !isConnected )
     {
         qWarning("failed to connect mapped signal to surfaceMapped slot.");
     }
 }

Sur réception du signal mapped de la surface créée par l'application cliente. Le slot de gestion de ce signal émet le signal clientAdded() avec la dite surface en argument.

void ihm::CGestionnaireIHM::surfaceMapped()
 {
     QWaylandQuickSurface *surface = qobject_cast<QWaylandQuickSurface *>( sender ( ) );
     emit clientAdded ( QVariant::fromValue ( surface ));
 }

Idéalement dans le constructeur, le signal clientAdded est connecté à une fonction QML du même nom défini dans le fichier QML principal, ce qui achève de lier les évènements c++ au QML :

QObject::connect ( this , SIGNAL ( clientAdded ( QVariant ) ) , rootObject() , SLOT ( clientAdded ( QVariant ) ) );

Notons que toutes les propriétés du QML principal sont accessibles en C++ grâce à la propriété rootObject() du QQuickView.

La partie C++ en résumé:

sch2 blog owi

Côté QML

La partie QML assure la fonction de gestionnaire de fenêtres : aspect graphique, affichage, désaffichage, etc. Elle est construite autour de deux fichiers indispensables :

  • le fichier principal main.qml qui d'une part, décrit le contenu graphique de l'interface (fond d'écran, barre de navigation et autres éléments indépendants des applications clientes) et d'autre part, défini les fonctions de composition de l'image finale.
  • le fichier windowContainer.qml qui décrit le contenu et comportement graphique de la fenêtre dans laquelle seront affichés les écrans des applications clientes.

Ci-dessous un exemple de contenu possible pour un composant windowContainer. Ici, le contenant est un Item sans particularité graphique avec des propriétés de position et de dimensions, une propriété child qui correspondra à l'écran à intégrer à la fenêtre.

 import QtQuick 2.0
 import QtQuick.Window 2.0
 import QtCompositor 1.0
 Item {
     id: container
     x: targetX
     y: targetY
     z: targetZ
     width: targetWidth
     height: targetHeight
     scale: targetScale
     property real targetX :0
     property real targetY :0
     property real targetZ :10
     property real targetWidth
     property real targetHeight
     property real targetScale: 1
     property variant child: null
 }

Rappelons-nous, la notification de connexion d'une application cliente est enfin arrivée jusqu'au QML par la fonction clientAdded() avec en paramètre une donnée contenant l'écran de l'application à afficher.
Il s'agit à présent de créer une fenêtre et d'y intégrer l'écran de l'application ce qui correspond à l'ajout du code suivant au fichier principal :

function clientAdded(window){
     //On commence par créer le composant fenêtre.
     var windowContainerComponent = Qt.createComponent("WindowContainer.qml");
     var windowContainer = windowContainerComponent.createObject(main)
    
     //On établit la vue de l'application comme élément enfant de la fenêtre. La vue de l'application est accessible grâce à la méthode item de la classe compositeur.
     windowContainer.child = compositor.item(window);
     windowContainer.child.parent = windowContainer;

    //On configure les propriétés de l'élément comme on le souhaite en modifiant les propriétés target. Ici on attribue au composant fenêtre les mêmes dimensions que celles de la vue à afficher, ainsi que sa visibilité.
    windowContainer.child.touchEventsEnabled = true;
    windowContainer.targetWidth = window.size.width;
    windowContainer.targetHeight = window.size.height;
    windowContainer.visible = visible
 }

Bon courage !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.