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 Threaded OpenGL in 4.8 de Jason Barron paru dans Qt Labs.

Cet article est une traduction d'un des tutoriels écrits par Nokia Corporation and/or its subsidiary(-ies) inclus dans la documentation de Qt, en anglais. Les éventuels problèmes résultant d'une mauvaise traduction ne sont pas imputables à Nokia.

II. Introduction

Si vous avez déjà utilisé le module OpenGL dans Qt à un moment ou un autre, vous vous êtes peut-être retrouvé à vouloir exécuter une partie d'OpenGL dans un thread séparé. L'implémentation sous-jacente d'OpenGL est en elle-même (principalement) réentrante, il n'y avait rien en pratique pour l'empêcher. En fait, pour rappel, dans le Qt Quarterly numéro 6, on avait même décrit la manière de le faire. C'est bien pour les gens qui passent la plupart de leur temps à écrire en OpenGL pur, mais qu'en est-il de ceux qui souhaitent utiliser une partie des classes de Qt dans un thread séparé ? Malheureusement, cela n'était pas possible, puisque ces classes Qt n'étaient pas thread-safe. Avec Qt 4.8, cela a changé et quelques-uns des scénarios les plus courants sont maintenant supportés. Pour utiliser cette nouvelle fonctionnalité sur X11, on doit activer l'option Qt::AA_X11InitThreads pour garantir que les appels en arrière-plan à GLX sont thread-safe ; pour Windows et Mac OS X, cela devrait fonctionner directement.

III. Le thread d'échange de tampons

En fonction de votre processeur graphique (GPU) et de vos pilotes, l'appel à swapBuffers() peut être un appel de fonction coûteux (en particulier sur les processeurs embarqués). Dans de nombreux cas, elle indique au GPU qu'il faut prendre toutes les commandes de rendu et les exécuter pour réaliser le rendu de l'image en cours. Si cette opération est synchrone, alors le thread principal est bloqué pendant que le GPU réalise son travail. C'est malheureux, parce que le thread courant a de meilleures choses à faire que d'attendre le GPU. Par exemple, il pourrait revenir à la boucle d'événements et gérer quelques actions d'un utilisateur, ou le trafic réseau ou encore avancer à la scène suivante d'une animation. La solution, dans Qt 4.8, est d'avoir un thread séparé dont le seul but est d'attendre le GPU en appelant swapBuffers(). En pratique, on peut tout dessiner comme d'habitude dans le thread principal mais, au lieu d'appeler swapBuffers(), on appelle doneCurrent() sur le contexte OpenGL courant.

On doit alors informer le thread d'échange que le rendu est achevé pour qu'il appelle makeCurrent() (ce qui active le contexte) et appeler ensuite swapBuffers(). Le thread d'échange pourra alors appeler doneCurrent() et informer le thread principal qu'il a terminé.

À noter qu'il y a ici un changement de contexte et cette surcharge peut être plus importante que les gains si le thread principal venait à attendre le GPU pour finir. Il est donc important de tester pour voir si on gagne réellement quelque chose.

IV. Le thread de chargement de textures

Le transfert d'un nombre important de textures (ou de grande taille) est généralement une opération coûteuse en raison de la quantité de données devant être envoyées au GPU. De nouveau, c'est l'une des opérations qui peuvent bloquer inutilement le thread principal. Dans la version 4.8, on peut contourner ce problème en créant une paire de QGLWidget partagés. L'un d'eux est activé dans un thread séparé (le thread de chargement) et ne sera jamais visible à l'écran. Le thread principal indique au thread de chargement les images à charger et celui-ci appelle simplement bindTexture() sur chacune de ces images. Il avise ensuite le thread principal à chaque fois que l'un des chargements d'image se termine et qu'elle est prête à être affichée à l'écran.

V. Le thread QPainter

Depuis Qt 4.8, il est possible d'utiliser QPainter dans un thread séparé pour dessiner dans un QGLWidget, un QGLPixelBuffer et un QGLFrameBufferObject, en supposant l'utilisation du moteur de rendu OpenGL [ES] 2.0. Il est important de noter que le QGLWidget n'est pas déplacé dans un thread secondaire (après tout, c'est un QGLWidget). Toutefois, étant donné que son contexte est utilisé autre part, il est nécessaire d'appeler doneCurrent() sur le QGLWidget pour libérer le contexte du thread principal. Ensuite, on doit créer un objet distinct, dérivant de QObject, qui peut être déplacé vers le thread de rendu secondaire. Le QObject utilisera le contexte du QGLWidget pour en faire le contexte actif dans le thread de rendu. Il pourra alors ouvrir un QPainter sur la cible et commencer à dessiner. Si la cible est un QGLWidget, alors on doit également activer le paramètre Qt::WA_PaintOutsidePaintEvent sur le QGLWidget. De plus, il faudra créer une classe héritant de QGLWidget et réimplémenter les fonctions resizeEvent() et paintEvent().

L'implémentation par défaut de ces fonctions essaye d'activer le contexte du QGLWidget et, puisque ces fonctions sont appelées depuis le thread principal (qui contient le widget), on ne veut pas que cela se produise, puisque l'on utilise ce contexte dans le thread de rendu. Dans l'implémentation de ces fonctions, on doit informer la classe dérivée de QObject qui s'occupe du rendu, de sorte qu'elle puisse redimensionner la fenêtre ou redessiner la scène si nécessaire. Le résultat peut être vu ci-dessous dans une nouvelle démo appelée glhypnotizer, qui gère plusieurs sous-fenêtres MDI contenant un QGLWidget, dessiné dans un thread séparé.

Image non disponible
Copie d'écran de l'exemple demos/glhypnotizer dans Qt 4.8

VI. Remerciements

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 à dourouc05 pour sa relecture très attentive et ses conseils. Merci à ClaudeLELOUP pour sa relecture orthographique et à djibril pour son suivi pour la publication.