Les cartes graphiques/Les cartes accélératrices 3D

Le premier jeu à utiliser de la "vraie" 3D fût le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.

Les bases du rendu 3D modifier

Une scène 3D est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.

Les objets 3D et leur géométrie modifier

Les objets placés dans la scène 3D sont composés de formes de base, combinées les unes aux autres pour former des objets complexes. En théorie, les formes géométriques en question peuvent être n'importe quoi : des triangles, des carrés, des courbes de Béziers, etc. En général, on utilise des polygones par simplicité, les autres solutions étant peu pratiques et plus complexes. Dans la quasi-totalité des jeux vidéo actuels, les objets sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de maillage, (mesh en anglais). Il a été tenté dans le passé d'utiliser des carrés/rectangles (rendu dit en quad) ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.

 
Illustration d'un dauphin, représenté avec des triangles.

Les modèles 3D sont définis par leurs sommets, aussi appelés vertices dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet. Ensuite, les sommets sont reliés entre eux. Un segment qui connecte une paire de sommets s'appelle une arête, comme en géométrie élémentaire. Si plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une face, ou encore une primitive. Je le répète, mais les seules faces actuellement utilisées en rendu 3D sont les triangles. Quand plusieurs faces sont sur un même plan, elles forment un "polygone", bien que le terme ne soit utilisé comme en mathématique. Un assemblage de plusieurs "polygones" donne une surface. Dans la suite du cours, nous parlerons surtout des sommets et des primitives.

 
Surface représentée par ses sommets, arêtes, triangles et polygones.

La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.

Les textures modifier

Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des textures, des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.

 
Texture Mapping

Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des texels. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.

Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.

Le placage de textures modifier

Plaquer une texture sur un objet peut se faire de deux manières. La première part d'une texture et associe un sommet à chaque texel. Mais ce n'est pas la technique utilisée actuellement. De nos jours, on fait l'inverse : on attribue un texel à chaque sommet. Le nom donné à cette technique de description des coordonnées de texture s'appelle l'UV Mapping, ou encore de placage de texture inverse.

Avec l'UV Mapping, chaque sommet est associé à des coordonnées de texture, qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.

Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le mip-mapping. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.

 
UV Mapping

Le placage de texture inverse est opposé à une technique qui part d'une texture et associe un sommet à chaque texel. Elle s'appelle le forward texture mapping, ce qui peut se traduire par placage de texture direct. Nous n'en parlerons pas beaucoup, car la majorité des cartes graphiques utilise le placage de texture inverse, mais quelques rares cartes graphiques ont utilisé du placage de texture direct. C'était le cas sur la console 3DO, la Sega Saturn, maiss aussi sur la toute première carte graphique de NVIDIA (la NV1).

La caméra : le point de vue depuis l'écran modifier

Outre les objets proprement dit, on trouve une caméra, qui représente les yeux du joueur. Cette caméra est définie par :

  • une position ;
  • par la direction du regard (un vecteur) ;
  • le champ de vision (un angle) ;
  • un plan qui représente l'écran du joueur ;
  • et un plan limite au-delà duquel on ne voit plus les objets.
 
Caméra.
 
Volume délimité par la caméra (view frustum).

Ce qui est potentiellement visible du point de vue de la caméra est localisé dans un volume, situé entre le plan de l'écran et le plan limite, appelé le view frustum. Suivant la perspective utilisée, ce volume n'a pas la même forme. Avec la perspective usuelle, le view frustum ressemble à un trapèze en trois dimensions, délimité par plusieurs faces attachées au bords de l'écran et au plan limite. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le view frustum est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.

Le culling et le clipping modifier

Un point important du rendu 3D est que ce qui n'est pas visible depuis la caméra ne doit idéalement pas être calculé. A quoi bon calculer des choses qui ne seront pas affichées, après tout ? Ce serait gâcher de la puissance de calcul. Et pour cela, les cartes graphiques et les moteurs graphiques incorporent de nombreuses optimisations pour éliminer les calculs inutiles. Diverses techniques de clipping ou de culling existent pour cela. La différence entre culling et clipping n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme culling.

 
View frustum culling : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.

La première forme de culling est le view frustum culling, dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du view frustum. Elle fait que ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Les autres formes de culling visent à éliminer ce qui est dans le view frustum, mais qui n'est pas visible depuis la caméra. Par exemple, il se peut que certains objets situés dans le view frustum ne soient pas visibles ou alors seulement partiellement. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. ces deux cas correspondent à deux types de culling. L'élimination des objets masqués par d'autres est appelé l'occlusion culling. L'élimination des parties arrières d'un objet est appelé le back-face culling. Enfin, certains objets qui sont trop loin ne sont tout simplement pas calculés et remplacé par du brouillard ou un autre artefact graphique, voire pas remplacé du tout.

 
Occlusion culling : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.

Pour résumer, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. Les cartes graphiques embarquent divers méthodes de culling pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le culling peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.

Les effets de brouillard modifier

Les effets de brouillard sont des effets graphiques assez intéressants. Ils sont nécessaires dans certains jeux vidéo pour l'ambiance (pensez à des jeux d'horreur comme Siletn Hill), mais ils ont surtout été utilisés pour économiser des calculs. Sur les vieux jeux vidéo, notamment sur d'anciennes consoles de jeux, les effets de brouillards étaient utilisés pour ne pas calculer les graphismes au-delà d'une certaine distance.

L'idée est de réduire la taille du view frustum, en faisant en sorte que le plan limite au-delà duquel on ne voit pas les objets soit assez proche de la caméra. Mais si le plan limite est trop proche, cela donnera une cassure dans le rendu qui se verra et sera non seulement inesthétique, mais pourrait aussi passer pour un bug. Pour masquer cette cassure, les programmeurs ajoutaient un effet de brouillard. Les objets au-delà du plan limite étaient totalement dans le brouillard, puis ce brouillard se réduisait progressivement en se rapprochant de la caméra, avant de s'annuler à partir d'une certaine distance.

La rastérisation et le pipeline graphique modifier

Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le lancer de rayons et la rasterization. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.

La raison est que le lancer de rayons demande beaucoup de puissance de calcul, sans compter que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent marginaux des compléments au rendu par rastérization. Tout cela explique que les jeux vidéo utilisent la rasterization.

La rasterization calcule une scène 3D intégralement, avant de faire des transformations pour n'afficher que ce qu'il faut à l'écran. Le calcul d'une image rendue en 3D passe par une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le pipeline graphique. Précisons cependant que la succession d'étape que nous allons voir n'est pas aussi stricte que vous pouvez le penser. En fait, il n'existe pas un pipeline graphique unique : chaque API 3D fait à sa sauce, le matériel ne respecte pas forcément cette succession d'étapes, et j'en passe. Cependant, toutes les API 3D modernes sont organisées plus ou moins de la même manière, ce qui fait que le pipeline que nous allons décrire colle assez bien avec les logiciels 3D anciens et modernes, ainsi qu'avec l'organisation des cartes graphiques (anciennes ou modernes).

Le cas le plus simple ne demande que trois étapes :

  • une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
  • une étape de traitement de la géométrie, qui gère tout ce qui a trait aux sommets et triangles ;
  • une étape de rastérisation qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
 
Pipeline graphique basique.

Voyons cela plus en détail. Précisons avant toute chose que nous allons omettre certaines étapes facultatives, afin de simplifier l’exposé. Par exemple, nous n'allons pas parler de l'étape de tesselation, qui permet de rajouter de la géométrie, ce qui sert à déformer les objets ou à augmenter leur réalisme.

Le traitement de la géométrie modifier

Le traitement de la géométrie se fait en plusieurs étapes.

La première étape est appelée étape de transformation effectue plusieurs changements de coordonnées pour chaque sommet.

  • Premièrement, elle place les objets au bon endroit dans la scène 3D, ce qui demande de mettre à jour les coordonnées de chaque sommet de chaque modèle. C'est la première étape de calcul : l'étape de transformation des modèles 3D.
  • Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de transformation de la caméra.
  • Ensuite, une correction de la perspective est effectuée, qui corrige la forme du view frustum : c'est l'étape de projection.

Lors de la seconde étape de traitement de la géométrie, les sommets sont éclairés dans une phase d'éclairage. Lors de cette phase, tout sommet se voit attribuer une couleur, qui définit son niveau de luminosité. Celui-ci indique si le sommet est éclairé ou est dans l'ombre.

Éventuellement, diverses opérations de culling sont ensuite effectuées à la fin de cette étape, notamment le clipping et le view frustum-culling.

 
Geometry pipeline.

Après toutes ces étapes, une fois que les sommets ont tous étés traités, ils sont assemblés en primitives, c'est à dire en triangles ou en carrés/rectangles. L'assemblage est absolument nécessaire pour la prochaine étape du pipeline graphique, qui manipule des primitives. Il s'agit de l'étape d'assemblage de primitives (primitive assembly).

L'étape de rastérisation modifier

 
Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.

L'étape de rastérisation effectue beaucoup de choses différentes, même pour faire un rendu basique. Elle fait le lien entre triangle et pixels, diverses opérations d'interpolation, plaque les textures, effectues divers tests de visibilité, mélange les couleurs, écrit dans le framebuffer.

Mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles, et nous devons les voir en même temps. Nous avons dit plus haut qu'il existait deux manières de lier les textures à la géométrie : la méthode directe et la méthode inverse (UV Mapping). Et les deux font que la rastérisation se fait de manière très différente.

L'étape de rastérisation avec placage de texture inverse modifier

Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés. Cette séparation en trois étapes est vraiment importante. Elle est commune à tous les API 3D modernes, en plus d'avoir une certaine logique. Après, les trois étapes en question sont elles-mêmes subdivisées en plusieurs étapes distinctes, qui s'enchainent dans un ordre bien précis.

Pour simplifier, la première étape s'occupe la traduction des formes (triangles) rendues dans une scène 3D en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est aussi lors de cette étape que sont appliquées certaines techniques de culling, qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres. Mais le principal n'est pas là.

Lors de cette étape, chaque pixel de l'écran se voit attribuer un ou plusieurs triangle(s). Pour mieux comprendre quels triangles sont associés à tel pixel, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant.

La rastérisation attribue un sommet à un sommet, on peut en déduire directement quelle texture appliquer sur ce pixel, quel texel. La rastérisation associe un triangle à chaque pixel, les coordonnées de texture des sommets de ce triangle sont aussi transférées au pixel. Une fois que la carte graphique sait quelles sont les coordonnées de texture associée au pixel, elle lit la texture avec ces coordonnées pour colorier le pixel. On associe donc un texel à chaque pixel, on travaille pixel par pixel.

Après l'étape de placage de textures, la carte graphique enregistre le résultat en mémoire. Lors de cette étape, divers traitements de post-processing sont effectués et divers effets peuvent être ajoutés à l'image. Uun effet de brouillard peut être ajouté, des tests de profondeur sont effectués pour éliminer certains pixels cachés, l'antialiasing est ajouté, on gère les effets de transparence, etc. Un chapitre entier sera dédié à ces opérations.

La rastérisation avec placage de texture direct modifier

La rastérisation avec placage de texture direct se fait d'une manière totalement différente. Avec elle, on n'associe pas une texture à un pixel, mais on fait l'inverse. Pour cela, la carte graphique parcourt les textures texel par texel. Pour chaque texel, elle récupère les coordonnées du sommet associé, puis détermine quel pixel est associé. Le placage de texture se fait donc en même temps que la rastérisation, ou avant, suivant comme on l'interprète.

L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. De plus, on est certain que chaque texture est accédée une seule fois lors du rendu, alors que la rastérisation accède à une même texture plusieurs fois.

Mais le désavantage est que certains texels qui n'auraient pas été rendus avec la rastérisation sont parcours et testés. De plus, déterminer quel pixel est associé à quel sommet est beaucoup plus complexe. Le clipping et le culling sont aussi plus complexes à implémenter.

Les API 3D modifier

De nos jours, le développement de jeux vidéo, ou tout simplement de tout rendu 3D, utilise des API 3D. Les API 3D les plus connues sont DirectX, OpenGL, et Vulkan. L'enjeu des API est de ne pas avoir à recoder un moteur de jeu différent pour chaque carte graphique ou ordinateur existant. Elles fournissent des fonctions qui effectuent des calculs bien spécifiques de rendu 3D, mais pas que. L'application de rendu 3D utilise des fonctionnalités de ces API 3D, qui elles-mêmes utilisent les autres intermédiaires, les autres maillons de la chaîne. Typiquement, ces API communiquent avec le pilote de la carte graphique et le système d'exploitation.

La description du pipeline graphique modifier

L'API ne fait pas que fournir des morceaux de code que les programmeurs peuvent utiliser. Elles fournissent des contraintes et des règles de programmation assez importantes. Notamment, elles décrivent le pipeline graphique à utiliser. Pour résumer, le pipeline graphique comprend plusieurs étapes : plusieurs étapes de traitement de la géométrie, une phase de rastérisation, puis plusieurs étapes de traitement des pixels. Une API 3D comme DirectX ou OpenGl décrète quelles sont les étapes à faire, ce qu'elles font, et l'ordre dans lesquelles il faut les exécuter. Cela signifie que le pipeline graphique varie suivant l'API. Par exemple les API récentes supportent certaines étapes comme l'étape de tesselation, qui sont absentes des API anciennes.

Pour donner un exemple, je vais prendre l'exemple d'OpenGL 1.0, une des premières version d'OpenGL, aujourd'hui totalement obsolète. Le pipeline décrit par penGL 1.0 est illustré ci-dessous. On voit qu'il implémente le pipeline graphique de base, avec une phase de traitement de la géométrie (per vertex operations et primitive assembly), la rastérisation, et les traitements sur les pixels (per fragment opertaions). On y voit la présence du framebuffer et de la mémoire dédiée aux textures, les deux étant soit séparées, soit placée dans la même mémoire vidéo. La display list est une liste de commande de rendu que la carte graphique doit traiter d'un seul bloc, chaque display list correspond au rendu d'une image, pour simplifier. Les étapes evaluator et pixel operations sont des étapes facultatives, qui ne sont pas dans le pipeline graphique de base, mais qui sont utiles pour implémenter certains effets graphiques.

 
Pipeline d'OpenGL 1.0

Le pipeline d'OpenGL 1.0 vu plus haut est très simple, comparé aux pipelines des API modernes. Pour comparaison, voici un schéma qui décrit le pipeline de DirextX 11. Vous voyez que le nombre d'étapes n'est pas le même, que les étapes elles-mêmes sont légèrement différentes, etc.

 
Pipeline de D3D 11

L'implémentation peut être logicielle ou matérielle modifier

La mise en œuvre d'une API 3D peut se faire en logiciel, en matériel ou les deux. En théorie, elles peuvent être implémentées en logiciel, le processeur peut faire tout le travail et envoyer le résultat à une carte d'affichage. Et c'était le cas avant l'invention des premières cartes accélératrices 3D. Le rôle des API 3D était de fournir des morceaux de code et un pipeline graphique, afin de simplifier le travail des développeurs, pas de déporter des calculs sur une carte accélératrice 3D.

D'ailleurs, OpenGl et Direct X sont apparues avant que les premières cartes graphiques grand public soient inventées. Par exemple, les accélérateurs 3D sont arrivés sur le marché quelques mois après la toute première version de Direct X, et Microsoft n'avait pas prévu le coup. OpenGL était lui encore plus ancien et ne servait pas initialement pour les jeux vidéos, mais pour la production d'images de synthèses et dans des applications industrielles (conception assistée par ordinateur, imagerie médicale, autres). OpenGL était l'API plébiscitée à l'époque, car elle était déjà bien implantée dans le domaine industriel, la compatibilité avec les différents OS de l'époque était très bonne, mais aussi car elle était assez simple à programmer.

Un tournant dans l'évolution d'OpenGL fut la sortie du jeu Quake, d'IdSoftware. Celui-ci pouvait fonctionner en rendu logiciel, mais le programmeur responsable du moteur 3D (le fameux John Carmack) ajouta une version OpenGL du jeu. Le fait que le jeu était programmé sur une station de travail compatible avec OpenGL faisait que ce choix n'était si stupide, même si aucune carte accélératrice de l'époque ne supportait OpenGL. C'était là un choix qui se révéla visionnaire. En théorie, le rendu par OpenGL aurait dû se faire intégralement en logiciel, sauf sur quelques rares stations de travail adaptées. Mais 3dfx ajouta un support minimal d'OpenGL, contenant uniquement les fonctionnalités nécessaires pour Quake. John Carmack le dit lui-même, dans le readme.txt de GLQuake :

«
Theoretically, glquake will run on any compliant OpenGL that supports the texture objects extensions, but unless it is very powerfull hardware that accelerates everything needed, the game play will not be acceptable. If it has to go through any software emulation paths, the performance will likely by well under one frame per second.

At this time (march ’97), the only standard opengl hardware that can play glquake reasonably is an intergraph realizm, which is a VERY expensive card. 3dlabs has been improving their performance significantly, but with the available drivers it still isn’t good enough to play. Some of the current 3dlabs drivers for glint and permedia boards can also crash NT when exiting from a full screen run, so I don’t recommend running glquake on 3dlabs hardware.

3dfx has provided an opengl32.dll that implements everything glquake needs, but it is not a full opengl implementation. Other opengl applications are very unlikely to work with it, so consider it basically a “glquake driver”.

»


Par la suite, de nombreux autres fabricants de cartes 3D firent la même chose que 3dfx. OpenGL et Direct 3D étaient partiellement implémentées en logiciel, car les cartes graphiques ne supportaient pas toutes les fonctionnalités de l'API. C'était l'époque du miniGL, des implémentations partielles d'OpenGL, fournies par les fabricants de cartes 3D, implémentées dans les pilotes de périphériques de ces dernières. Les fonctionnalités d'OpenGL implémentées dans ces pilotes étaient presque toutes exécutées en matériel, par la carte graphique. Un cap fût franchi avec la sortie de la fameuse Geforce 256, qui incorpora la phase de T&L (Transform & Lighting) d'OpenGL directement en hardware. La phase de traitement géométrique d'OpenGl était réalisée directement par la carte graphique, au lieu de l'être en logiciel.

Avec l'évolution du matériel, les pilotes de périphériques devinrent de plus en plus complets, au point de devenir des implémentations totales d'OpenGL. De nos jours, les API sont surtout implémentées en matériel, la quasi-totalité des fonctionnalités des API étant implémentées soit avec des shaders, soit avec des circuits non-programmables.

Mais au-delà d'OpenGL, chaque fabricant de carte graphique avait sa propre API propriétaire, qui était gérée par leurs pilotes de périphériques (drivers). Par exemple, les premières cartes graphiques de 3dfx interactive, les fameuses voodoo, disposaient de leur propre API graphique, l'API Glide. Elle facilitait la gestion de la géométrie et des textures, ce qui collait bien avec l'architecture de ces cartes 3D. Mais ces API propriétaires tombèrent rapidement en désuétude avec l'évolution de DirectX et d'OpenGL.

L'API impose des contraintes sur le matériel modifier

Plus haut, j'ai dit que les API imposent un ordonnancement précis, décrit par les étapes du pipeline. Le matériel doit respecter cet ordonnancement et faire comme si tout le pipeline graphique décrit par l'API était respecté. Je dis faire comme si, car il se peut que le matériel ne respecte pas cet ordre à la lettre. Il le fera dans les grandes lignes, mais il arrive que la carte graphique fasse certaines opérations en avance, comparé à l'ordre imposé par l'API, pour des raisons de performance. Typiquement, effectuer du culling ou les tests de profondeur plus tôt permet d'éviter de rejeter de nombreux pixels invisibles à l'écran, et donc d'éliminer beaucoup de calculs inutiles. Mais la carte graphique doit cependant corriger le tout de manière à ce que pour le programmeur, tout se passe comme l'API 3D l'ordonne. Les écarts à l'ordonnancement de l'API sont corrigés d'une manière ou une autre par le hardware, par des circuits spécialisés de remise en ordre.

De manière générale, sans même se limiter à l'ordonnancement des étapes du pipeline graphique, les règles imposées par les API 3D sont des contraintes fortes, qui contraignent les cartes graphiques dans ce qu'elles peuvent faire. De nombreuses optimisations sont rendues impossibles à cause des contraintes des API 3D.

L'architecture d'une carte 3D modifier

Toute carte graphique contient obligatoirement des circuits essentiels qui ne sont pas liés au rendu 3D proprement dit, mais sont nécessaires pour qu'elle fonctionne : de la mémoire vidéo, des circuits de communication avec le bus, des circuits d’interfaçage avec l'écran, et d'autres circuits. À cela, il faut ajouter les circuits pour le rendu 3D proprement dit, qui comprennent un ensemble hétérogène de circuits aux fonctions fort différentes. Les circuits non-liés au rendu 3D sont globalement les mêmes que pour une carte d'affichage 2D, aussi nous n'en reparlerons pas ici et nous allons nous concentrer sur les circuits du pipeline graphique.

Les architectures en mode immédiat et en tiles modifier

Il existe deux types de cartes graphiques : celles en mode immédiat, et celles avec un rendu en tiles. Les deux ont sensiblement les mêmes circuits, à quelques différences près, mais elles les utilisent d'une manière fort différente. Dans la suite de ce cours, nous parlerons surtout des cartes graphiques en mode immédiat, les architectures en tile étant reléguées à la fin du cours. Nous faisons ainsi car les cartes graphiques des ordinateurs de bureau ou portables sont toutes en mode immédiat, alors que les cartes graphiques des appareils mobiles, smartphones et autres équipements embarqués ont un rendu en tiles. Les raisons à cela sont multiples. La première est que les architectures en tiles sont considérées comme moins performantes que celles en mode immédiat, et elles sont d'autant moins performantes que la géométrie de la scène 3D est complexe. Les architectures en tiles sont donc utilisées pour les équipements où la performance n'est pas une priorité, comme les appareils mobiles, alors que le rendu en mode immédiat est utilisé pour les ordinateurs performants (de bureau ou portable). Par contre, le rendu en tiles est plus simple, plus facile à implémenter en matériel. Une autre caractéristique est un avantage du rendu en tile pour le rendu en 2D, comparé aux architectures en mode immédiat, qui se marie bien aux besoins des smartphones et autres objets connectés.

Mais expliquons donc quelle est la différence entre ces deux types de cartes graphiques. Avec le rendu en tiles, la géométrie est intégralement rendue avant de faire la rastérisation. De plus, la rastérisation découpe l'écran en l'écran en carrés/rectangles, appelés des tiles, qui sont rendus séparément, les unes après les autres. Une carte graphique en tile calcule toute la géométrie, mémorise le résultat du rendu géométrique en mémoire vidéo, découpe l'écran en tiles qui sont rastérisées et texturées une par une. Cette forme de rendu est compatible avec la séparation du pipeline en deux étapes : une étape de traitement géométrique, et une étape de rastérisation proprement dite. Mais les cartes graphiques en mode immédiat ne font pas cela : la rastérisation se fait pixel par pixel, pas par tiles.

Pour comprendre quelles sont les implications en termes de performance, nous allons séparer la carte graphique en deux circuits : un qui gère la géométrie, et un qui gère la rastérisation au sens large. Nous allons les appeler l'unité géométrique et le rastériseur, respectivement. Les deux lisent ou écrivent des données dans la mémoire vidéo. L'unité géométrique lit en mémoire vidéo la scène 3D, qui est mémorisée dans un tampon de sommets. De même, les textures sont lues dans la mémoire vidéo. Enfin, une fois qu'on a finit de calculer un pixel, il doit être enregistré dans le framebuffer. Cela est vrai pour les cartes graphiques en rendu immédiat et en rendu à tiles. Les cartes graphiques ajoutent souvent des mémoires caches pour la géométrie et les textures, afin de rendre leur accès plus rapide. Le tout est illustré ci-dessous. Nous n'avons pas parlé de la manière dont communiquent le rastériseur et l'unité géométrique, volontairement, car c'est là que se situe la différence entre les deux types de cartes 3D.

 
Carte graphique, généralités

Avec les cartes graphiques en mode immédiat, l'unité géométrique envoie ses résultats à l'unité de rastérisation, sans passer par la mémoire. Et là, un premier problème survient. Un triangle dans une scène 3D correspond généralement à plusieurs pixels. Ce qui fait qu'il y a un déséquilibre entre géométrie et rastérisation : la rastérisation prend plus de temps de calcul que la géométrie. Une conséquence est qu'il arrive fréquemment que le circuit de rastérisation soit occupé, alors que l'unité de géométrie veut lui envoyer des données. Dans ce cas, on doit mettre en attente les résultats géométriques dans une mémoire tampon, appelé le tampon de primitives. De plus, la gestion de la rastérisation demande d'utiliser des informations assez lourdes, qui sont mémorisées en mémoire vidéo, comme le z-buffer dont nous parlerons à la fin du cours.

 
Carte graphique en rendu immédiat

Avec les cartes graphiques en rendu à tiles, les choses sont totalement différentes. Premièrement, l'unité géométrique n'envoie pas ses résultats au rastériseur, mais mémorise le tout en mémoire vidéo. Plus précisément, les résultats sont enregistrés tile par tile. Le rastériseur lit alors la géométrie d'une tile depuis la mémoire vidéo, et la rastérise. L'avantage est que la tile est tellement petite qu'elle est intégralement rastérisée sans avoir à passer par la mémoire vidéo. En effet, le rastériseur incorpore une petite mémoire SRAM qui mémorise la tile et toutes les informations nécessaires pour effectuer le rendu. Pas besoin d’accéder à un gigantesque z-buffer pour toute l'image, juste besoin d'un z-buffer minuscule pour la tile en cours de traitement, qui tient totalement dans la SRAM pour la tile.

 
Carte graphique en rendu par tiles

Maintenant, faisons le bilan au niveau des accès mémoire. La performance d'une carte graphique est limitée par la quantité d'accès mémoire qu'on peut effectuer par seconde. Autant dire que les économiser est primordial. Avec une architecture en mode immédiat, le tampon de primitives évite d'avoir à passer par la mémoire vidéo. Par contre, la rastérisation utilise énormément d'accès mémoire pour rendre les pixels, notamment à cause de cette histoire de z-buffer. Peu d'accès mémoire liés à la géométrie, mais la rastérisation est gourmande en accès mémoire. Avec les architectures à tile, c'est l'inverse. Toute la géométrie est enregistré en mémoire vidéo, mais la rastérisation ne fait que très peu d'accès mémoire grâce à la SRAM pour les tiles. Au final, les deux architectures sont optimisées pour deux types de rendus différents. Les cartes à rendu en tuile brillent quand la géométrie n'est pas trop compliquée, mais que la rastérisation et les traitements des pixels sont lourds. Les cartes en mode immédiat sont elles douées pour les scènes géométriquement lourdes, mais avec peu d'accès aux pixels. Le tout est limité par divers caches qui tentent de rendre les accès mémoires moins fréquents, sur les deux types de cartes, mais sans que ce soit une solution miracle.

Un avantage des GPU en rendu à tile est que l’antialiasing est plus rapide. Pour ceux qui ne le savent pas, l'antialiasing est une technique qui améliore la qualité d’image, en simulant une résolution supérieure. Une image rendue avec antialiasing aura la même résolution que l'écran, mais n'aura pas certains artefacts liés à une résolution insuffisante. Et l'antialiasing a lieu intégralement dans les circuits situés dans l'étape de rastérisation ou après. Tout se passe donc dans le rastériseur. Les GPU en mode immédiat disposent de techniques d'optimisations pour l’antialiasing, mais il n’empêche que le GPU doit mémoriser des informations en mémoire vidéo pour faire un antialiasing correct. Alors qu'avec le rendu en tiles, tout l’antialiasing se passe dans le circuit de rastérisation, sans avoir à passer par la mémoire vidéo. La SRAM qui mémorise la tile en cours suffit pour l'antialiasing. L'antialiasing est donc plus rapide.

Les carte graphique modernes sont partiellement programmables modifier

Les cartes graphiques modernes sont capables d’exécuter des programmes informatiques, comme le ferait un processeur d'ordinateur normal. Les programmes informatiques exécutés par la carte graphique sont appelés des shaders. Les shaders sont classifiés suivant les données qu'ils manipulent. On parle de pixel shader pour ceux qui manipulent des pixels, de vertex shaders pour ceux qui manipulent des sommets, de geometry shader pour ceux qui manipulent des triangles, de hull shaders et de domain shaders pour la tesselation, de compute shader pour des opérations sans lien avec le rendu 3D, etc. Ils sont écrits dans un langage de haut-niveau, le HLSL pour les shaders Direct X et le GLSL pour les shaders Open Gl, puis sont ensuite traduit (compilés) en instructions machines compréhensibles par la carte graphique. Au début, ces langages et la carte graphique supportaient uniquement des opérations simples. Mais au fil du temps, les spécifications de ces langages sont devenues de plus en plus riches à chaque version de Direct X ou d'Open Gl, et le matériel en a fait autant. Les premiers shaders apparus étaient les vertex shaders, avec la Geforce 3 qui a rendu la gestion de la géométrie programmable. Par la suite, l'étape de traitement des pixels est elle aussi devenue programmable et les pixels shaders ont fait leur apparition. Puis d'autres formes de shaders sont apparues, pour effectuer des calculs géométriques complexes ou des calculs non-graphiques.

Les shaders sont exécutés par un ou plusieurs processeurs intégrés à la carte graphique, qui portent le nom de processeurs de shaders. Les cartes graphiques récentes contiennent un grand nombre de processeurs de shaders, plusieurs centaines. Mais une carte graphique moderne ne contient pas que des processeurs de shaders, mais aussi des circuits spécialisés dans le rendu 3D qui prennent en charge une étape du pipeline graphique, appelés des unités de traitement graphiques. On trouve ainsi une unité pour le placage de textures, une unité pour l'étape de transformation, une pour la gestion de la géométrie, une unité de rasterization, une unité d'enregistrement des pixels en mémoire appelée ROP, etc. Les unités sont divisés en deux catégories : d'un côté les circuits non-programmables (dits fixes) et de l'autre les circuits programmables (les processeurs de shaders). Globalement, la gestion de la géométrie et des pixels est programmable, mais la rasterization, le placage de texture, le culling et l'enregistrement du framebuffer ne l'est pas.

 
Pipeline 3D : ce qui est programmable et ce qui ne l'est pas dans une carte graphique moderne.

Outre les unités du paragraphe précédent, une carte graphique contient d'autres circuits qui s'occupent de la logistique, du transfert de données ou de la répartition du travail entre les unités de traitement proprement dites. Le tampon de primitive vu plus haut en est un exemple et d'autres mémoires tampon de ce type existent. L'un de ces circuits de logistique est le processeur de commandes, dont nous parlerons dans quelques chapitres. Pour simplifier, il commande les différentes étapes du pipeline graphique et s'assure que les étapes s’exécutent dans le bon ordre.

Ce qu'il faut comprendre, c'est qu'une carte graphique moderne est un mélange de circuits non-programmables et de circuits programmables. L'assemblage de circuits fixes et de processeurs de shaders peut sembler contradictoire. Pourquoi ne pas tout rendre programmable ? Ou au contraire, pourquoi ne pas utiliser seulement des circuits fixes ? Le choix entre les deux parait simple. Les circuits programmables sont légèrement plus lents, mais plus flexibles, plus simples à concevoir et facilitent la vie aux programmeurs. Par contre, les circuits fixes sont plus rapides, plus économes en énergie, utilisent moins de transistors, mais sont plus compliqués à concevoir et moins flexibles. Mélanger les deux semble étrange.

La réponse rapide est qu'il s'agit d'un compromis entre flexibilité et performance qui permet d'avoir le meilleur des deux mondes. Rendre la gestion de la géométrie ou des pixels programmable est très intéressant pour le programmeur, ce qui explique qu'on a des processeurs de shader pour les vertex shaders et pixel shaders. par contre, le programmeur ne gagne pas grand chose à avoir un rastériseur programmable, mais les performances de rastérisation sont cruciales. Aussi, ce circuits est laissé non-programmable et conçu avec un circuit fixe plus rapide. Dans la fin de ce chapitre, et dans le chapitre suivant, nous allons étudier comment cette recherche du compromis a donné naissance aux cartes graphiques modernes. Nous allons voir l'évolution dans le temps des cartes graphiques à la fin de ce chapitre. Dans le chapitre suivant, nous allons voir pourquoi les vertex shaders et les pixels shaders sont apparus, en prenant l'exemple des algorithmes d'éclairage.

Un historique simplifié du hardware des cartes graphiques modifier

Le hardware des cartes graphiques a fortement évolué dans le temps, ce qui n'est pas une surprise. Les évolutions de la technologie, avec la miniaturisation des transistors et l'augmentation de leurs performances a permit aux cartes graphiques d'incorporer de plus en plus de circuits avec les années. Avant l'invention des cartes graphiques, toutes les étapes du pipeline graphique étaient réalisées par le processeur : il calculait l'image à afficher, et l’envoyait à une carte d'affichage 2D. Au fil du temps, de nombreux circuits furent ajoutés, afin de déporter un maximum de calculs vers la carte vidéo. Les GPU actuels s'occupent de tous les traitements liés au moteur graphique.

Dès le début de la 3D, le choix entre circuits fixe et circuits programmables s'est fait ressentir. Les toutes premières cartes graphiques se classaient en deux camps : d'un côté les cartes graphiques complètement non-programmables, de l'autre des GPU totalement programmables. La toute première carte 3D était du second camp : c’était la Rendition Vérité V1000. Elle contenait un processeur MIPS cadencé à 25 MHz, 4 mébioctets de RAM, une ROM pour le BIOS, et un RAMDAC, rien de plus. C'était un vrai ordinateur complètement programmable de bout en bout. Les programmeurs ne pouvaient cependant pas utiliser cette programmabilité avec des shaders, mais elle permettait à Rnedition d'implémenter n'importe quelle API 3D, que ce soit son API propriétaire ou OpenGL, voire DirectX. Mais les performances s'en ressentaient. Réaliser la rastérisation et le placage de texture en logiciel n'est pas efficace, pareil pour les opérations de fin de pipeline comme l'antialiasing. Au final, sa programmabilité était un plus pour la compatibilité logicielle, mais qui avait un cout en termes de performances. Le manque d'unités fixes très rapides pour la rastérisation, le placage de texture ou les opérations de fin de pipeline était clairement un gros défaut.

Le second camp, celui des cartes graphiques totalement non-programmables et ne contenant que des circuits fixe, regroupe les Voodoo de 3dfx, les Riva TNT de NVIDIA, les Rage/3D d'ATI, la Virge/3D de S3, et la Matrox Mystique. Elles contenaient des circuits pour gérer les textures, quelques effets graphiques (brouillard) et l'étape d'enregistrement des pixels en mémoire. Par la suite, ces cartes s’améliorèrent en ajoutant plusieurs circuits de gestion des textures, pour colorier plusieurs pixels à la fois. Cela permettait aussi d'utiliser plusieurs textures pour colorier un seul pixel : c'est ce qu'on appelle du multitexturing. Les performances avec ces cartes étaient nettement meilleures que pour la Rendition V1000. Le fait d'avoir des circuits câblés rapides pour certaines tâches bien choisie donnait de bonnes performances, et personne n'avait la moindre utilité pour des circuits programmables à l'époque.

 
Carte 3D sans rasterization matérielle.

Les cartes suivantes ajoutèrent une gestion des étapes de rasterization directement en matériel. Les cartes ATI rage 2, les Invention de chez Rendition, et d'autres cartes graphiques supportaient ces étapes en hardware. De nos jours, ce genre d'architecture est commun chez certaines cartes graphiques intégrées dans les processeurs ou les cartes mères.

 
Carte 3D avec gestion de la géométrie.

La première carte graphique capable de gérer la géométrie fût la Geforce 256, la toute première Geforce. Son unité de gestion de la géométrie n'est autre que la bien connue T&L (Transform And Lighting). Elle implémentait des algorithmes d'éclairage de la scène 3D assez simples, comme un éclairage de Phong, qui étaient directement câblés dans ses circuits. Mais le défaut de cette approche était que ces cartes graphiques incorporaient des algorithmes d'éclairage très spécifiques, alors qu'il existe de très nombreux algorithmes d'éclairage, au point où on ne peut pas tous les mettre dans une carte graphique. Les programmeurs avaient donc le choix entre programmer les algorithmes d’éclairage qu'ils voulaient ou utiliser ceux de la carte graphique.

 
Carte 3D avec gestion de la géométrie.

À partir de la Geforce 3 de Nvidia, les unités de traitement de la géométrie sont devenues programmables. L'avantage était que les programmeurs n'étaient pas limités aux algorithmes implémentés dans la carte 3D pour obtenir de bonnes performances. Ils pouvaient programmer l'algorithme d'éclairage qu'ils voulaient et pouvaient le faire exécuter directement sur la carte graphique. Les unités de traitement de la géométrie deviennent donc des processeurs indépendants, capables d’exécuter des programmes appelés Vertex Shaders. Ces shaders sont écrits dans un langage de haut-niveau, le HLSL ou le GLSL, et sont traduits (compilés) par les pilotes de la carte graphique avant leur exécution. Au fil du temps, les spécifications de ces langages sont devenues de plus en plus riches et le matériel en a fait autant. D'autres types de shaders ont été inventés : pixels shaders pour le traitement de pixels, shaders de géométrie, shaders génériques, etc. Les premiers à faire leur apparition furent les pixel shaders, suivis par les geometry shaders et les shaders pour la tesselation.

Pour résumer, l'évolution des cartes 3D s'est faite d'une manière assez complexe. Le compromis entre circuits fixes et programmables a été difficile à trouver. Les concepteurs ont procédé par essais et erreurs, jusqu'à trouver le compromis actuel. Les premières cartes graphiques sont parties sur deux pistes totalement opposées : d'un côté une carte totalement programmable, de l'autre des cartes avec des circuits fixes non-programmables. L'échec de la Rendition Vérité 1000, totalement programmable mais avec de trop faibles performances, a orienté le marché dans la seconde direction. Les cartes 3D ont alors incorporé des circuits de rastérisation et de placage/filtrage de texture, qui sont toujours là de nos jours. Elles ont ensuite intégré des circuits de calculs géométrique, avant de revenir en arrière pour les remplacer par des circuits programmables. Leur programmabilité s'est ensuite accrue, en incorporant des processeurs capables d’exécuter n'importe quel algorithme, mais sans revenir sur les circuits fixes établis. Expliquer pourquoi demande cependant d'y passer un chapitre entier, ce qui est le sujet du chapitre suivant.