Qt Graphics et performance - Le moteur de rendu Raster

| Gunnar | Qt Labs |

Traducteur

traducteur : Guillaume Belz

Le moteur de rendu Raster est le moteur de rendu par défaut de Qt. L'ensemble des fonctionnalités de dessin de Qt y est implémenté, uniquement par logiciel, ce qui permet d'utiliser ce moteur sur toutes les plates-formes.

Cet article décrit en détail le fonctionnement de ce moteur pour optimiser le rendu des applications et les fonctionnalités qui peuvent être très coûteuses en temps et mesure les performances de plusieurs fonctions.

N'hésitez pas à commenter cet article !
Commentez

- Titre : Qt Graphics et performance - Le moteur de rendu Raster

- Auteurs : Gunnar, Qt Labs

- Traducteur : traducteur : Guillaume Belz

- Parution : 24 octobre 2010

- Mise à jour : 10 février 2020

- Licence : Copyright ® 2010 Gunnar. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. L'article original

- II. Introduction

- III. Histoire

- IV. Structure générale

- IV-A. Strokes lignes, polylignes et le chemin

- V. Changements d'état

- VI. Fonctions Span

- VII. Cas particuliers et optimisations

- VIII. Quelques chiffres

- IX. Conclusion

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 Quarterly 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 - The Raster Engine de Gunnar paru dans Qt Labs.

II - Introduction

Le sujet du jour est le moteur de rendu par défaut de Qt. C'est le moteur de référence et le seul qui implémente toutes les fonctionnalités possibles offertes par QPainter.

III - Histoire

L'histoire du moteur logiciel de Qt a commencé autour de décembre 2004, si ma mémoire est bonne. Mon collègue Trond et moi avons travaillé pendant un certain temps sur une nouvelle architecture de moteur de rendu pour Qt 4, nom de code « Arthur ». Trond a travaillé sur les moteurs X11 et OpenGL 1.x et je me suis concentré sur la combinaison du moteur Win32 GDI/GDI+ avec QPainter et les API similaires. Nous avions introduit quelques nouvelles fonctionnalités, telles que l'anticrénelage, la transparence alpha pour QColor, la prise en charge complète des transformations et les gradients linéaires. Comme peu de ces nouvelles fonctionnalités ont été prises en charge par GDI, cela signifiait que l'utilisation de ces fonctions impliquait de passer à GDI+, qui était à l'époque incroyablement lent, au moins sur toutes les machines que nous avions dans le bureau d'Oslo à l'époque. En fait, activer le mode de rendu graphique avancé GDI+ pour faire les transformations n'était pas très rapide non plus.

Puis nous sommes tombés sur la boîte à outils appelée Anti-Grain Geometry (AGG) qui a implémenté toutes les fonctions par logiciel, entièrement en C++, et nous étions surpris par tout ce qu'elle pouvait faire. Notre première réaction fut de nous recroqueviller sur le sol en agonisant, en pensant que nous avions tout faux à ce sujet. L'utilisation de ces API natives ne nous avait pas du tout aidés. En fait, elles nous empêchaient d'obtenir l'ensemble des fonctionnalités que nous voulions avec une performance qui aurait été acceptable. Une fois que nous nous sommes remis au travail, notre première idée a été d'essayer d'implémenter un moteur de rendu AGG personnalisé, lequel n'aurait qu'à déléguer tous les dessins au pipeline d'AGG. Mais hélas, l'utilisation des templates dans l'API d'AGG combinée avec l'extrême généricité de l'API de QPainter n'a permis d'aboutir qu'à un pipeline beaucoup moins performant que les démos que nous avions vues.

Nous avons donc pris nos vacances de Noël et puis repris en janvier 2005. Toujours très déprimés par l'ensemble des nouvelles fonctionnalités qui ne fonctionnaient pas, combiné au fait d'être limités par un sous-ensemble minimal de l'API native, je suis allé voir Matthias et Lars et j'ai demandé si je pouvais avoir trois semaines, le temps de coder ensemble un moteur de rendu permettant du réaliser le rendu uniquement de manière logicielle, comme preuve de concept. J'ai reçu un « OK » et j'ai passé les semaines suivantes à implémenter de manière logicielle des transformations de pixmaps, le filtrage bi-linéaire, la prise en charge du découpage et, trois semaines plus tard, j'avais un moteur de rendu fonctionnel, uniquement logiciel, et j'ai annoncé fièrement que j'avais « à peu près terminé ». J'ai reconstruit une image de ce dont je m'en souviens :

Le système de découpe allait un peu dans tous les sens, les modèles de bitmap avaient été brisés, mais le pire venait peut-être du fait que tout le texte était dessiné à l'aide QPainterPath et que tous les rendus avaient été lissés. Bien que le rendu ne soit pas correct à 100%, les performances des différentes fonctionnalités sont correctes. On a convenu que c'était un bon début, mais qu'il fallait un peu plus de travail. Et ainsi a commencé le sprint pour la version Qt 4.0 bêta quelques mois plus tard.

La version initiale qui a été publiée avec Qt 4.0 a bien fonctionné pour ce qui concerne les fonctionnalités, mais, avec le recul, les performances étaient loin de ce que nos utilisateurs de Qt exigeaient. En conséquence, nous avons récolté de nombreuses critiques au cours de la première année de Qt 4.0. Depuis, nous avons fait beaucoup, et je veux dire BEAUCOUP, et mon sentiment est que c'est le moteur qui réalise le meilleur rendu pour une utilisation moyenne de Qt, donc je pense que nous avons fait un bon choix à l'époque en abandonnant GDI et GDI+. Comme je l'ai indiqué dans mon précédent billet, nous nous sommes amusés à porter ce moteur sur tous les systèmes Desktop, pour que celui-ci soit le moteur par défaut, pour des raisons de rapidité et de cohérence.

IV - Structure générale

Le fonctionnement global du moteur, c'est que tout dessin est décomposé en bandes horizontales avec une valeur de couverture, appelée span. Plusieurs spans ensemble formeront le « masque » d'une forme donnée et chaque pixel qui est à l'intérieur du masque est rempli en utilisant une fonction span.

L'image montre une ligne d'un polygone qui est rempli avec un dégradé linéaire. Il y a quatre spans : une qui augmente l'opacité du polygone et deux qui diminuent l'opacité du dégradé. Pour chaque pixel dans le polygone, la fonction de gradient est appelée et nous écrivons le pixel sur sa destination, peut-être en la mélangeant avec un alpha si la valeur de couverture est différente de l'opacité complète ou si le pixel que nous recevons de la fonction gradient contient un alpha.

Le découpage utilise également le même mécanisme. La fonction span pour le découpage prend les spans qui arrivent, calcule leurs intersections avec les spans qui définissent le découpage et appelle la fonction de remplissage.

Toutes les opérations suivent ce modèle. Lorsqu'un appel à drawRect arrive, nous générons une liste de spans pour chaque ligne et mettons en place une fonction span selon le pinceau courant. Une pixmap est similaire, nous créons une liste de spans et utilisons une fonction span pour la pixmap. Un polygone est passé à une fonction de conversion des lignes qui produit une liste de spans, etc. Nous avons deux convertisseurs de lignes, un pour le rendu avec anti-aliasing et un autre pour le rendu avec aliasing. Celui avec anti-aliasing est en quelque sorte un dérivé de greyraster.c de FreeType, avec quelques modifications mineures. Je pense que nous avions besoin d'ajouter le support « odd-even fills », par exemple. Le texte est également converti en span.

IV-A - Strokes lignes, polylignes et le chemin

Ces primitives sont transmises à un processus séparé appelé stroker. Le stroker crée un nouveau chemin qui correspond visuellement à la forme à remplir qui est représentée par le tracé. Il y a une API publique pour cela aussi dans QPainterPathStroker. Cette forme à remplir est ensuite transmise à l'un des convertisseurs de lignes, qui à son tour convertit la forme en span. Pour les tracés en pointillés, le même processus est suivi et la forme à remplir qui en résulte est un chemin avec une quantité potentiellement très grande de sous-chemins. Naturellement, la conversion d'un tel sous-chemin est coûteuse, ce qui est l'une des raisons pour lesquelles nous n'avons explicitement pas mis les lignes en pointillés dans la liste des caractéristiques de haute performance. En fait, dans de nombreux cas, le rendu des lignes pointillées est l'une des opérations les plus lentes disponibles dans le moteur Raster, donc il faut l'utiliser avec une extrême prudence.

Une autre solution, qui peut donner de meilleurs résultats, est de fixer une pixmap dans un pinceau de 2x2 en noir et blanc ou en noir et transparent et de l'utiliser pour tracer le stroke. C'est un peu plus lourd à mettre en place, mais si c'est ce qu'il faut faire pour aller un peu plus vite, alors il faut l'utiliser.

V - Changements d'état

Toute les fonctions telles que setBrush, setTransform ou d'autres qui changent l'état de QPainter, se traduiront par un ensemble différent de fonctions span utilisées. Chaque pinceau (ou n'importe quel type permettant le remplissage si vous voulez, par exemple un stylo, qui, à ce niveau, fait essentiellement du remplissage) a une fonction span particulière qui lui est associée et également des données de span par pinceau. Pour le remplissage avec des couleurs unies, les données de span contiennent la couleur ; pour le rendu de pixmaps transformées, elles contiennent la matrice inverse, un pointeur vers les pixels sources, les octets par ligne et d'autres informations requises. Pour les découpages, elles contiennent la fonction span à appeler après avoir découpé les spans. La seule chose à noter à propos des changements d'état, c'est qu'à chaque fois que vous passez d'une brosse à une autre brosse ou d'une transformation à une autre, ces structures doivent être mises à jour. Jusqu'à Qt 4.4, c'était dans de nombreux cas un problème notable pour les performances, comptant jusqu'à 10-15% lors de profiling du rendu de vue de scène graphique, mais depuis Qt 4.5, les effets de ce changement sont minimaux.

Eh bien, ça n'est peut-être pas minimal pour tracer une ligne de deux pixels de long, mais minimal par rapport au remplissage d'un rectangle de 64x64. Le fait est que même si le moteur Raster est le moteur qui gère au mieux les changements d'état, il y a des cas où il peut être amélioré et être minimisé.

VI - Fonctions Span

La tâche des fonctions span est de générer un pixel et de le combiner avec la destination en fonction de l'état actuel de QPainter. Bien que le moteur Raster supporte le rendu vers tous nos formats d'image, sauf le format 8-bits indexé, en interne, il réalise tous les rendus dans le format ARGB32_Premultiplied (dans lequel les composantes RGB sont enregistrées après multiplication par alpha). Prémultiplier par le canal alpha a l'avantage que nous n'avons pas à multiplier le canal alpha par les canaux de couleur et nous permet donc d'économiser une division lors de la composition. La raison de faire tous les rendus dans un format unique est qu’une autre solution n'est tout simplement pas gérable. Il suffit de penser au nombre de combinaisons des modes de composition multiplié par le nombre de formats d'image que peut avoir l'image source, multiplié par le nombre de formats de destination possibles. Pour supporter toutes les combinaisons, nous avons une approche générique qui consiste, pour chaque span, à :

- récupérer les pixels sources, par exemple à partir d'un gradient, d'une pixmap, d'une image ou d'une couleur unie, et les convertir en ARGB32_Premultiplied ;

- récupérer les pixels de destination et les convertir en ARGB32_Premultiplied ;

- mélanger de la source dans la destination en utilisant le mode de composition actuel ;

- convertir le résultat en format de destination et le réécrire.

Cela peut sembler beaucoup de travail, mais heureusement, l'histoire ne s'arrête pas là.

VII - Cas particuliers et optimisations

Comme je l'ai indiqué dans le chapitre sur les performances, dans la documentation de QPainter que j'ai ajoutée récemment et qui a été le début de cette série d'articles sur le blog, le tout est de définir dans quelles conditions nous voulons être rapides et dans quelles conditions nous souhaitons que le travail soit simplement fait. Au fil des ans, depuis la première version du moteur Raster durant l'été 2005, nous avons ajouté des tonnes de cas particuliers en fonction de l'expérience que nous avons acquise sur les fonctions qui sont appelées le plus et qui ont le plus d'impact.

Tout d'abord, si vous regardez les étapes au-dessus que nous suivons pour chaque span, vous voyez que nous transformons les pixels en ARGB32_Premultiplied. Les couleurs unies sont faciles à représenter, les gradients sont générés dans ce format directement, donc la conversion n'est réalisée que pour les images et les pixmaps. Si l'image est ARGB32_Premultiplied, aucune conversion n'est nécessaire et il nous suffit d'utiliser le pointeur de scanline directement, sans aucune copie. Notre format RGB32 est spécifié pour être 0xffRRGGBB, avec l'alpha fixé à 0xFF. Cela signifie qu'il est compatible au niveau des pixels avec ARGB32_Premultiplied, ce qui signifie aussi qu'il peut également être utilisé directement. Si la source est ARGB32, vous obtiendrez un memcpy pour chaque ligne, où les données au format ARGB32 sont copiées dans un tampon temporaire et converties en ARGB32_Premultiplied. Que pouvez-vous retenir de ça ? De ne pas dessiner des images ARGB32 dans le moteur Raster. Deuxièmement, de ne pas créer un QPainter sur une image ARGB32, les conséquences seront exactement les mêmes, mais au moment de la lecture et de l'écriture des pixels de destination. Maintenant, vous savez pourquoi QPixmap est de préférence dans ce format aussi...

Le mode de composition Source est un cas particulier pour la plupart des opérations. Par exemple, nous ne lisons pas la destination pour la composition Source, parce que nous savons qu'il n'y a pas de mélange impliqué, à moins que les spans aient une couverture partielle qui le fait. Cela signifie que Source est effectivement juste une écriture en mémoire.

SourceOver est habituellement un cas particulier qui doit être incorporé et fusionné avec l'opacité de couverture donc elle est aussi généralement plus rapide que les modes de composition. En ce qui concerne les optimisations ci-dessous, elles ne sont valables que pour Source et SourceOver, donc si vous voulez de meilleures performances, assurez-vous que c'est ce que vous utilisez. SourceOver est la composition par défaut dans QPainter, de toute façon.

Pour les dégradés et les pixmaps, nous avons besoin de créer un tableau de données pour la source. Pour les couleurs unies, c'est juste un seul pixel, donc c'est plus rapide. La couleur de la source bénéficie également du fait que vous n'avez seulement qu'à traverser la mémoire pour la destination, dans laquelle vous écrivez également, le besoin de mise en cache est ainsi considérablement réduit.

Les rectangles pleins sont très fréquents, à la fois avec QPainter::fillRect et QPainter::drawRect. Dans Qt 4.4, ces deux fonctions impliquaient un changement d'état. En fait, fillRect impliquait deux changements d'état, car elle changeait le pinceau en fonction des paramètres qui étaient passés à fillRect puis remettait QPainter dans l'état dans lequel il était avant. Avec Qt 4.5, dans le cadre du projet Falcon, nous avons introduit une nouvelle sous-classe interne QPaintEngine qui supporte la fonction fillRect avec une couleur sans changer d'état. Cela correspond à la façon dont les applications utilisent normalement QPainter de toute façon.

En plus d'être sans changement d'état, la fonction fillRect est un cas particulier pour un certain nombre de cas d'utilisation. Par exemple, pour RGB16, nous écrivons deux pixels à la fois, pour les machines Intel, il y a une version optimisée avec SSE/MMX. Les cas particuliers de fillRect ont aussi l'avantage qu'ils ne nécessitent pas de span, c'est juste une courte boucle 2D, ce qui nous économise aussi un peu de travail, du moins si les spans sont courts.

L'optimisation Duff device. Je ne peux pas prendre le crédit pour son ajout, mais il est utilisé dans beaucoup d'endroits différents dans le moteur Raster aujourd'hui. Cela concerne le déroulement des boucles. Si vous n'êtes pas familier avec cette technique, renseignez-vous sur ce sujet. C'est une belle fonctionnalité du C++ qui permet de faire des choses potentiellement plus rapidement.

Le découpage rectangulaire est également un cas particulier, du moins tant qu'il n'y a pas de transformation définie sur QPainter. La translation est bien sûr un cas particulier, mais la mise à l'échelle et la rotation désactivent cette optimisation. L'avantage que nous avons de faire du découpage rectangulaire est que la recherche du span à remplir est faite au niveau de QRect, plutôt qu'au niveau de chaque span, ce qui la rend beaucoup plus rapide.

Donc, si vous utilisez une composition Source ou SourceOver, une transformation sans perspective et sans lissage et que le découpage est rectangulaire, vous pouvez bénéficier également de l'avantage de nos fonctions de mélange des pixmaps. Celles-ci ont été ajoutées dans Qt 4.5 et c'est la raison pour laquelle le rendu des pixmaps est un peu plus rapide maintenant que dans les versions antérieures. Dans Qt 4.5, nous avions des fonctions de mélange pour la mise à l'échelle et la translation seulement et dans Qt 4.6, nous avons également ajouté à la liste les rotations. Encore une fois, nous nous concentrons sur un sous-ensemble sélectionné de formats, correspondant à ceux qui peuvent être utilisés avec QPixmap. Ces fonctions ne sont disponibles que pour :

- ARGB32_Premultiplied sur ARGB32_Premultiplied ;

- ARGB32_Premultiplied sur RGB32 ;

- ARGB32_Premultiplied sur RGB16 ;

- ARGB8565_Premultiplied sur RGB16 ;

- RGB32 sur RGB32 ;

- RGB16 sur RGB16.

Je pense que c'est tout.

Les contours sont traités par le stroker dans le cas général. Cependant, il y a encore un certain nombre de cas particuliers pour lesquels nous pouvons utiliser un algorithme du point central (une variante de l'algorithme de Bresenham). Les lignes, les polylignes et les chemins qui ne contiennent que des segments de lignes seront restitués en utilisant l'approche rapide basée sur le point aussi longtemps que l'épaisseur du trait est égale ou inférieure à un. Nous utilisons également cette méthode pour dessiner les segments de lignes pointillées de un pixel de large. Pour toute largeur de tracé plus grande qu'un, pour les trajectoires courbes ou pour l'anti-aliasing, nous retournons à l'approche stroker qui fonctionne, mais est beaucoup moins optimale. En fait, je pense qu'il y a un cas particulier pour les lignes pointillées avec anti-aliasing aussi, tant qu'elles sont minces.

Lorsque l'anti-aliasing est activé, nous avons souvent besoin de retourner au stroker pour les contours, ce qui est un peu plus lent que le cas général. En plus de cela, il y a beaucoup plus de spans générées pour le contenu avec l'anti-aliasing, en raison de l'effet d'apparition et de disparition sur le bord de la primitive, donc attendez-vous à ce que l'anti-aliasing ait un coût important.

Le rendu du texte a été très fortement optimisé depuis la version 4.5 pour la plupart des moteurs de rendu, au point que maintenant, le principal goulot d'étranglement est en fait la disposition des textes réels sur la chaîne. Nous travaillons sur une API pour mettre en cache cela, le rendu de texte peut donc être vraiment rapide. Mais sur la base de l'API actuelle, il est aussi bon qu'il peut l'être. Toutefois, si la transformation est une rotation ou une mise à échelle, alors nous retombons sur un rendu de chemin. Seule la version Windows du moteur Raster permet de dessiner les symboles avec un angle de rotation en utilisant les chemins rapides, alors méfiez-vous de cela.

Cela fait beaucoup de détails, mais permet de donner une idée de ce qu'il y a à prendre en compte lorsque vous écrivez du code pour ce moteur. Si vous ne dessinez que des pixmaps de 1024x1024, aucune de ces choses n'a d'importance puisse que tout le temps est quand même passé dans la fonction span qui réalise le mélange des pixmaps, mais à la seconde où vous avez plus de contenu, plusieurs lignes, plusieurs polygones, qui sont plus petits, alors ces choses sont essentielles pour réaliser de bonnes performances.

Les performances globales du moteur, lorsqu'il est utilisé conformément à ce qui est décrit ci-dessus, peuvent être décrites par la formule suivante :

traitementsConstants + O(pixelsTouchés * capacitésDesBusEtMémoires)

Il n'y a rien de scientifique dans cette formule, mais quand vous recherchez le rendu optimal, tout le temps sera passé dans l'une des nombreuses boucles à l'intérieur de qdrawhelper_xxx.cpp ou de qblendfunctions.cpp. Ces boucles passent tout leur temps sur du traitement par pixel. Si ces fonctions pouvaient être modifiées pour être plus rapides en utilisant des algorithmes un peu différemment, alors très bien. Mais si vous voyez lors d'un profiling que tout le temps est passé par exemple dans qt_blend_argb32_on_argb32, cela signifie que vous pouvez nous dire de mélanger les pixmaps avec alpha. Nous ajouterons cela aussi vite que nous pourrons et les pertes entre votre application et le traitement effectif seront à zéro. Si tout le temps est consacré au traitement des pixels, alors c'est une bonne chose. Les traitements constants ici sont le temps passé dans les changements d'état et le coût d'appel des fonctions et assimilés.

VIII - Quelques chiffres

J'ai reçu quelques commentaires sur l'un des blogs précédents, disant qu'il serait bien d'avoir quelques graphiques à barres. Je vais alors ajouter quelques chiffres sur les performances qu'il est possible d'obtenir avec le moteur de rendu Raster. J'ai mesuré les temps d'affichage à la fois sur mon ordinateur de bureau Windows et sur mon N900 pour obtenir une comparaison. Les performances vont de plusieurs millions d'opérations par seconde à seulement quelques centaines. L'échelle des graphiques est donc logarithmique, gardez ce point à l'esprit lorsque vous les regardez.

Comme vous pouvez le voir, le taux de remplissage est plus ou moins lié au nombre de pixels concernés. Pour certaines opérations, il faut un peu plus de temps pour faire quelque chose, comme drawPixmap avec mise à l'échelle qui est un peu plus lent que drawPixmap sans, mais vous voyez que la formule brute que j'ai donnée plus haut tient assez souvent. Doublez la taille de la primitive dans chaque direction et vous avez un quart de la performance. Il n'était pas dans mes intentions de vous tromper avec plusieurs paramètres différents pour drawPixmap, c'est juste la façon dont le test a été mis en place.

Si vous comparez les trois versions du rendu d'un rectangle de 4x4, vous voyez qu'elles diffèrent lorsque les rectangles sont de petite taille. drawRect sans changement de pinceau est plus rapide, à environ 7,4 Mops/sec (million d'opérations par seconde), suivie par fillRect à environ 6,1 Mops/sec puis drawRect au changement de pinceau à 1,8 Mops/sec. Avec un rectangle de 128x128, la différence entre les deux est juste un peu plus petite. C'est là où je voulais en venir avec les changements d'état ci-dessus : il est possible de les utiliser et si vous dessinez les zones de taille moyenne, cela n'a pas d'importance. Mais si vous dessinez des pixels, que vous tracez plein de petites lignes ici et là ou que vous utilisez des effets de particules avec des pixmaps de 8x8, alors vous voulez le faire dans une petite boucle avec rien d'autre qui ne se passe.

Vous pouvez également voir que la vitesse de mise à l'échelle sans lissage présente des performances proches de la version sans mise à l'échelle.

Enfin, si vous comparez les performances sur N900 avec la version Desktop sous Windows, vous voyez que, malgré le fait que la machine sous Windows possède un processeur quatre fois plus rapide, la vitesse est souvent dix fois plus basse sur N900. Pourquoi ? Parce que le CPU n'est pas la seule limitation : les capacités des bus ou des mémoires sont aussi un facteur limitant, et, pour être honnête, ce n'est pas une comparaison très équitable...

J'espère que vous avez aimé ce billet et d'autres viendront en 2010.

IX - Conclusion

Au nom de toute l'équipe Qt, j'aimerais adresser le plus grand remerciement à Nokia pour nous avoir autorisés à traduire cet article !

Un grand merci à eusebe19 et à jacques_jean pour leur relecture très attentive et leurs conseils.