I. L'article original▲
Le site Qt Labs permet aux développeurs de Qt de présenter les projets, plus ou moins avancés, sur lesquels ils travaillent.
Nokia, Qt, Qt Labs et leurs logos sont des marques déposées de Nokia Corporation en Finlande et/ou dans les autres pays. Les autres marques déposées sont détenues par leurs propriétaires respectifs.
Cet article est la traduction de l'article Qt Graphics and Performance - An Overview de Gunnar paru dans Qt Labs.
II. Introduction▲
Pour cette série blog que j'écris, le mieux est que je commence par une vue d'ensemble de ce que sont les painters, les pixmaps, les widgets, les graphicsview et le backingstore.
Au centre de tous les Qt Graphics, on retrouve la classe QPainter. Elle permet de faire le rendu dans une surface, grâce à la classe QPaintDevice. Des exemples de surface de dessin sont les classes QImage, QPixmap et QWidget. La façon dont cela fonctionne est que pour une implémentation donnée de QPaintDevice, nous récupérons un moteur de rendu personnalisé qui prend en charge le rendu sur cette surface. Tout cela est expliqué dans notre documentation « ainsi peut-être pas trop intéressant ».
Regardons cela plus en détail.
III. QWidgets et QWindowSurface▲
Même si QWidget est une classe héritant de QPaintDevice, on ne réalisera jamais le rendu directement dans une surface de QWidget. Au lieu de cela, au moment où l’on appelle une fonction paintEvent, le rendu est redirigé vers une surface non visible qui est représentée par la classe interne QWindowSurface. Cela a été traditionnellement implémenté en utilisant la fonction QPainter::setRedirected(), mais cela a depuis été remplacé par un mécanisme interne entre QPainter et QWidget qui est légèrement meilleur.
Parfois, nous appelons cette surface le backingstore, mais, en réalité, ce n'est qu'une surface 2D. Si vous avez déjà regardé dans le code source de Qt et avez trouvé la classe QWidgetBackingStore, vous avez du voir que celle-ci est responsable de déterminer quelles sont les parties de la surface de la fenêtre qui doivent être mises à jour avant que la fenêtre soit affichée à l'écran, ce qui fait de QWidgetBackingStore un véritable gestionnaire de mise à jour du rendu. Lorsque le concept de backingstore a été introduit dans Qt 4.1, les deux classes (NDT : les classes QWindowSurface et QWidgetBackingStore) étaient les mêmes, mais l'introduction de plusieurs méthodes pour effectuer le rendu à l'écran nous a fait les diviser en deux.
Dans les premiers temps, le rendu des widgets était effectué directement à l'écran. Bien que l'option pour dessiner directement à l'écran soit toujours disponible, il n'est pas recommandé de l'utiliser. Je crois que le seul système qui prend en charge à distance (« remotely supports »), c'est X11, mais il est plus ou moins testé et cause donc souvent des artefacts dans les styles les plus complexes. Ajouter l'option Qt::WA_PaintOnScree permet au gestionnaire de rendu interne de Qt d'ignorer ce widget au moment de réaliser le rendu dans la QWindowSurface et d'envoyer à la place un paintEvent particulier seulement à ce widget. Avant Qt 4.5, il y avait un gain de vitesse significatif quand 10 à 100 widgets étaient mis à jour, mais dans Qt 4.5, le gestionnaire de rendu a été optimisé pour mieux gérer cela ; ainsi, réaliser le rendu directement à l'écran est généralement pire que de le réaliser en mémoire.
Retour à la surface de la fenêtre. Tous les widgets sont combinés dans la surface de la fenêtre de haut en bas (NDT : le haut fait référence au top-widget, c'est à dire au widget parent de tous les autres et le bas aux bottom-widgets, qui sont contenus dans un widget parent). top-widget est utilisé pour remplir la surface avec son fond ou avec un fond transparent si l'attribut Qt::WA_TranslucentBackground est fixé. Tous les autres widgets sont considérés comme transparents. Un QLabel affiche seulement un peu de texte, mais ne touche à rien d'autre. Qu'est-ce que cela signifie pour le gestionnaire de rendu ? C'est que chaque widget qui chevauche l'étiquette, mais en arrière d'elle, doit être dessiné avant. Si pour une application, vous savez que certains widgets sont opaques et qu'ils redessineront chaque pixel pour chaque paintEvent, on pourra définir l'attribut Qt::WA_OpaquePaintEvent, ce qui permet au gestionnaire de rendu d'exclure les régions des widgets en arrière-plan qui seront redessinées par le widget.
Comme tous les widgets sont redessinés sur la même surface, nous devons faire en sorte qu’ils ne soient pas redessinés accidentellement en dehors de leurs propres limites et dans d'autres widgets. Comme il n'y a aucune garantie que les widgets seront dessinés à l'intérieur de leurs limites, ce qui pourrait potentiellement conduire à des artefacts de rendu, nous avons créé un découpage interne au QPainter appelé le système de découpe (« system clip »). Pour la plupart des widgets, le système de découpe est un rectangle : en regardant la section sur la performance dans la documentation de QPainter, nous voyons que ce n'est pas si mal. Le découpage en rectangle, avec des pixels alignés, est rapide. D’autre part, un widget utilisant un masque est un désastre en matière de performance. Il est plus lent à mettre en place et le rendu est plus lent. Le système de découpage utilise le même découpage qui est passé au paintEvent, sauf que le découpage dans le paintEvent été déplacé pour correspondre à la partie supérieure gauche du widget plutôt qu'en haut à gauche de la surface. Il ne fait pas attribuer la région du paintEvent comme étant un découpage du rendu. Elle a déjà été définie et nous ne détectons pas que c'est exactement la même région et il suffit de recommencer entièrement. Le but de la région et du rectangle dans le paintEvent est alors que les widgets peuvent décider de ne pas redessiner certaines parties. Ceci est particulièrement utile lorsque vous avez beaucoup d'éléments dans un widget permettant de visualiser une scène ou quelque chose de similaire, par exemple dans une application de cartographie.
En plus du système de découpe qui est utilisé avant l'appel au paintEvent, le système de rendu doit également être dans un état propre, ce qui signifie la mise en place des brosses, des stylos, des polices et autres. Le coût n'est pas énorme, mais, si vous avez de nombreux widgets, il se cumule. Ainsi, bien que les widgets ne soient pas des fenêtres natives, il y a quand même un coût pour les redessiner. Soyez conscient de cela quand vous concevez votre application. Par exemple, implémenter une galerie de photos à l'aide QLabel avec des pixmaps dans un QScrollArea ne permet pas de les dimensionner. Vous devez mettre en place le découpage et tous les autres paramètres pour chaque QLabel, même si le QLabel affiche seulement une pixmap. Un widget unique pour la visualisation pourrait redimensionner plus efficacement, puisque le widget peut alors implémenter une petite boucle qui redessine les pixmaps au bon endroit.
Ce système de backingstore et de surface de fenêtre n’est valable sur Mac OS X que lorsque les systèmes graphiques Raster ou OpenGL sont utilisés. Je recommande fortement d'utiliser Raster, il implémente l'ensemble des fonctionnalités, il est souvent plus rapide avec le même profil de performances que Qt sur Windows et la correction des bugs de rendu est une priorité plus élevée pour le Raster que pour le moteur CoreGraphics. Nous prévoyons d'utiliser le système Raster par défaut sur Max OS X, il suffit de régler quelques problèmes d'intégration du système de fenêtres.
IV. Les systèmes graphiques▲
Le concept de système graphique a été introduit dans Qt 4.5. L'idée est de pouvoir choisir au démarrage, au niveau application, le type de rendu que vous souhaitez. Le système graphique est chargé de dessiner sur les pixmaps en arrière-plan et sur la surface de la fenêtre. Nous avons actuellement des systèmes graphiques pour les systèmes Raster, OpenGL 1.x, OpenGL/ES 2.0, OpenVG et X11. Vous pouvez sélectionner le système graphique au démarrage de l'application avec l'option en ligne de commande -graphicssystem raster|opengl|opengl1|x11|native, où « native » correspond au système par défaut. Une autre possibilité consiste à ajouter la même option dans le fichier configure, ce qui fixera cette option pour toutes les applications utilisant Qt. Enfin, il y a la fonction QApplication::setGraphicsSystem() qui permet de sélectionner le système graphique directement dans le code pour une application donnée.
Dans les prochains blogs, nous irons plus loin dans la description des moteurs de rendu, mais, pour l'instant, regardons simplement les points importants.
IV-A. Le système Raster▲
Le système Raster est l'implémentation de référence de QPainter. Il implémente toutes les fonctionnalités que nous avons définies et fait tout de manière logicielle. Quand le portage vers une nouvelle plate-forme est lancé, comme avec le S60, nous commençons généralement par faire tourner le système Raster. Il est actuellement le système par défaut sur Windows, sur l'embarqué, sur S60 et le sera aussi sur Mac OS X.
Que pensez-vous du système Raster sur X11 ? Ignorez une seconde que vous utilisez actuellement un processus local pour le cache des polices. Les performances sont correctes sur X11 et j'ai vu beaucoup de personnes basculer dessus à l'exécution. Si l'on considère l'affichage à distance, cela semble énorme, mais il continue à n'être pas trop mauvais. La façon dont il fonctionne avec le moteur de rendu X11, c'est que chaque gradient et transformation de pixmaps se fait au niveau logiciel puis est transféré sous forme d'image au niveau des commandes de rendu. Pourquoi ne pas tout faire du côté client et ne télécharger que les parties qui doivent être actualisées ? Nous pouvons regarder les vidéos HD (pour une définition de la HD, de toute façon) sur YouTube, probablement que nous pouvons nous permettre de télécharger quelques pixels. Ceci est lié à la génération de commentaires sur XRender et aux gradients et aux transformations côté serveur, mais cela a été tenté à plusieurs reprises et les performances ne sont tout simplement pas assez bonnes.
L'intégration du système de fenêtres est codée à la main pour chaque plate-forme pour tirer le meilleur parti de celui-ci. Pour Windows, la surface est une QImage qui partage ses données avec une DIBSECTION (structure qui inclut des informations sur un bitmap : dimensions, format de couleur, masques et d'autres informations permettant de gérer le stockage des données), ce qui permet d'obtenir une bonne vitesse lors de la copie des bits. Sur X11, nous utilisons des images MIT à mémoire partagée. Nous avons utilisé les pimas à mémoire partagée, elles ont été retirées de Xorg, mais nous avons eu ce patch impressionnant de la communauté, ce qui nous a permis d'être à nouveau opérationnels. Sur Mac OS X, nous testons l'utilisation des « GL texture streaming » pour le transfert de l'image en cache vers l'écran et nous obtenons des chiffres prometteurs, donc j'espère que cela sera ajouté aussi à Qt 4.7.
Du fait que c'est juste un tableau d'octets, la plupart des API natives ont la capacité de réaliser le rendu dans le même tampon que celui que nous utilisons. Cela rend l'intégration avec les thèmes natifs assez simple, ce qui est l'une des raisons pour lesquelles cela est attrayant en tant que système de bureau graphique par défaut, bien que n'ayant pas d'accélération matérielle.
IV-B. Le système OpenGL▲
Nous avons deux systèmes graphiques OpenGL dans Qt. Un pour OpenGL 1.x, qui est principalement implémenté en utilisant les fonctionnalités de pipeline fixe en combinaison avec quelques programmes de fragments ARB. Il a été écrit pour les versions Desktop de Qt, au début de Qt 4.0 (2004-2005) et a peu évolué depuis. Vous pouvez l'activer en écrivant -graphicssystem opengl1 dans la ligne de commande. Il est actuellement en mode de survie, ce qui signifie que nous corrigeons les problèmes critiques tels que les crashes, mais sinon, nous le laissons tel quel. De notre côté, nous ne nous concentrons pas dessus pour améliorer les performances, même s'il donne d'assez bons résultats dans certains cas.
Notre objectif principal est le système graphique OpenGL/ES 2.0, qui est écrit pour fonctionner sur du matériel graphique moderne. Il n'utilise pas des fonctionnalités d'un pipeline fixe, mais seulement les vertex shaders et fragment shaders. Depuis Qt 4.6, c'est le moteur de rendu utilisé par défaut pour QGLWidget. Ce n'est que lorsque l'une des fonctionnalités demandées n'est pas disponible que nous réutilisons la version 1.x. Quand nous faisons référence à notre moteur de rendu OpenGL, c'est bien de la version 2.0 que nous parlons.
Nous voulions utiliser OpenGL comme moteur de rendu par défaut sur tous les systèmes Desktop pendant un moment, mais il y a deux problèmes majeurs avec. Dessiner sans anti-aliasing est une plaie, il est presque impossible de garantir qu'une ligne sera dessinée où vous le souhaitez sur certains pilotes. L'intégration des thèmes natifs est très difficile. Il est rarement possible de passer un contexte OpenGL à une fonction utilisant les thèmes et de lui demander de dessiner dedans, donc, nous devons utiliser les pixmaps temporaires pour les éléments de style. Sur Mac OS X, il existe une fonction pour obtenir un contexte CGContext à partir d'un contexte OpenGL, mais nous n'avons pas réussi pour le moment à obtenir des résultats appréciables avec. D'un autre côté, une grande partie du contenu de l'interface graphique ne dépend pas de ces caractéristiques, ce qui rend OpenGL optimal typiquement pour le rendu de scène, comme la fenêtre d'un QGraphicsView ou une galerie de photos. Donc, autant la configuration par défaut de Qt est tournée vers l'avenir, autant nous considérons que la meilleure configuration par défaut pour un ordinateur de bureau doit être une combinaison du système Raster pour les widgets utilisant la thématique native et OpenGL pour un ou deux widgets hauts. Rien n'est décidé à ce sujet, mais nous continuons à étudier les autres solutions.
Un autre problème avec l'utilisation d'OpenGL par défaut est le partage des polices. Avec le système Raster, nous pourrions théoriquement partager les symboles pré-rendus entre les processus d'une manière multi-plate-forme en utilisant la mémoire partagée, alors qu'avec OpenGL, cela devient un peu plus difficile. Sur le système X11, il existe une extension pour convertir des textures dans des XPixmaps, lesquelles peuvent être partagées entre les processus, mais cela force généralement à convertir les textures dans un format moins optimal, ce qui les rend un peu plus longs à dessiner, alors ce n'est pas encore optimal. Sur Windows, Mac OS X, S60 ou QWS, nous aurions besoin de travailler au niveau des pilotes pour le partage des identifiants des textures, ce que nous n'avons pas actuellement.
IV-C. Le système OpenVG▲
Je laisse actuellement cette section un peu vide. Je n'ai pas participé à son implémentation, à sa mise en place, ni à son exécution. Il est basé sur EGL, ce qui le rend tout à fait similaire au système graphique OpenGL. Nous espérons que OpenVG sera utilisé dans un certain nombre de systèmes embarqués de milieu de gamme.
La chose intéressante avec OpenVG est qu'il correspond assez bien à l'API de QPainter. Il prend en charge les chemins, stylos, pinceaux, dégradés et modes de composition : en théorie, les API vectorielles devraient donc fonctionner de manière optimale.
Rhys Weatherley, qui a écrit le moteur de rendu OpenVG, envisage de faire un billet sur l’ensemble du fonctionnement interne du moteur de rendu OpenVG dans un avenir proche.
V. Les images et les pixmaps▲
La différence entre ces deux classes est largement abordée dans la documentation, mais je voudrais mettre en évidence une chose qui n’est pas des moindres.
Notre documentation dit : « QImage est conçu et optimisé pour les entrées et sorties et pour l'accès direct aux pixels et leurs manipulations, tandis que QPixmap est conçu et optimisé pour montrer des images à l'écran. »
V-A. Le système Raster▲
Lorsque vous utilisez le système de rendu Raster, les pixmaps sont implémentées comme des QImage, avec une différence qui pourrait être importante : lorsque vous convertissez une QImage en une QPixmap, nous ajoutons quelques petites choses.
L'image est convertie en un format de pixel qui est rapide à rendre dans le tampon mémoire, c'est-à-dire ARGB32_Premultiplied, RGB32, RGB16 ou ARGB8565_Premultiplied. Lorsque les images sont chargées depuis le disque en utilisant le plugin PNG ou lorsqu'elles sont générées par l'application, le format est souvent ARGB32 (non-prémultiplié) puisqu'il s'agit d'un format facile à travailler. J'ai mesuré que copier depuis le format ARGB32_Premultiplied vers RGB32 est 2 à 4 fois plus rapide que de dessiner directement sur RGB32 non-premultiplié, selon les cas d'utilisation.
Deuxièmement, nous vérifions les données de pixels pour les pixels transparents et nous les convertissons dans un format opaque si aucun pixel transparent n'est trouvé. Cela signifie que si un fichier « .png » est chargé au format ARGB32 à partir du disque, mais qu'il ne contient que des pixels opaques, il sera rendu par un RGB32, qui est aussi 2 à 4 fois plus rapide.
V-B. Le système OpenGL▲
Lorsque vous utilisez le système graphique OpenGL, l'implémentation effective de QPixmap varie un peu d'une configuration à l'autre. La meilleure option s'active lorsque votre version d'OpenGL prend en charge les Frame Buffer Objects (FBO) en combinaison avec l'extension GL_EXT_framebuffer_blit. Dans ce cas, la pixmap est représentée comme une texture OpenGL, et à chaque fois qu'un QPainter est ouvert sur la pixmap, nous récupérons un FBO du pool interne et nous l'utilisons pour dessiner dans la texture.
Sans ces extensions disponibles, ce qui est généralement le cas pour OpenGL/ES 2.0, l'implémentation de QPixmap est une QImage (avec les mêmes optimisations que Raster) qui est référencée par un identifiant de texture. Lorsque vous ouvrez un QPainter sur la pixmap, vous dessinez dans la QImage et quand la pixmap est affichée sur l'écran, l'identifiant de la texture est utilisé. En interne, il y a un processus de synchronisation entre les deux représentations, il y aura donc une étape unique de rechargement de la texture après avoir dessiné dedans.
V-C. En général▲
Si vous avez l'intention de dessiner deux fois la même QImage, convertissez-la toujours en QPixmap.
Il y a quelques cas où QPixmap est potentiellement un mauvais choix. Nous avons les fonctions QPixmap::scaled(), QPixmap::tranformed() et similaires, qui, historiquement, sont là parce que nous voulions que QImage et QPixmap aient la même interface. Nous avons le projet de réimplémenter ces fonctionnalités en nous basant sur QPixmap, mais, actuellement, aucun moteur de rendu ne fait cela. Aussi, dans le cas d'OpenGL, ou pour X11 sur ce point, appeler QPixmap::tranformed() implique une conversion de QPixmap en QImage, conversion uniquement par logiciel.
Par défaut, un QPixmap est considéré comme opaque. Lorsque vous faites QPixmap::fill(Qt::transparent), cela est réalisé dans une pixmap avec canal alpha, qui est plus lent à dessiner. Si la pixmap va finir comme opaque, initialisez-la avec QPixmap::fill(Qt::white). Vous pouvez même sauter complètement l'étape d'initialisation, si vous savez que tous les pixels seront écrits avec une couleur opaque au moment où la pixmap est dessinée.
Avant de passer à autre chose, je vais juste donner un petit avertissement sur les fonctions setAlphaChannel et setMask et regarder naïvement les fonctions alphaChannel() et mask(). Ces fonctions font partie de l'héritage de Qt 3 dont nous n'avons pas tout à fait réussi à nous débarrasser lors du passage à Qt 4. Dans le passé, le canal alpha d'une pixmap ou son masque était stocké séparément des données de la pixmap. En fonction de la plate-forme sur laquelle vous étiez, l'implémentation effective était un peu différente. Par exemple, sur X11, vous aviez une pixmap de 1 bit pour le masque, un canal alpha de 8 bits et un tampon de couleurs considéré de 24 bits. Sous Windows, vous aviez un masque de 1 bit et un tampon de pixels codé en ARGB 32 bits. Dans Qt 4, nous avons fusionné tout cela en une seule API, de sorte que QPixmap peut être considéré comme une structure de données compacte de pixels ARGB. Pour autant, nous n'avons pas supprimé les fonctions implémentées de l'ancienne API. En fait, nous avons même ajouté les accesseurs pour le canal alpha, nous n’avons donc fait qu'aggraver la situation. L'API est restée dans une certaine mesure pratique, mais ces quatre fonctions impliquaient de toucher à l'ensemble des données et, soit de fusionner la source avec la pixmap, soit d'extraire une nouvelle pixmap à partir du contenu de la pixmap actuelle. En conclusion : ne les utilisez pas. Avec les modes de composition, vous pouvez manipuler le canal alpha des pixmap à l'aide de QPainter. Un autre avantage est que cela pourrait également être optimisé par des instructions SSE pour le moteur de rendu Raster ou par accélération matérielle avec OpenGL, donc cela pourrait être un peu plus rapide. Il y a aussi QGraphicsOpacityEffect qui vous permet de définir un masque sur des widgets et des éléments graphiques, mais pour le moment, ce n'est pas aussi rapide que nous le souhaiterions.
VI. QGraphicsView▲
Je consacre un chapitre spécifique sur graphicsview, je vais donc commenter rapidement la différence entre l'utilisation de QGraphicsView avec des items et de QWidget. QGraphicsView, avec sa scène peuplée avec des objets, est très semblable sur plusieurs points aux widgets et à leur mise à jour « their repaint handling ». Avec l'ajout des layouts et les QGraphicsWidgets, la différence est encore plus floue. Alors, quelle solution faut-il choisir ? De plus en plus souvent, nous constatons que les gens choisissent de créer leurs interfaces utilisateur dans des QGraphicsView plutôt que de les créer en utilisant des widgets traditionnels.
Par rapport à des widgets, des items dans une vue graphique sont peu coûteux. Si l'on considère à nouveau la galerie photo, qui utilise un item différent pour chacun des éléments de la vue, cela peut (je dis bien « peut ») être raisonnable. Un widget est redessiné dans son paintEvent. Un QGraphicsItem est redessiné dans sa fonction paint. Une chose intéressante dans cette fonction, pour les items, est qu'il n'y a pas d'appel à begin puisque QPainter est déjà correctement configuré pour le rendu. Il a moins de garanties concernant l'état du QPainter contrairement à une utilisation par un widget. Il peut y avoir une transformation et des découpages, mais aucune garantie à propos des polices, des styles de dessin ou des brosses. Cela rend la configuration un peu moins coûteuse.
Une autre amélioration considérable par rapport aux widgets est que les items ne sont pas découpés par défaut. Ils ont un rectangle enveloppant et il y a un contrat entre celui qui implémente une sous-classe d'item et la scène, pour que l'objet ne se dessine pas à l'extérieur de ce rectangle. Si l'on compare cela au système de découpe que nous devons définir pour les widgets, alors ici encore, il y a moins de travail à faire pour les items. Si un item ne respecte pas ce contrat, il y aura des artefacts de rendu, mais pour graphicsview, cela est considéré comme étant un compromis acceptable.
La plupart des éléments d'interface sont assez simples. Un bouton, par exemple, peut être composé d'une image de fond et un texte court. Du point de vue de QPainter, il y a un appel à drawPixmap et un appel à DrawText. Moins il y a de temps passé entre les appels à QPainter, meilleure est la performance. Moins il y a de changements d'état entre les appels à QPainter, meilleure est la performance. Si vous retournez voir combien de temps se passe entre ces appels pour un bouton, vous vous rendrez rapidement compte que les widgets traditionnels sont assez lourds. Si les widgets veulent survivre à l'épreuve du temps, ils auront besoin de se comporter davantage comme les QGraphicsItem.
VII. Conclusion▲
J'ai argumenté assez longuement et j'espère que vous avez trouvé quelques informations utiles dans mon propos. Vous avez peut être remarqué que je ne mentionne pas l'impression, la génération des PDF ou des SVG, je ne mets pas l'accent sur les moindres détails des moteurs de rendu X11 ou CoreGraphics. C'est parce que, comme indiqué dans la documentation de QPainter sur les performances, nous concentrons nos efforts sur la performance sur seulement quelques moteurs que nous considérons comme essentiels pour Qt.
Au nom de toute l'équipe Qt, j'aimerais adresser le plus grand remerciement à Nokia pour nous avoir autorisé la traduction de cet article !