Les cartes graphiques/Version imprimable

Un livre de Wikilivres.
Aller à la navigation Aller à la recherche

Nuvola-inspired File Icons for MediaWiki-fileicon-ps.png

Ceci est la version imprimable de Les cartes graphiques.

  • Si vous imprimez cette page, choisissez « Aperçu avant impression » dans votre navigateur, ou cliquez sur le lien Version imprimable dans la boîte à outils, vous verrez cette page sans ce message, ni éléments de navigation sur la gauche ou en haut.
  • Cliquez sur Rafraîchir cette page pour obtenir la dernière version du wikilivre.
  • Pour plus d'informations sur les version imprimables, y compris la manière d'obtenir une version PDF, vous pouvez lire l'article Versions imprimables.


Les cartes graphiques

Une version à jour et éditable de ce livre est disponible sur Wikilivres,
une bibliothèque de livres pédagogiques, à l'URL :
https://fr.wikibooks.org/wiki/Les_cartes_graphiques

Vous avez la permission de copier, distribuer et/ou modifier ce document selon les termes de la Licence de documentation libre GNU, version 1.2 ou plus récente publiée par la Free Software Foundation ; sans sections inaltérables, sans texte de première page de couverture et sans Texte de dernière page de couverture. Une copie de cette licence est incluse dans l'annexe nommée « Licence de documentation libre GNU ».

Les cartes d'affichage

Les cartes graphiques sont des cartes qui s'occupent de communiquer avec l'écran, pour y afficher des images. Au tout début de l'informatique, ces opérations étaient prises en charge par le processeur : celui-ci calculait l'image à afficher à l'écran, et l'envoyait pixel par pixel à l'écran, ceux-ci étant affichés immédiatement après. Cela demandait de synchroniser l'envoi des pixels avec le rafraichissement de l'écran. Pour simplifier la vie des programmeurs, les fabricants de matériel ont inventé des cartes d'affichage, ou cartes vidéo. Avec celles-ci, le processeur calcule l'image à envoyer à l'écran, et la transmet à la carte d'affichage. Celle-ci prend alors en charge son affichage à l'écran, déchargeant le processeur de cette tâche.

Les cartes d'affichage en mode texte[modifier | modifier le wikicode]

Illustration du mode texte.

Les premières cartes graphiques fonctionnaient en mode texte, c'est à dire qu'elles traitaient des caractères et non des pixels. Il était impossible de modifier des pixels individuellement. À la place, la carte graphique pouvait modifier des paquets de pixels, des sortes de pavés rectangulaires de pixels. Chaque pavé affiche un caractère, ce qui induit que tous les caractères ont une taille fixe, que ce soit en largeur ou en hauteur. Par exemple, chaque caractère occupera 8 pixels de haut et 5 pixels de large. Ce mode texte est toujours présent dans nos cartes graphiques actuelles et est encore utilisé par le BIOS ou par les écrans bleus de Windows.

VGAText3.png

Les caractères sont des lettres, des chiffres, ou des symboles courants, même si des caractères spéciaux sont disponibles. Ceux-ci sont encodés dans un jeu de caractère spécifique (ASCII, ISO-8859, etc.). L'ensemble des caractères gérés par une carte graphique (y compris ceux créés par l'utilisateur) s'appelle la table des caractères. Certaines cartes graphiques permettent à l'utilisateur de créer ses propres caractères en modifiant cette table. La mémoire de caractères est une mémoire ROM/EEPROM. Dans cette mémoire, chaque caractère est représenté par une matrice de pixels, avec un bit par pixel.

Représentation d'un caractère à l'écran et dans la table de caractère.

Ces caractères se voient attribuer des informations en plus de leur code ASCII. À la suite de leur code ASCII, on peut trouver un octet qui indique si le caractère clignote, sa couleur, sa luminosité, si le caractère doit être souligné, etc. Une gestion minimale de la couleur est parfois présente. La carte graphique contient un circuit chargé de gérer les attributs des caractères : l'ATC (Attribute Controller), aussi appelé le contrôleur d'attributs.

Le tampon de texte est une mémoire dans laquelle les caractères à afficher sont placés les uns à la suite des autres. Chaque caractère est alors stocké en mémoire sous la forme d'un code ASCII suivi d'un octet d'attributs. Le processeur va envoyer les caractères à afficher un par un, ceux-ci étant accumulés dans le tampon de texte au fur et à mesure de leur arrivée.

Vient ensuite le CRTC (Cathod Ray Tube Controller) ou contrôleur de tube cathodique. Celui-ci gère l'affichage sur l'écran proprement dit. Sur les vieux écrans CRT, les pixels sont affichés les uns après les autres, ligne par ligne, en commençant par le pixel en haut à gauche. Pour une carte en mode texte, ce contrôleur va envoyer ces pixels les uns après les autres, en utilisant la mémoire de caractères et le tampon de texte. Pour cela, il se souvient du prochain pixel à afficher grâce à deux registres : un registre X pour la ligne, et un registre Y pour la colonne. Évidemment, nos deux registres de ligne et de colonne sont incrémentés régulièrement afin de passer au pixel suivant. Le CRTC va déduire à quel caractère cela correspond dans le tampon de texte avec quelques bits des registres X et Y, le lire et l'envoyer à la mémoire de caractères. Là, le code du caractère, et les bits restants des deux registres sont utilisés pour calculer l'adresse mémoire du pixel à afficher. Ce pixel est alors envoyé à l'écran.

Adressage des pixels dans le text buffer grâce au CRTC.

Enfin, le pixel à afficher est envoyé à l'écran. Ceci dit, les écrans assez anciens fonctionnent en analogiques et non en binaire, ce qui demande de faire une conversion. C'est le rôle du DAC, un convertisseur qui traduit des données binaires en données analogiques. Sur les écrans récents, ce DAC n'existe pas : les données sont envoyées sous forme numérique à l'écran, via une interface DVI ou autre, et sont automatiquement gérées par l'écran.

Architecture interne d'une carte d'affichage en mode texte

Les cartes d'affichage en mode graphique[modifier | modifier le wikicode]

Les cartes en mode texte ont rapidement été remplacées par des cartes graphiques capables de colorier chaque pixel de l'écran individuellement. Il s'agit d'une avancée énorme, qui permet beaucoup plus de flexibilité dans l'affichage. Ces cartes graphiques étaient conçues avec des composants assez similaires aux composants des cartes graphiques à mode texte. Divers composants sont toutefois modifiés.

  • La mémoire de caractères a évidemment disparu. Ou plutôt, elle est encore là, mais n'est pas active en mode graphique.
  • La mémoire vidéo stocke une image à afficher à l'écran, et non des caractères : on appelle cette mémoire vidéo le Frame Buffer.
  • Le CRTC peut gérer différentes résolutions, quelques registres permettant de configurer la résolution voulue.

La couleur fait son apparition. Toutefois, ces cartes graphiques codaient ces couleurs sur 4 bits, 8 bits, à la rigueur 16. Dans ces conditions, les couleurs n'étaient pas encodées au format RGB habituel : chaque couleur se voyait attribuer un numéro prédéterminé lors de la conception de la carte graphique. Vu que les écrans ne gèrent que des données au format RGB, ces cartes graphiques devaient effectuer la conversion en RGB avec un circuit, la Color Look-up Table. Dans le cas le plus simple, il s'agissait d'une ROM qui mémorisait la couleur RGB pour chaque numéro envoyé en adresse. Dans d'autres cas, cette mémoire était une RAM, ce qui permettait de modifier la palette au fur et à mesure : on pouvait ainsi changer les couleurs de la palette d'une application à l'autre sans aucun problème. Cette Color Look-up Table était alors fusionnée avec le DAC, et formait ce qu'on appelait le RAMDAC.

Architecture interne d'une carte d'affichage en mode graphique


Les cartes accélératrices 2D

Avec l'arrivée des interfaces graphiques (celles des systèmes d'exploitation, notamment) et des jeux vidéo 2D, les cartes graphiques ont pu s'adapter. Les cartes graphiques 2D ont d'abord commencé par accélérer le tracé et coloriage de figures géométriques simples, tels les lignes, segments, cercles et ellipses, afin d’accélérer les premières interfaces graphiques. Par la suite, diverses techniques d'accélération de rendu 2D se sont fait jour. La base d'un rendu en 2D est de superposer des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. Ces images sont appelées des sprites.

Les sprites sont superposées les uns au-dessus des autres, au bon endroit sur l'écran. Cette superposition se traduit par une copie des pixels de l'image aux bons endroits dans la mémoire, chaque sprite étant copié dans la portion de mémoire qui contient l'arrière-plan. Le rendu des sprites doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les sprites qui se superposent sur les autres) : on parle d'algorithme du peintre. Ce genre de copie arrive aussi lorsqu’on doit scroller, ou qu'un objet 2D se déplace sur l'écran.

Exemple de rendu 2D utilisant l'algorithme du peintre.

Ces techniques ne sont pas utiles que pour les jeux vidéo, mais peuvent aussi servir à accélérer le rendu d'une interface graphique. Après tout, les lettres, les fenêtres d'une application ou le curseur de souris sont techniquement des sprites qui sont superposés les uns au-dessus des autres. C'est ainsi que les cartes graphiques actuelles supportent des techniques d’accélération du rendu des polices d'écriture, une accélération du scrolling ou encore un support matériel du curseur de la souris, toutes dérivées des techniques d'accélération des sprites.

Le circuit de Blitter[modifier | modifier le wikicode]

Certaines cartes 2D ont introduit un composant pour accélérer les copies d'images en mémoire. Ce genre de copie arrive souvent lorsqu'on doit scroller, ou qu'un objet 2D se déplace sur l'écran. Déplacer une fenêtre sur votre bureau est un bon exemple : le contenu de ce qui était présent sur l'écran doit être déplacé vers le haut ou vers le bas. Dans la mémoire vidéo, cela correspond à une copie des pixels correspondant de leur ancienne position vers la nouvelle. Cela a aussi des applications dans les jeux en 2D. La base d'un rendu en 2D, c'est de superposer des images les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. Ces images sont superposées sur l’arrière-plan au bon endroit sur l'écran, ce qui se traduit par une copie des pixels de l'image aux bons endroits dans la mémoire. Ce circuit chargé des copies s'appelle le blitter. Sans blitter, les copies étaient donc à la charge du processeur, qui devait déplacer lui-même les données en mémoire. Le blitter est conçu pour ce genre de tâches, sauf qu'il n'utilise pas le processeur.

La gestion des masques[modifier | modifier le wikicode]

Ceci dit, un blitter possède d'autres fonctionnalités. Il peut effectuer une opération bit à bit entre les données à copier et une donnée fournie par le programmeur. Pour voir à quoi cela peut servir, reprenons notre exemple du jeu 2D, basé sur une superposition d'images. Les images des différents personnages sont souvent des images rectangulaires. Par exemple, l'image correspondant à notre bon vieux pacman ressemblerait à celle-ci. Évidemment, cette image s'interface mal avec l’arrière-plan. Avec un arrière-plan blanc, les parties noires de l'image du pacman se verraient à l'écran.

Image de Pacman.

L'idéal serait de ne pas toucher à l’arrière-plan sur les pixels noirs de pacman, et de ne modifier l’arrière-plan que pour les pixels jaunes. Ceci est possible en fournissant un masque, une image qui indique quels pixels modifier lors d'un transfert, et quels sont ceux qui ne doivent pas changer. Grâce à ce masque, le blitter sait quels pixels modifier. Le blitter prend l'image du pacman, le morceau de l’arrière-plan auquel on superpose pacman, et le masque. Pour chaque pixel, il effectue l'opération suivante : ((arrière-plan) AND (masque)) OR (image de pacman). Au final, l'image finale est bel et bien celle qu'on attend.

Masque de Pacman.

L'accélération matérielle des sprites[modifier | modifier le wikicode]

Avec d'autres cartes 2D, les sprites ne sont pas copiés sur un arrière-plan préexistant. À la place, c'est la carte graphique qui décidera d'afficher les pixels de l’arrière-plan ou du sprite pendant l'envoi des pixels à l'écran, lors du balayage effectué par le CRTC. Pour cela, les sprites sont stockés dans des registres ou des RAM. Pour chaque RAM /sprite, on trouve trois registres permettant de mémoriser la position du sprite à l'écran : un pour sa coordonnée X, un autre pour sa coordonnée Y, et un autre pour sa profondeur (pour savoir celui qui est superposé au-dessus de tous les autres). Lorsque le CRTC demande à afficher le pixel à la position (X , Y), chaque triplet de registres de position est comparé à la position X,Y fournie par le CRTC. Si aucun sprite ne correspond, les mémoires des sprites sont déconnectées du bus et le pixel affiché est celui de l'arrière-plan. Dans le cas contraire, la RAM du sprite est connectée sur le bus et son contenu est envoyé au RAMDAC. Si plusieurs sprites doivent s'afficher en même temps, le bus choisit celui dans la profondeur est la plus faible (celui superposé au-dessus de tous les autres).

Sprites matériels.

Cette technique a autrefois été utilisée sur les anciennes bornes d'arcade, ainsi que sur certaines consoles de jeu bon assez anciennes. Mais de nos jours, elle est aussi présente dans les cartes graphiques actuelles dans un cadre particulièrement spécialisé : la prise en charge du curseur de la souris, ou le rendu de certaines polices d'écritures ! Les cartes graphiques contiennent un ou plusieurs sprites, qui représentent chacun un curseur de souris, et deux registres, qui stockent les coordonnées x et y du curseur. Ainsi, pas besoin de redessiner l'image à envoyer à l'écran à chaque fois que l'on bouge la souris : il suffit de modifier le contenu des deux registres, et la carte graphique place le curseur sur l'écran automatiquement. Pour en avoir la preuve, testez une nouvelle machine sur laquelle les drivers ne sont pas installés, et bougez le curseur : lag garantit !


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éos utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération de ces jeux vidéos en 3D, les fabricants de cartes graphiques se sont adaptés. Ils ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer les calculs effectués pour rendre une scène 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 parler rapidement de comment les jeux vidéos font pour calculer une scène en 3D. Ce rendu en 3 dimensions, aussi appelé rendu 3D, est en effet ce qui est pris en charge par les cartes accélératrices 3D, totalement ou partiellement. Nous allons donc commencer par quelques rapides rappels sur les bases du rendu 3D.

Les bases du rendu 3D[modifier | modifier le wikicode]

Une scène 3D est composée d'un espace en trois dimensions, dans laquelle le moteur physique du 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 de 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 | modifier le wikicode]

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éos actuels, seuls les triangles sont utilisés. Les objets sont donc modélisés par un assemblage de triangles collés les uns aux autres. Cet assemblage de triangles/carrés/polygones est ce qu'on appelle un 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 que les triangles, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.

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

Les polygones sont définis par leurs sommets, aussi appelés vertices dans le domaine du rendu 3D. Dans ce qui suit, nous utiliseront indifféremment les termes vertice et sommet. 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 vertices. Ensuite, les sommets sont reliés entre eux. Le 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. Concrètement, les seules face actuellement utilisées en rendu 3D sont les triangles et les carrés (quad). 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.

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, amis aussi comment les relier. Il faut savoir quel sommet appartient à tel triangle. 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. Les manières de structurer ces informations sont nombreuses, mais nous en dirons plus dans le chapitre sur le rendu de la géométrie.

Rendu en fil de fer d'un modèle 3D complexe.

Tout objet à rendre en 3D est donc composé d'un assemblage de polygones, généralement des triangles. Le rendu 3D normal travaille sur des triangles, qui sont éclairés et coloriés par divers algorithmes. Une partie de ce traitement tient à gérer la lumière dans la scène, ce qui permet de colorier un triangle en nuances de gris, représentant sa luminosité. Pour rajouter de la couleur, ces objets sont recouverts par des textures, des images qui servent de papier peint à un objet. Un objet géométrique est donc éclairé, puis recouverts par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief. Précisons cependant qu'il est possible de se passer des textures et du coloriage des triangles en s’arrêtant au tracé des arêtes et sommets. On obtient alors un rendu en fil de fer, (rendu wireframe en anglais). L'effet est particulier et a peu été utilisé dans les jeux vidéos.

La caméra : le point de vue depuis l'écran[modifier | modifier le wikicode]

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 frustrum).

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. Il porte le nom de view frustrum. Suivant la perspective utilisée, ce volume n'a pas la même forme. Avec la perspective usuelle, le view frustrum 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 frustrum est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours.

Un point important du rendu 3D est que ce qui est en-dehors du view frustrum n'est pas affiché à l'écran et ne doit pas être calculé ou rendu. Mais il se peut que certains objets situés dans le view frustrum 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. Toujours est-il que ce qui n'est pas affiché ne doit pas être calculé ou rendu et 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.

Il existe plusieurs grands types de culling. Le premier est le view frustrum culling, dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du view frustrum, ce qui n'est pas dans le champ de vision de la caméra. Le second type est l'élimination des objets masqués par d'autres, appelé l'occlusion culling. Le troisième type est l'élimination des parties arrières d'un objet. Ne pas calculer la géométrie de ces faces n'est pas possible, mais on peut abandonner les calculs d'éclairage ou de placage de texture.

View frustum culling : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.
Occlusion culling : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.

Pour le view frustrum culling, un soin particulier doit être pris pour les triangles dont une partie seulement est dans le champ de vision. Ils doivent être découpés en plusieurs triangles, tous présents intégralement dans le champ de vision. Les algorithmes pour ce faire sont assez nombreux et il serait compliqué d'en faire une liste exhaustive, aussi nous laissons le sujet de côté pour le moment.

Clipping/View frustrum culling dans le cadre d'un écran de forme carrée (en gris).

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.

Le pipeline graphique[modifier | modifier le wikicode]

Le 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éos. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. La raison est qu'il demande beaucoup de puissance de calcul, ce qui le rend peu adapté aux jeux vidéos. De plus, le matériel pour accélérer le lancer de rayon n'est pas très efficace et n'a pas pris commercialement. Ce qui explique que depuis un bon moment, les jeux vidéos utilisent la rasterization. Celle-ci calcule une scène 3D intégralement, avant de faire des transformations pour n'afficher que ce qu'il faut à l'écran.

Il existe deux sous-types de rasterization : le rendu en mode immédiat, et le rendu en tiles. La plupart des cartes graphiques des ordinateurs de bureau ou des ordinateurs portables utilisent la première méthode. Le rendu en tiles est surtout utilisé sur les équipements mobiles ou embarqués, qui fonctionnent à basse performance et à basse consommation. Une des raisons est un avantage dy rendu en tile pour le rendu en 2D, comparé aux architectures en mode immédiat. En général, les cartes graphiques utilisant le rendu en tiles consomment moins d'énergie et sont plus rapides que leur équivalent en mode immédiat pour le rendu 2D, ce qui en fait un avantage certain pour les appareils mobiles.

Le rendu en mode immédiat[modifier | modifier le wikicode]

Avec le rendu en mode immédiat, le calcul de l'image finale passe par une suite d'étapes consécutives, l'ensemble étant appelé le pipeline graphique. Le cas le plus simple ne demandant que trois étapes :

  • une étape de traitement de la géométrie, qui gère tout ce qui a trait aux sommets et triangles ;
  • une étape de rasterization, qui détermine quelle partie de l'image 3D s'affiche à l'écran, et qui attribue chaque sommet/triangle à un pixel donné de l'écran ;
  • une étape de traitement des pixels, classiquement divisée en une étape de placage de textures et une étape d'enregistrement des pixels en mémoire.

Dans certains cas, des traitements supplémentaires sont ajoutés. Mais ces étapes rajoutées peuvent être vues comme des subdivisions des trois étapes précédentes. Par exemple, les cartes graphiques modernes supportent une étape de tesselation, qui permet de rajouter de la géométrie. Cela permet de déformer les objets ou d'augmenter leur réalisme. Cette étape est classiquement rangée dans le traitement de la géométrie. Tout cela pour dire que l'organisation en trois étapes est très pédagogique, en plus d'être terriblement efficace.

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

  • La première étape place les objets au bon endroit dans la scène 3D. Lors de la modélisation d'un objet, celui-ci est encastré dans un cube : un sommet du cube possède la coordonnée (0, 0, 0), et les vertices de l'objet sont définies à partir de celui-ci. Pour placer l'objet dans la scène, il faut tenir compte de sa localisation, calculée par le moteur physique : si le moteur physique a décrété que l'objet est à l'endroit de coordonnées (50, 250, 500), toutes les coordonnées des vertices de l'objet doivent être modifiées. Pendant cette étape, l'objet peut subir une translation, une rotation, ou un gonflement/dégonflement (on peut augmenter ou diminuer sa taille). C'est la première étape de calcul : l'étape de transformation.
  • Ensuite, les vertices sont éclairées dans une phase d'éclairage. Chaque vertice se voit attribuer une couleur, qui définit son niveau de luminosité : est-ce que la vertice est fortement éclairée ou est-elle dans l'ombre ?
  • Vient ensuite une phase de traitement de la géométrie, où les vertices sont assemblées en triangles, points, ou lignes, voire en polygones. Ces formes géométriques de base sont ensuite traitées telles quelles par la carte graphique. Sur les cartes graphiques récentes, cette étape peut être gérée par le programmeur : il peut programmer les divers traitements à effectuer lui-même.
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.

Vient ensuite la traduction des formes (triangles) rendues dans une scène 3D en un affichage pixelisé à l'écran. Cette étape de rasterization détermine à quoi ressemble la scène visible sur l'écran. 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é au pixel correspondant. C'est aussi lors de cette étape que sont appliquées certaines techniques de culling, qui éliminent les portions non-visibles de l'image.

À la suite de cela, les textures sont appliquées sur la géométrie. La carte graphique sait à quel triangle correspond chaque pixel et peut donc colorier le pixel en question en fonction de la couleur de la texture appliquée sur la géométrie. C'est la phase de placage de textures. Sur les cartes graphiques récentes, cette étape peut être gérée par le programmeur : il peut programmer les divers traitements à effectuer lui-même. En plus de cela, les pixels de l'écran peuvent subir des traitements divers et variés avant d'être enregistrés et affichés à l'écran. Après l'étape de placage de textures, la carte graphique enregistre le résultat en mémoire. Lors de cette étape, divers traitements sont effectués et divers effets peuvent être ajoutés à l'image : un effet de brouillard peut être ajouté, des tests de visibilité sont effectués pour éliminer certains pixels cachés, l'antialiasing est ajouté, etc.

Le rendu en tiles[modifier | modifier le wikicode]

Le rendu en tiles est différent sur deux points. Premièrement, l'écran est découpé en rectangles qui sont rendus séparément, là où le rendu en mode immédiat rend l'écran comme un tout qui est traité pixel par pixel. Les rectangles en question sont appelés des tiles, d'où le nom donné à la méthode. Ensuite, le rendu n'est pas composé d'une suite d'étapes consécutives. Avec le rendu en mode immédiat, les calculs se font au fil de l'eau. La carte 3D calcule une partie de la géométrie puis envoie le résultat à l'étape de rasterisation, avant d'en traiter les pixels associés. Mais avec le rendu en tiles, la géométrie est intégralement rendue avant que le traitement des pixels commence. Le résultat du calcul de la géométrie est mémorisé en mémoire vidéo, avant de démarrer la seconde étape de traitement des pixels.

La différence entre les deux est assez importante pour comprendre les avantages et inconvénients de chaque méthode. Les cartes graphiques des ordinateurs de bureau ou des ordinateurs portables sont toutes en rendu en mode immédiat. Mais les cartes graphiques des appareils mobiles, smartphones ou d'autres équipements embarqués sont de type "rendu en tiles". Les raisons à cela sont la performance. 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. Par contre, le rendu en tiles est plus simple et plus facile à implémenter en matériel. Les architectures en tiles sont donc utilisées pour es é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).

Le principal défaut du rendu en tiles est que le rendu se fait en deux passes, avec une mémorisation du résultat de la première passe en mémoire vidéo. Et cette mémorisation demande beaucoup de lectures et d'écritures : d'écritures pour mémoriser le résultat de la première passe, de lectures pour l'utiliser dans la seconde passe. La mémoire vidéo est donc beaucoup utilisée et doit avoir un débit suffisant. Ce qui est un désavantage pour les cartes graphiques à haute performance. L'usage de mémoires cache compense cependant encore plus le désavantage pour les architectures à tiles. Le rendu en mode immédiat et en tiles permettent tous deux l'utilisation de mémoires caches, notamment pour ce qui est des textures et des sommets. Mais le rendu en tiles permet d'utiliser des caches pour les étapes finales du traitement des pixels, là où le rendu en mode immédiat ne le peut pas vraiment. L'idée est d'utiliser un cache capable de mémoriser un tile complet, ce qui permet de finaliser le calcul du tile dans le cache et d'écrire uniquement le résultat final en mémoire vidéo. Le rendu en mode immédiat ne permet pas ce genre de facéties.

Un autre avantage des architectures en tiles est qu'elles permettent d'éliminer rapidement les portions non-affichées de la scène 3D. Le rendu en mode immédiat a certes des techniques de culling assez avancées. Mais elles sont effectuées dans l'étape de rasterization, ce qui est assez tardif comparé à ce qu'on observe sur les architectures à tiles, pour diverses raisons techniques.

Finalement, ce qui est économisé d'un côté est gaspillé de l'autre et tout est histoire de compromis. De plus, diverses optimisations spécifiques à chaque approche permettent d'éliminer des lectures/écritures "superflues", ce qui complexifie la comparaison entre les deux approches.

L'architecture d'une carte 3D en mode immédiat[modifier | modifier le wikicode]

Avant l'invention des cartes graphiques, toutes ces é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 circuits d'une carte graphique varie grandement selon son degré de sophistication. Les premières cartes graphiques ne faisaient pas grand chose et le processeur ne leur déléguait que peu de calculs. Mais avec l'évolution des technologies, de plus en plus de calculs de rendu ont été délégués à la carte graphique. Cela s'en ressent sur l'organisation des circuits des cartes graphiques. Si certains circuits sont toujours présents sur toutes les cartes graphiques, d'autres ont étés ajoutés progressivement, au fil des années.

Répartition du travail entre processeur et GPU, sur les cartes graphiques récentes. On voit que le GPU s'occupe des traitements liés au moteur graphique, tandis que les autres traitements (son, physique) sont pris en charge par le processeur.

Toute carte graphique contient obligatoirement 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. A cela, il faut ajouter les circuits pour le rendu 3D proprement dit. Ces derniers sont regroupés dans un ensemble hétérogène de circuits aux fonctions forts différentes. Il est composé de circuits non-programmables (dits fixes) et de circuits programmables. Conceptuellement, on sépare les circuits fixes et programmables dans deux sous-pipelines séparés, ce qui permet de faire la distinction entre pipeline programmable qui regroupe les circuits programmables et le pipeline fixe pour le reste. Mais cette distinction est purement conceptuelle et ne correspond pas vraiment à la manière dont les circuits sont organisés réellement. Le pipeline graphique est secondé par des circuits qui s'occupent de lire/écrire des textures et vertices depuis la mémoire vidéo. Ces deux circuits portent le nom d'unité de texture et d'input assembler.

Architecture de base d'une carte 3D - 1

Les circuits essentiels[modifier | modifier le wikicode]

La mémoire vidéo est nécessaire pour stocker l'image à afficher à l'écran, mais aussi pour mémoriser temporairement des informations importantes. Dans le cas le plus simple, elle sert simplement de Framebuffer : elle stocke l'image à afficher à l'écran. Au fil du temps, elle s'est vu ajouter d'autres fonctions, comme stocker les textures et les vertices de l'image à calculer, ainsi que divers résultats temporaires. Elle est très proche des mémoires RAM qu'on trouve sous forme de barrettes dans nos PC, à quelques différences près. En premier lieu, la mémoire vidéo peut supporter un grand nombre d'accès mémoire simultanés. Ensuite, elle est optimisée pour accéder à des données proches en mémoire.

La carte graphique communique via un bus, un vulgaire tas de fils qui connectent la carte graphique à la carte mère. Les premières cartes graphiques utilisaient un bus nommé ISA, qui fût rapidement remplacé par le bus PCI, plus rapide. Viennent ensuite le bus AGP, puis le bus PCI-Express. Ce bus est géré par un contrôleur de bus, un circuit qui se charge d'envoyer ou de réceptionner les données sur le bus. Les circuits de communication avec le bus permettent à l'ordinateur de communiquer avec la carte graphique, via le bus PCI-Express, AGP, PCI ou autre. Il contient quelques registres dans lesquels le processeur pourra écrire ou lire, afin de lui envoyer des ordres du style : j'envoie une donnée, transmission terminée, je ne suis pas prêt à recevoir les données que tu veux m'envoyer, etc.

Le circuit d’interfaçage avec l'écran permet à la carte graphique d'envoyer à l'écran une image à afficher. Enfin, on trouve naturellement des circuits qui s'occupent des rendus 2D et 3D proprement dit.

Les circuits du pipeline fixe[modifier | modifier le wikicode]

Toute carte graphique contient des circuits, aussi appelés unités, qui prennent en charge une étape du pipeline graphique. Entre les différentes unités, on trouve souvent des mémoires pour mettre en attente les vertices ou les pixels, au cas où une unité est trop occupée. Pour plus d'efficacité, ces cartes graphiques possédaient parfois plusieurs unités de traitement des vertices et des pixels, ou plusieurs ROP. Dans ce cas, ces unités multiples sont précédées par un circuit qui se charge de répartir les vertex ou pixels sur chaque unités. Généralement, ces unités sont alimentées en vertex/pixels les unes après les autres (principe du round-robin).

Tout les traitements que la carte graphique doit effectuer, qu'il s'agisse de rendu 2D, de calculs 2D, du décodage matérielle d'un flux vidéo, ou de calculs généralistes, sont envoyés par le pilote de la carte graphique, sous la forme de commandes. L'envoi des données à la carte graphique ne se fait pas immédiatement : il arrive que la carte graphique n'ait pas fini de traiter les données de l'envoi précédent. Il faut alors faire patienter les données tant que la carte graphique est occupée. Les pilotes de la carte graphique vont les mettre en attente dans une file (une zone de mémoire dans laquelle on stocke des données dans l'ordre d'ajout) : le tampon de commandes. Ensuite, ces commandes sont interprétées par un circuit spécialisé : le processeur de commandes. Celui-ci est chargé de piloter les circuits de la carte graphique.

Les toutes premières cartes graphiques contenaient simplement des circuits pour gérer les textures, en plus de la mémoire RAM vidéo. Seules l'étape de texturing, quelques effets graphiques (brouillard) et l'étape d'enregistrement des pixels en mémoire étaient prises en charge par la carte graphique. 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 cartes graphiques construites sur cette architecture sont très anciennes. On parle des cartes graphiques ATI rage, 3DFX Voodoo, Nvidia TNT, etc.

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 commune 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 simples, comme un éclairage de Phong, qui étaient directement câblés dans ses circuits. Nous étudierons d'ailleurs cette unité et les algorithmes qu'elle utilise dans quelques chapitres.

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

Les circuits du pipeline programmable : vertex et pixels shaders[modifier | modifier le wikicode]

À partir de la Geforce 3 de Nvidia, les unités de traitement de la géométrie sont devenues programmables. Cela permet une grande flexibilité, à savoir que changer le comportement ne nécessite pas de re-câbler tout le circuit. 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. Par la suite, l'étape de traitement des pixels est elle aussi devenue programmable et les pixels shaders (programmes de traitement de pixels) ont fait leur apparition. Par la suite, d'autres types de shaders ont été inventés : shaders de géométrie, shaders génériques, etc. 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.

Les premières cartes graphiques avaient des jeux d'instructions séparés pour les unités de vertex shader et les unités de pixel shader, et les processeurs étaient séparés. Pour donner un exemple, c'était le cas de la Geforce 6800. Cette séparation entre unités de texture et de vertices était motivée par le fait que les unités de vertice n’accédaient jamais à la mémoire, contrairement aux unités de traitement de pixels qui doivent accéder aux textures.

Carte 3D avec pixels et vertex shaders non-unifiés.

Depuis DirectX 10, ce n'est plus le cas : le jeu d'instructions a été unifié entre les vertex shaders et les pixels shaders, ce qui fait qu'il n'y a plus de distinction entre processeurs de vertex shaders et de pixels shaders, chaque processeur pouvant traiter indifféremment l'un ou l'autre. Cela a un avantage considérable, car tous les jeux vidéo n'ont pas les mêmes besoins. Certains vont avoir une géométrie très développées mais peu de besoin en termes de textures ou d'effets de post-processing. Ceux là gagnent à avoir beaucoup d'unités de gestion de la géométrie et peu d'unités de traitement des pixels. A l'inverse, d'autres jeux vidéos ont besoin de beaucoup de puissance de calcul pour traiter les pixels, mais arrivent à économiser énormément sur la géométrie. L'usage de shaders unifiés permet d'adapter la répartition entre vertex shaders et pixels shaders suivant les besoins de l'application, là où la séparation entre unités de vertex et de pixel ne le permettait pas.

Carte 3D avec pixels et vertex shaders unfifiés.


Le multi-GPU

Illustration du multi-GPU où deux cartes graphiques communiquent via un lien indépendant du bus PCIExpress. On voit que le débit du lien entre les deux cartes graphique est ajouté au débit du bus PCIExpress.

Combiner plusieurs cartes graphiques dans un PC pour gagner en performances est la base des techniques dites de multi-GPU, tels le SLI et le Crossfire. Ces technologies sont surtout destinées aux jeux vidéos, même si les applications de réalité virtuelle, l'imagerie médicale haute précision ou les applications de conception par ordinateur peuvent en tirer profit. C'est ce genre de choses qui se cachent derrière les films d'animation : Pixar ou Disney ont vraiment besoin de rendre des images très complexes, avec beaucoup d'effets. Et ne parlons pas des effets spéciaux créés par ordinateur. Contrairement à ce qu'on pourrait penser, le multi-GPU n'est pas une technique récente. Pensez donc qu'en 1998, il était possible de combiner dans un même PC deux cartes graphiques Voodoo 2, de marque 3dfx (un ancien fabricant de cartes graphiques, aujourd'hui disparu). Autre exemple : dans les années 2006, le fabricant de cartes graphiques S3 avait introduit cette technologie pour ses cartes graphiques Chrome.

Le multi-GPU peut se présenter sous plusieurs formes, la plus simple consistant à placer plusieurs GPU sur une même carte graphique. Mais il est aussi possible d'utiliser plusieurs cartes graphiques séparées, connectées à la carte mère via PCI-Express. Si les deux cartes ont besoin d’échanger des informations, les transferts passent par le bus PCI-Express ou par un connecteur qui relie les deux cartes (ce qui est souvent plus rapide). Il n'y a pas de différences de performances avec la solution utilisant des cartes séparées reliées avec un connecteur. Tout le problème des solutions multi-GPU est de répartir les calculs sur plusieurs cartes graphiques, ce qui est loin d'être chose facile. Il existe diverses techniques, chacune avec ses avantages et ses inconvénients, que nous allons aborder de suite.

Split Frame Rendering[modifier | modifier le wikicode]

Le Split Frame Rendering découpe l'image en morceaux, qui sont répartis sur des cartes graphiques différentes. Ce principe a été décliné en plusieurs versions, et nous allons les passer en revue. Nous pouvons commencer par faire la différence entre les méthodes de distribution statiques et dynamiques. Avec les méthodes statiques, la manière de découper l'image est toujours la même : celle-ci sera découpée en blocs, en lignes, en colonnes, etc; de la même façon quelque soit l'image. Avec les techniques dynamiques, le découpage s'adapte en fonction de la complexité de l'image. Nous allons commencer par aborder les méthodes statiques.

Scanline interleave[modifier | modifier le wikicode]

Historiquement, la première technique multi-GPU fût utilisée par les cartes graphiques Voodoo 2. Avec cette technique, chaque carte graphique calculait une ligne sur deux, la première carte rendait les lignes paires et l'autre les lignes impaires. On peut adapter la technique à un nombre arbitraire de GPU, en faisant calculer par chaque GPU une ligne sur 3, 4, 5, etc. Cette technique s'appelait le Scan Line Interleave.

Scanline interleave

Cette technique avait un avantage certain quand la résolution des images était limitée par la quantité de mémoire vidéo, ce qui était le cas de la Voodoo 2, qui ne pouvait pas dépasser une résolution de 800 * 600. Avec le scan line interleave, les deux framebuffers des deux cartes étaient combinés en un seul framebuffer plus gros, capable de supporter des résolutions plus élevées. Cette technique a toutefois un gros défaut : l’utilisation de la mémoire vidéo n'est pas optimale. Comme vous le savez, la mémoire vidéo sert à stocker les objets géométriques de la scène à rendre, les textures, et d'autres choses encore. Avec le scan line interleave, chaque objet et texture est présent dans la mémoire vidéo de chaque carte graphique. Il faut dire que ces objets et textures sont assez grands : la carte graphique devant rendre une ligne sur deux, il est très rare qu'un objet doive être rendu totalement par une des cartes et pas l'autre. Avec d'autres techniques, cette consommation de mémoire peut être mieux gérée.

Checker board[modifier | modifier le wikicode]

La technique du Checker Board découpe l'image non en lignes, mais en carrés de plusieurs pixels. Dans le cas le plus simple, les carrés ont une taille fixe, de 16 pixels de largeur par exemple. Si les carrés sont suffisamment gros, il arrive qu'ils puissent contenir totalement un objet géométrique. Dans ces conditions, une seule carte graphique devra calculer cet objet géométrique et charger ses données, qui ne seront donc pas dupliquées dans les deux cartes. Le gain en terme de mémoire peut être appréciable si les blocs sont suffisamment gros. Mais il arrive souvent qu'un objet soit à la frontière entre deux blocs : il doit donc être rendu par les deux cartes, et sera stocké dans les deux mémoires vidéos.

Pour plus d'efficacité, on peut passer d'un découpage statique, où tous les carrés ont la même taille, à un découpage dynamique, dans lequel on découpe l'image en rectangles dont la longueur et la largeur varient. En faisant varier le mieux possible la taille et la longueur de ces rectangles, on peut faire en sorte qu'un maximum de rectangles contiennent totalement un objet géométrique. Le gain en terme de mémoire et de rendu peut être appréciable. Néanmoins, découper des blocs dynamiquement est très complexe, et le faire efficacement est un casse-tête pour les développeurs de drivers.

Screen spiting[modifier | modifier le wikicode]

Il est aussi possible de simplement couper l'image en deux : la partie haute de l'image ira sur un GPU, et la partie basse sur l'autre. Cette technique peut être adaptée avec plusieurs GPU, en découpant l'image en autant de parties qu'il y a de GPU. Vu que de nombreux objets n'apparaissent que dans une portion de l'image, le drivers peut ainsi répartir les données de l'objet pour éviter toute duplication entre cartes graphiques. Cela demande du travail au driver, mais cela en vaut la peine, le gain en terme de mémoire étant appréciable.

Screen spliting

Le découpage de l'image peut reposer sur une technique statique : la moitié haute de l'image pour le premier GPU, et le bas pour l'autre. Ceci dit, quelques complications peuvent survenir dans certains jeux, les FPS notamment, où le bas de l'image est plus chargé que le haut. C'est en effet dans le bas de l'image qu'on trouve un sol, des murs, les ennemis, ou d'autres objets géométriques complexes texturés, alors que le haut représente le ciel ou un plafond, assez simple géométriquement et aux textures simples. Ainsi, le rendu de la partie haute sera plus rapide que celui du bas, et une des cartes 3D finira par attendre l'autre.

Mieux répartir les calculs devient alors nécessaire. Pour cela, on peut choisir un découpage statique adapté, dans lequel la partie haute envoyée au premier GPU est plus grande que la partie basse. Cela peut aussi être fait dynamiquement : le découpage de l'image est alors choisi à l’exécution, et la balance entre partie haute et basse s'adapte aux circonstances. Comme cela, si vous voulez tirer une roquette sur une ennemi qui vient de prendre un jumper (vous ne jouez pas à UT ou Quake ?), vous ne subirez pas un gros coup de lag parce que le découpage statique était inadapté. Dans ce cas, c'est le driver qui gère ce découpage : il dispose d'algorithmes plus ou moins complexes capables de déterminer assez précisément comment découper l'image au mieux. Mais il va de soit que ces algorithmes ne sont pas parfaits.

Alternate Frame Rendering[modifier | modifier le wikicode]

L'alternate Frame Rendering consiste à répartir des images complètes sur les différents GPUs. Dans sa forme la plus simple, un GPU calcule une image, et l'autre GPU calcule la suivante en parallèle. Les problèmes liés à la répartition des calculs entre cartes graphiques disparaissent alors. Cette technique est supportée par la majorité des cartes graphiques actuelles. Cette technique a été inventé par ATI, sur ses cartes graphiques Rage Fury, afin de faire concurrence à la Geforce 256. Évidemment, on retrouve un vieux problème présent dans certaines des techniques vues avant : chaque objet géométrique devra être présent dans la mémoire vidéo de chaque carte graphique, vu qu'elle devra l'afficher à l'écran. Il est donc impossible de répartir les différents objets dans les mémoires des cartes graphiques. Mais d'autres problèmes peuvent survenir.

Un des défauts de cette approche est le micro-stuttering. Dans des situations où le processeur est peu puissant, les temps entre deux images peuvent se mettre à varier très fortement, et d'une manière beaucoup moins imprévisible. Le nombre d'images par seconde se met à varier rapidement sur de petites périodes de temps. Alors certes, on ne parle que de quelques millisecondes, mais cela se voit à l’œil nu. Cela cause une impression de micro-saccades, que notre cerveau peut percevoir consciemment, même si le temps entre deux images est très faible. Suivant les joueurs, des différences de 10 à 20 millisecondes peuvent rendre une partie de jeu injouable. Pour diminuer l'ampleur de ce phénomène, les cartes graphiques récentes incorporent des circuits pour limiter la casse. Ceux-ci se basent sur un principe simple : pour égaliser le temps entre deux images, et éviter les variations, le mieux est d’empêcher des images de s'afficher trop tôt. Si une image a été calculée en très peu de temps, on retarde son affichage durant un moment. Le temps d'attente idéal est alors calculé en fonction de la moyenne du framerate mesuré précédemment.

Ensuite, il arrive que deux images soient dépendantes les unes des autres : les informations nées lors du calcul d'une image peuvent devoir être réutilisées dans le calcul des images suivantes. Cela arrive quand des données géométriques traitées par la carte graphique sont enregistrées dans des textures (dans les Streams Out Buffers pour être précis), dans l'utilisation de fonctionnalités de DirectX ou d'Open GL qu'on appelle le Render To Texture, ainsi que dans quelques autres situations. Évidemment, avec l'AFR, cela pose quelques problèmes : les deux cartes doivent synchroniser leurs calculs pour éviter que l'image suivante rate des informations utiles, et soit affichée n'importe comment. Sans compter qu'en plus, les données doivent être transférées dans la mémoire du GPU qui calcule l'image suivante.


La hiérarchie mémoire d'un GPU

Dans ce chapitre, nous allons voir comment est organisée la mémoire d'un GPU, ou plutôt devrait-on dire les mémoires d'un GPU. Et oui : un GPU contient beaucoup de mémoires différentes. La hiérarchie mémoire des GPUs est assez particulière, que ce soit au niveau des caches ou de la mémoire, parfois des registres.

Les mémoires d'un GPU[modifier | modifier le wikicode]

Un GPU contient évidemment une mémoire vidéo, de grande taille, capable de stocker textures, vertices, images et bien d'autres choses nécessaires pour un rendu 3D. On y trouve souvent des caches dédiés aux textures ou aux vertices, et les GPUs récents contiennent aussi des caches L1 et L2 de faible taille. Mais sur les cartes graphiques récentes, les caches sont complétés par des Local Store, des mémoires RAM qui servent de cache, mais fonctionnent comme des mémoires RAM normales.

Hiérarchie mémoire typique d'un GPU récent.

La mémoire vidéo[modifier | modifier le wikicode]

La mémoire vidéo est nécessaire pour stocker l'image à afficher à l'écran, mais aussi pour mémoriser temporairement des informations importantes. Dans le cas le plus simple, elle sert simplement de Framebuffer : elle stocke l'image à afficher à l'écran. Au fil du temps, elle s'est vu ajouter d'autres fonctions, comme stocker les textures et les vertices de l'image à calculer, ainsi que divers résultats temporaires.

Elle est très proche des mémoires RAM qu'on trouve sous forme de barrettes dans nos PC, à quelques différences près. En premier lieu, la mémoire vidéo peut supporter un grand nombre d'accès mémoire simultanés. Ensuite, elle est optimisée pour accéder à des données proches en mémoire.

Les anciennes cartes graphique pouvaient lire ou écrire directement dans la mémoire RAM, grâce à certaines fonctionnalités du bus AGP. Mais généralement, les données sont copiées depuis la mémoire RAM vers la mémoire vidéo, en passant par le bus. Cette copie est effectuée par un circuit spécialisé : le contrôleur DMA, qui permet d'échanger des données entre mémoire vidéo et mémoire RAM sans devoir utiliser le processeur. Il est souvent intégré dans le contrôleur de bus.

Les caches d'un CPU[modifier | modifier le wikicode]

Les cartes graphiques sont censées avoir peu de caches. Les premières cartes graphiques n'avaient qu'un cache de texture et éventuellement un cache de vertices, avec un cache d’instruction. Cette situation a perduré durant très longtemps, l'usage de caches plus complexe n'étant pas vraiment utile sur les cartes graphiques. Ce n'est que par la suite, quand les GPU commencèrent à être utilisés pour du calcul généraliste (scientifique, notamment), que la situation changea. Les GPU utilisèrent alors de plus en plus de caches généralistes. La hiérarchie mémoire des GPU ressemblent de plus en plus à celle des CPU, du moins pour les caches. On y trouve toute une hiérarchie de caches, avec des caches L1, L2, L3, etc.

Les Local Store des processeurs de shaders[modifier | modifier le wikicode]

En plus d'utiliser des caches, les processeurs de flux utilisent des local stores, des mémoires RAM intermédiaires entre la RAM principale et les caches/registres. Typiquement, chaque processeur de flux possède sa propre mémoire locale. Ces local stores peuvent être vus comme des caches, mais que le programmeur doit gérer manuellement. La faible capacité de ces mémoires, tout du moins comparé à la grande taille de la mémoire vidéo, les rend utile pour stocker temporairement des résultats de calcul "peu imposants". L'utilité principale est donc de réduire le trafic avec la mémoire centrale, les écritures de résultats temporaires étant redirigés vers les local stores.

Local stores d'un GPU.

Les processeurs de shaders récents sont des processeurs de flux[modifier | modifier le wikicode]

Beaucoup de processeurs graphiques actuels sont des processeurs de flux, aussi appelés stream processors. Ce sont des processeurs SIMD qui utilisent une hiérarchie de registres. Voici à quoi ressemble l'architecture d'un Stream Processor :

Stream processor

Les bancs de registres locaux et globaux[modifier | modifier le wikicode]

On voit que les processeurs de flux ont plusieurs bancs de registres. On trouve d'abord quelques bancs de registres locaux (Local Register File), directement connectés aux unités de calcul. Ces derniers sont reliés à un banc de registres plus gros, le banc de registre global, qui sert d'intermédiaire entre la mémoire RAM et les bancs de registres locaux. La différence entre les bancs de registres locaux/globaux et un cache vient du fait que les caches sont souvent gérés par le matériel, tandis que ces bancs de registres sont gérés via des instructions machines. Le processeur dispose d'instructions pour transférer des données entre les bancs de registres ou entre ceux-ci et la mémoire. Leur gestion peut donc être déléguée au logiciel, qui saura les utiliser au mieux.

Outre son rôle d'intermédiaire, le banc de registre global sert à transférer des données entre les bancs de registres locaux, où à stocker des données globales utilisées par des Clusters d'ALU différents. Les transferts de données entre la mémoire et le Global Register File ressemblent fortement à ceux qu'on trouve sur les processeurs vectoriels. Un processeur de flux possède quelques instructions capables de transférer des données entre ce Global Register File et la mémoire RAM. Et on trouve des instructions capables de travailler sur un grand nombre de données simultanées, des accès mémoires en Stride, en Scatter-Gather, etc.

Registres d'un Stream processor.

L'utilité de cette organisation[modifier | modifier le wikicode]

On peut se demander pourquoi utiliser plusieurs couches de registres ? Le fait est que les processeurs de flux disposent d'une grande quantité d'unités de calcul. Et cela peut facilement aller à plus d'une centaine ou d'un millier d'ALU ! Si on devait relier toutes cas unités de calcul à un gros banc de registres, celui-ci serait énorme, lent, et qui chaufferait beaucoup trop. Pour garder un banc de registres rapide et pratique, on est obligé de limiter le nombre d'unités de calcul connectées dessus, ainsi que le nombre de registres contenus dans le banc de registres. La solution est donc de casser notre gros banc de registres en plusieurs plus petits, reliés à un banc de registres plus gros, capable de communiquer avec la mémoire. Ainsi, nos unités de calcul vont aller lire ou écrire dans un banc de registres local très rapide.


Le processeur de commandes

Une carte graphique est un périphérique comme un autre, connecté sur la carte mère (sauf pour certaines cartes graphiques intégrées). La carte graphique accélère les jeux vidéos, les applications de conception assistée par ordinateur (solidworks), ou de rendu d'images 3D (blender, maya, etc). Elle peut aussi accélérer le traitement de l'affichage 2D : essayez d'utiliser Windows sans pilote de carte graphique, vous verrez de quoi je parle.

Pour déléguer ses calculs à la carte 3D, l'application pourrait communiquer directement avec la carte graphique, en écrivant dans ses registres et dans sa mémoire vidéo. Seul problème : le programme ne pourra communiquer qu'avec un ou deux modèles de cartes, et la compatibilité sera presque inexistante. Pour résoudre ce problème, les concepteurs de systèmes d'exploitations et de cartes graphiques ont inventé des API 3D, des bibliothèques qui fournissent des "sous-programmes" de base, des fonctions, que l'application pourra exécuter au besoin. De nos jours, les plus connues sont DirectX, et OpenGL. Les fonctions de ces APIs vont préparer des données à envoyer à la carte graphique, avant que le pilote s'occupe de les communiquer à la carte graphique.

Dans ce chapitre, nous allons voir ce que fait le pilote de la carte graphique, avant de voir comment la carte graphique traite les demandes envoyées. Nous allons parler du pilote de la carte graphique, de ce qu'il fait, de ses fonctions. Puis, nous allons voir l'interface entre le logiciel et les circuits de rendu 3D de la carte graphique : le processeur de commandes. Ce processeur de commandes est un circuit qui sert de chef d'orchestre, qui pilote le fonctionnement global de la carte graphique.

Le pilote de carte graphique[modifier | modifier le wikicode]

Le pilote de la carte graphique est un logiciel qui s'occupe de faire l'interface entre le logiciel et la carte graphique. De manière générale, les pilotes de périphérique sont des intermédiaires entre les applications/APIs et le matériel. En théorie, le système d'exploitation est censé jouer ce rôle, mais il n'est pas programmé pour être compatible avec tous les périphériques vendus sur le marché. A la place, le système d'exploitation ne gère pas directement certains périphériques, mais fournit de quoi ajouter ce qui manque. Le pilote d'un périphérique sert justement à ajouter ce qui manque : ils ajoutent au système d'exploitation de quoi reconnaitre le périphérique, de quoi l'utiliser au mieux. Pour une carte graphique, il a diverses fonctions que nous allons voir ci-dessous.

Avant toute chose, précisons que les systèmes d'exploitation usuels (Windows, Linux, MacOsX et autres) sont fournis avec un pilote de carte graphique générique, compatible avec la plupart des cartes graphiques existantes. Rien de magique derrière cela : toutes les cartes graphiques vendues depuis plusieurs décennies respectent des standards, comme le VGA, le VESA, et d'autres. Et le pilote de base fournit avec le système d'exploitation est justement compatible avec ces standards minimaux. Mais le pilote ne peut pas profiter des fonctionnalités qui sont au-delà de ce standard. L'accélération 3D est presque inexistante avec le pilote de base, qui ne sert qu'à faire du rendu 2D (de quoi afficher l’interface de base du système d'exploitation, comme le bureau de Windows). Et même pour du rendu 2D, la plupart des fonctionnalités de la carte graphique ne sont pas disponibles. Par exemple, certaines résolutions ne sont pas disponibles, notamment les grandes résolutions. Ou encore, les performances sont loin d'être excellentes. Si vous avez déjà utilisé un PC sans pilote de carte graphique installé, vous avez certainement remarqué qu'il était particulièrement lent.

La gestion des interruptions[modifier | modifier le wikicode]

Une fonction particulièrement importante est la réponse aux interruptions lancées par la carte graphique. Pour rappel, les interruptions sont une fonctionnalité qui permet à un périphérique de communiquer avec le processeur, tout en économisant le temps de calcul de ce dernier. Typiquement, lorsqu'un périphérique lance une interruption, le processeur stoppe son travail et exécute automatiquement une routine d'interruption, un petit programme qui s'occupe de gérer le périphérique, de faire ce qu'il faut, ce que l'interruption a demandé. Il y a plusieurs interruptions possibles, chacune ayant son propre numéro et sa fonction dédiée, chacune ayant sa propre routine d'interruption. Après tout, la routine qui gère un débordement de la mémoire vidéo n'est pas la même que celle qui gère un changement de fréquence du GPU ou la fin du calcul d'une image 3D. Le pilote fournit l'ensemble des routines d'interruptions nécessaires pour gérer la carte graphique.

La compilation des shaders[modifier | modifier le wikicode]

Le pilote de carte graphique est aussi chargé de traduire les shaders en code machine. En soi, cette étape est assez complexe, et ressemble beaucoup à la compilation d'un programme informatique normal. Les shaders sont écrits dans un langage de programmation de haut niveau, comme le HLSL ou le GLSL. Mais ce n'est pas ce code source qui est transmis au pilote de carte graphique. À la place, les shaders sont pré-compilés vers un langage dit intermédiaire, avant d'être compilé par le pilote en code machine. Le langage intermédiaire, comme son nom l'indique, sert d'intermédiaire entre le code source écrit en HLSL/GLSL et le code machine exécuté par la carte graphique. Il ressemble à un langage assembleur, mais reste malgré tout assez générique pour ne pas être un véritable code machine. Par exemple, il y a peu de limitations quant au nombre de processeurs ou de registres. L'avantage de cette méthode est que les optimisations importantes ont déjà été réalisées lors de la pré-compilation du code source vers le code intermédiaire, et le pilote a peu de choses à faire pour traduire le langage intermédiaire en code machine. Il doit bien faire la traduction, ajouter quelques optimisations de bas niveau par-ci par-là, mais rien de bien gourmand en processeur. Autant dire que cela économise plus le processeur que si on devait compiler complètement les shaders à chaque exécution.

Fait amusant, il faut savoir que le pilote peut parfois remplacer les shaders d'un jeu vidéo à la volée. Les pilotes récents embarquent en effet des shaders alternatifs pour les jeux les pus vendus et les plus gourmands. Lorsque vous lancez un de ces jeux vidéo et que le shader originel s'exécute, le pilote le détecte automatiquement et le remplace par la version améliorée, fournie par le pilote. Évidemment, le shader alternatif du pilote est optimisé pour le matériel adéquat. Cela permet de gagner en performance, voire en qualité d'image, sans pour autant que les concepteurs du jeu n'aient quoique ce soit à faire.

Enfin, certains shaders sont fournis par le pilote pour d'autres raisons. Par exemple, les cartes graphiques récentes n'ont pas de circuits fixes pour traiter la géométrie. Autant les anciennes cartes graphiques avaient des circuits de T&L qui s'en occupaient, autant tout cela doit être émulé sur les machines récentes. Sans cette émulation, les vieux jeux vidéos conçus pour exploiter le T&L et d'autres technologies du genre ne fonctionneraient plus du tout. Émuler les circuits fixes disparus sur les cartes récentes est justement le fait de shaders, présents dans le pilote de carte graphique.

Le tampon de commandes[modifier | modifier le wikicode]

Le pilote de la carte graphique gère aussi la mémoire de la carte graphique : où placer les textures, les vertices, et les différents buffers de rendu.

L'envoi des données à la carte graphique ne se fait pas immédiatement : il arrive que la carte graphique n'ait pas fini de traiter les données de l'envoi précédent. Il faut alors faire patienter les données tant que la carte graphique est occupée. Les pilotes de la carte graphique vont les mettre en attente dans une portion de la mémoire : le tampon de commandes. Ce tampon de commandes est ce qu'on appelle une file, une zone de mémoire dans laquelle on stocke des données dans l'ordre d'ajout. Si le tampon de commandes est plein, le driver n'accepte plus de demandes en provenance des applications. Un tampon de commandes plein est généralement mauvais signe : cela signifie que la carte graphique est trop lente pour traiter les demandes qui lui sont faites. Par contre, il arrive que le tampon de commandes soit vide : dans ce cas, c'est simplement que la carte graphique est trop rapide comparé au processeur, qui n'arrive alors pas à donner assez de commandes à la carte graphique pour l'occuper suffisamment.

L'arbitrage de l'accès à la carte graphique[modifier | modifier le wikicode]

Il n'est pas rare que plusieurs applications souhaitent accéder en même temps à la carte graphique. Imaginons que vous regardez une vidéo en streaming sur votre navigateur web, avec un programme de cloud computing de type Folding@Home qui tourne en arrière-plan, sur Windows. Le décodage de la vidéo est réalisé par la carte graphique, Windows s'occupe de l'affichage du bureau et des fenêtres, le navigateur web doit afficher tout ce qui est autour de la vidéo (la page web), le programme de cloud computing va lancer ses calculs sur la carte graphique, etc. Des situations de ce genre sont assez courantes et c'est le pilote qui s'en charge.

Autrefois, de telles situations étaient gérées simplement. Chaque programme avait accès à la carte graphique durant quelques dizaines ou centaines de millisecondes, à tour de rôle. Si le programme finissait son travail en moins de temps que la durée impartie, il laissait la main au programme suivant. S’il atteignait la durée maximale allouée, il était interrompu pour laisser la place au programme suivant. Et chaque programme avait droit d'accès à la carte graphique chacun à son tour. Un tel algorithme en tourniquet est très simple, mais avait cependant quelques défauts. De nos jours, les algorithmes d'ordonnancement d'accès sont plus élaborés, bien qu'il soit difficile de trouver de la littérature ou des brevets sur le sujet.

Les autres fonctions[modifier | modifier le wikicode]

Le pilote a aussi bien d'autres fonctions. Par exemple, il s'occupe d'initialiser la carte graphique, de fixer la résolution, d'allouer la mémoire vidéo, de gérer le curseur de souris matériel, etc.

Le processeur de commandes[modifier | modifier le wikicode]

Le pilote de la carte graphique envoie des commandes à la carte graphique, commandes qui sont gérées par le processeur de commandes. Ces commandes demandent à la carte graphique d'effectuer une opération 2D, ou une opération 3D, ou encore le rendu d'une vidéo.

Les commandes graphiques[modifier | modifier le wikicode]

Les commandes graphiques en question varient beaucoup selon la carte graphique. Les commandes sont régulièrement revues et chaque nouvelle architecture a quelques changements par rapport aux modèles plus anciens. Des commandes peuvent apparaitre, d'autres disparaitre, d'autre voient leur fonctionnement légèrement altéré, etc. Mais dans les grandes lignes, on peut classer ces commandes en quelques grands types. Les commandes les plus importantes s'occupent de l'affichage 3D : afficher une image à partir de paquets de vertices, préparer le passage d'une image à une autre, changer la résolution, etc. Plus rare, d'autres commandes servent à faire du rendu 2D : afficher un polygone, tracer une ligne, coloriser une surface, etc. Sur les cartes graphiques qui peuvent accélérer le rendu des vidéos, il existe des commandes spécialisées pour l’accélération des vidéos. Enfin, d'autres commandes servent pour la synchronisation avec le CPU (on verra cela plus tard).

Pour donner quelques exemples, prenons les commandes 2D de la carte graphique AMD Radeon X1800.

Commandes 2D Fonction
PAINT Peindre un rectangle d'une certaine couleur
PAINT_MULTI Peindre des rectangles (pas les mêmes paramètres que PAINT)
BITBLT Copie d'un bloc de mémoire dans un autre
BITBLT_MULTI Plusieurs copies de blocs de mémoire dans d'autres
TRANS_BITBLT Copie de blocs de mémoire avec un masque
NEXTCHAR Afficher un caractère avec une certaine couleur
HOSTDATA_BLT Écrire une chaine de caractère à l'écran ou copier une série d'image bitmap dans la mémoire vidéo
POLYLINE Afficher des lignes reliées entre elles
POLYSCANLINES Afficher des lignes
PLY_NEXTSCAN Afficher plusieurs lignes simples
SET_SCISSORS Utiliser les scissors ?
LOAD_PALETTE Charger la palette pour affichage 2D

Le processeur de commandes[modifier | modifier le wikicode]

Le processeur de commande est un circuit qui gère les commandes envoyées par le processeur. En soi, le processeur de commande est un circuit assez compliqué. Sur les cartes graphiques anciennes, c'était un circuit séquentiel complexe, fabriqué à la main et était la partie la plus complexe du processeur graphique. Sur les cartes graphiques modernes, c'est un véritable microcontrôleur, avec un processeur, de la mémoire RAM, etc.

Le processeur de commandes récupère les commandes dans le tampon de commande, en mémoire RAM, pour les recopier dans la mémoire vidéo et/ou une mémoire interne. Cette copie se fait via la technologie DMA, une technologie de transfert de données entre mémoire RAM et périphérique qui n'utilise pas le processeur principal. Une fois la copie faite, le processeur de commande décode la commande et l’exécute sur la carte graphique. Il garde en mémoire l'état de traitement de chaque commande : est-ce qu'elle est en train de s’exécuter sur le processeur graphique, est-ce qu'elle est mise en pause, est-ce qu'elle attend une donnée en provenance de la mémoire, est-ce qu'elle est terminée, etc. De plus, le processeur de commande peut communiquer avec le processeur via ce qu'on appelle des interruptions (les mêmes interruptions qui permettent à un périphérique d'interrompre le processeur pour exécuter une routine de traitement). Cela sert pour signaler qu'une commande s'est terminée ou a échouée, mais ce n'est pas la seule utilité de ce mécanisme.

La fonction principale, sur les cartes modernes, est de répartir le travail entre les différents circuits. Le processeur de commande décide à quel processeur ou circuit doit être envoyé une commande. Cela est très important sur les cartes graphiques qui gèrent plusieurs commandes simultanées : répartir plusieurs commandes sur plusieurs processeurs est une tâche difficile.

Parallélisme et synchronisation avec le CPU[modifier | modifier le wikicode]

Sur les cartes graphiques modernes, le processeur de commandes peut démarrer une commande avant que les précédentes soient terminées. Il est même possible que la carte graphique puisse exécuter plusieurs commandes en même temps, dans des circuits séparés. Par exemple, il est possible d’exécuter une commande ne requérant que des calculs, en même temps qu'une commande qui ne fait que faire des copies en mémoire : les deux commandes utilisent des circuits différents. En soi, exécuter plusieurs commandes en même temps permet un gain de performances et une meilleure utilisation du processeur graphique. Si une commande n'utilise que 70% du processeur graphique, alors on peut remplir les 30% restants avec une seconde commande. Évidemment, le processeur de commande doit être modifié pour permettre ce genre d'optimisation : il doit gérer plusieurs commandes en exécution, gérer plusieurs tampons de commandes, etc. De plus, cette parallélisation du processeur de commandes a un désavantage : celui-ci doit gérer les synchronisations entre commandes.

Avec un processeur de commande gérant le parallélisme, celui-ci doit gérer les synchronisations entre commandes. Par exemple, imaginons que Direct X décide d'allouer et de libérer de la mémoire vidéo. Direct X et Open GL ne savent pas quand le rendu de l'image précédente se termine. Comment éviter d'enlever une texture tant que les commandes qui l'utilisent ne sont pas terminées ? Ce problème ne se limite pas aux textures, mais vaut pour tout ce qui est placé en mémoire vidéo.

De manière générale, Direct X et Open GL doivent savoir quand une commande se termine. Un moyen pour éviter tout problème serait d'intégrer les données nécessaires à l'exécution d'une commande dans celle-ci : par exemple, on pourrait copier les textures nécessaires dans chacune des commandes. Mais cela gâche de la mémoire, et ralentit le rendu à cause des copies de textures. Les cartes graphiques récentes incorporent des commandes de synchronisation : les fences. Ces fences vont empêcher le démarrage d'une nouvelle commande tant que la carte graphique n'a pas fini de traiter toutes les commandes qui précèdent la fence. Pour gérer ces fences, le tampon de commandes contient des registres, qui permettent au processeur de savoir où la carte graphique en est dans l’exécution de la commande.

Un autre problème provient du fait que les commandes se partagent souvent des données, et que de nombreuses commandes différentes peuvent s'exécuter en même temps. Or, si une commande veut modifier les données utilisées par une autre commande, il faut que l'ordre des commandes soit maintenu : la commande la plus récente ne doit pas modifier les données utilisées par une commande plus ancienne. Pour éviter cela, les cartes graphiques ont introduit des instructions de sémaphore, qui permettent à une commande de bloquer tant qu'une ressource (une texture) est utilisée par une autre commande.

Commandes de synchronisation Fonction
NOP Ne rien faire
WAIT_SEMAPHORE Attendre la synchronisation avec un sémaphore
WAIT_MEM Attendre que la mémoire vidéo soit disponible et inoccupée par le CPU


Les unités de gestion de la géométrie

Nous allons maintenant voir les circuits chargés de gérer la géométrie. Il existe deux grands types de circuits chargés de traiter la géométrie : l'input assembler charge les Sur les cartes graphiques assez anciennes, ce cache est souvent très petit, à peine 30 à à 50 sommets. Pour profiter le plus possible de ce cache, les concepteurs de jeux vidéo peuvent changer l'ordre des sommets en mémoire.s depuis la mémoire vidéo, et les circuits de traitement de vertices les traitent. Ceux-ci effectuent plusieurs traitements, qui peuvent être synthétisés en trois grandes étapes.

  • La première étape de traitement de la géométrie consiste à placer les objets au bon endroit dans la scène 3D. Lors de la modélisation d'un objet, celui-ci est encastré dans un cube : un sommet du cube possède la coordonnée (0, 0, 0), et les vertices de l'objet sont définies à partir de celui-ci. Pour placer l'objet dans la scène, il faut tenir compte de sa localisation, calculée par le moteur physique : si le moteur physique a décrété que l'objet est à l'endroit de coordonnées (50, 250, 500), toutes les coordonnées des sommets de l'objet doivent être modifiées. Pendant cette étape, l'objet peut subir une translation, une rotation, ou un gonflement/dégonflement (on peut augmenter ou diminuer sa taille). C'est la première étape de calcul : l'étape de transformation.
  • Ensuite, les sommets sont éclairées dans une phase de lightning. Chaque Sur les cartes graphiques assez anciennes, ce cache est souvent très petit, à peine 30 à à 50 sommets. Pour profiter le plus possible de ce cache, les concepteurs de jeux vidéo peuvent changer l'ordre des sommets en mémoire. se voit attribuer une couleur, qui définit son niveau de luminosité : est-ce que le sommet est fortement éclairée ou est-elle dans l'ombre ?
  • Vient ensuite une phase de traitement de la géométrie, où les sommets sont assemblés en triangles, points, lignes, en polygones. Ces formes géométriques de base sont ensuite traitées telles quelles par la carte graphique. Sur les cartes graphiques récentes, cette étape peut être gérée par le programmeur : il peut programmer les divers traitements à effectuer lui-même.

L'input assembler[modifier | modifier le wikicode]

L'input assembler charge les informations géométriques, présentes dans en mémoire vidéo, dans les unités de traitement des sommets. C'est une unité d'accès mémoire un peu particulière, mais qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Pour faire son travail, il a besoin d'informations mémorisées dans des registres, à savoir l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc).

Input assembler

Avant leur traitement, les objets géométriques présents dans la scène 3D sont mémorisés dans la mémoire vidéo, sous une forme plus ou moins structurée. Rappelons que les objets 3D sont représentés comme un assemblage de triangles collés les uns aux autres, l'ensemble formant un maillage. La position d'un triangle est déterminée par la position de chacun de ses sommets. Avec trois coordonnées x, y et z pour un sommet, un triangle demande donc 9 cordonnées. De plus, il faut ajouter des informations sur la manière dont les sommets sont reliés entre eux, quel sommet est relié à quel autre, comment les arêtes sont connectées, etc. Toutes ces informations sont stockées dans un tableau en mémoire vidéo : le tampon de sommets.

Cube en 3D

Le contenu du tampon de sommet dépend de la représentation utilisée. Il y a plusieurs manières de structure les informations dans le tampon de sommet, qui ont des avantages et inconvénients divers. Toutes ces représentations cherchent à résoudre un problème bien précis : comment indiquer comment les sommets doivent être reliés entre triangles. Le point crucial est qu'un sommet est très souvent partagé par plusieurs triangles. Par exemple, prenez le cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent à limiter la consommation de mémoire ou faciliter la traversée du tampon de sommet.

Les techniques anciennes : Triangle strip et Triangle fan[modifier | modifier le wikicode]

Pour gérer le partage des sommets entre triangles, la représentation la plus simple est appelée le maillage sommet-sommet (Vertex-Vertex Meshes). L'idée est que chaque sommet précise, en plus de ses trois coordonnées, quels sont les autres sommets auxquels il est relié. Les sommets sont regroupés dans un tableau, et les autres sommets sont identifiés par leur position dans le tableau, leur indice. Les informations sur les triangles sont implicites et doivent être reconstruites à partir des informations présentes dans le tampon de sommets. Autant dire que niveau praticité et utilisation de la puissance de calcul, cette techniques est peu efficace. Par contre, le tampon de sommet a l'avantage, avec cette technique, d'utiliser peu de mémoire. Les informations sur les arêtes et triangles étant implicites, elles ne sont pas mémorisées, ce qui économise de la place.

Vertex-Vertex Meshes (VV)

Dans la représentation précédente, les arêtes sont présentes plus ou moins directement dans le tampon de sommets. Mais il existe des méthodes pour que les informations sur les arêtes soient codées de manière implicite. L'idée est que deux sommets consécutifs dans le tampon de sommet soient reliés par une arête. Ainsi, les informations sur les arêtes n'ont plus à être codées dans le tampon de sommet, mais sont implicitement contenues dans l'ordre des sommets. Ces représentations sont appelées des Corner-tables. Dans le domaine du rendu 3D, deux techniques de ce genre ont été utilisées : la technique du triangle fans et celle des triangle strips.

La technique des triangles strip permet d'optimiser le rendu de triangles placés en série, qui ont une arête et deux sommets en commun. L'optimisation consiste à ne stocker complètement que le premier triangle le plus à gauche, les autres triangles étant codés avec un seul sommet. Ce sommet est combiné avec les deux derniers sommets chargés par l'input assembler pour former un triangle. Pour gérer ces triangles strips, l'input assembler doit mémoriser dans un registre les deux derniers sommets utilisées. En mémoire, le gain est énorme : au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, sauf le premier de la surface.

Triangle strip

La technique des triangles fan fonctionne comme pour le triangles strip, sauf que le sommet n'est pas combiné avec les deux sommets précédents. Supposons que je crée un premier triangle avec les sommets v1, v2, v3. Avec la technique des triangles strips, les deux sommets réutilisés auraient été les sommets v2 et v3. Avec les triangles fans, les sommets réutilisés sont les sommets v1 et v3. Les triangles fans sont utiles pour créer des figures comme des cercles, des halos de lumière, etc.

Triangle fan

Le tampon d'indices[modifier | modifier le wikicode]

Enfin, nous arrivons à la dernière technique, qui permet de limiter l'empreinte mémoire tout en facilitant la manipulation de la géométrie. Cette technique est appelée la représentation face-sommet. Elle consiste à stocker les informations sur les triangles et sur les sommets séparément. Le tampon de sommet contient juste les coordonnées des sommets, mais ne dit rien sur la manière dont ils sont reliés. Les informations sur les triangles sont quand à elles, non pas reconstituées à la volée, mais mémorisées dans un tableau séparé. Ce dernier est appelé le tampon d'indices et n'est rien de plus qu'une liste de triangles.

Chaque triangle dans le tampon d'indices est codé non pas par ses trois coordonnées, mais par trois indices. Un indice est tout simplement un numéro qui indique la position du sommet dans le tampon de sommet. On pourrait se demander pourquoi ne pas utiliser les coordonnées du sommet directement. La raison est que les sommets étant partagés entre plusieurs triangle, il y aurait beaucoup de redondance. Si un sommet est partagé entre N triangles, les coordonnées du sommets seraient copiés en N exemplaires. Avec la technique du tampon d'indice, les coordonnées sont codées en un seul exemplaire, mais le tampon d'indice contiendra N exemplaires de l'indice. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. On se retrouve avec deux tableaux : un pour les indices, un pour les vertices. Sauf qu'un indice prend moins de place qu'un sommet : entre un indice et trois coordonnées, le choix est vite fait. Et entre 7 exemplaires d'un sommets, et 7 exemplaire d'un indice et un sommet associé, le gain en mémoire est du côté de la solution à base d'index.

On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D.
Représentation face-sommet.

Avec un tampon d'indices, un sommet peut être chargé plusieurs fois depuis la mémoire vidéo. Pour exploiter cette propriété, les cartes graphiques intercalent une mémoire cache pour mémoriser les sommets déjà chargés : le cache de sommets. Chaque sommet est stocké dans ce cache avec son indice en guise de Tag. Sur les cartes graphiques assez anciennes, ce cache est souvent très petit, à peine 30 à à 50 sommets. Pour profiter le plus possible de ce cache, les concepteurs de jeux vidéo peuvent changer l'ordre des sommets en mémoire.

Cache de sommets.

Transformation[modifier | modifier le wikicode]

Chaque sommet appartient à un objet, dont la surface est modélisée sous la forme d'un ensemble de points. Chaque point est localisé par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape consiste à placer cet objet aux coordonnées (X, Y, Z) déterminées par le moteur physique : le centre de l'objet passe des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour. De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, l'objet subit une translation, une rotation et une mise à l'échelle.

Ensuite, la carte graphique va effectuer un dernier changement de coordonnées. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).

Toutes ces transformations ne sont pas réalisées les unes après les autres. À la place, elles sont toutes effectuées en un seul passage. Pour réussir cet exploit, les concepteurs de cartes graphiques et de jeux vidéos utilisent ce qu'on appelle des matrices, des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Le lien avec la 3D, c'est qu'appliquées sur le vecteur (X, Y, Z) des coordonnées d'un sommet, la multiplication par une matrice peut simuler des translations, des rotations, ou des mises à l'échelle. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, etc. Et mieux : il existe des matrices dont le résultat correspond à plusieurs opérations simultanées : rotation ET translation, par exemple. Autant vous dire que le gain en terme de performances est assez sympathique.

Mais les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Et pour multiplier une matrice par un vecteur, il faut que le nombre de coordonnées dans le vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée, la coordonnée homogène. Pour faire simple, elle ne sert à rien, et est souvent mise à 1, par défaut.

Les anciennes cartes graphiques contenaient un circuit spécialisé dans ce genre de calculs, qui prenait un sommet et renvoyait le sommet transformé. Il était composé d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, certaines cartes graphiques comportaient plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets d'un même objet en même temps.

Eclairage[modifier | modifier le wikicode]

Seconde étape de traitement : l'éclairage. À la suite de cette étape d'éclairage, chaque sommet se voit attribuer une couleur, qui correspond à sa luminosité. Le calcul exact de cette couleur demande de calculer trois couleurs indépendantes, dont l'origine est différente, et qui ne proviennent pas des mêmes types de sources lumineuses. Par exemple, on peut simuler le soleil sans utiliser de source de lumière grâce à cette couleur. Il s'agit d'une source de lumière ambiante. Par simplicité, il est dit que celle-ci est égale en tout point de la scène 3D (d’où le terme lumière ambiante). Mais toute scène 3D contient aussi des sources de lumières, comme des lampes, des torches, etc. Celles-ci sont modélisées comme de simples points, qui ont une couleur bien précise (la couleur de la lumière émise) et émettent une intensité lumineuse codée par un entier. La lumière provenant de ces sources de lumière est appelée la lumière directionnelle.

Lumière ambiante.
Lumière directionnelle.
  • La couleur ambiante correspond à la lumière ambiante réfléchie par la surface. Celle-ci s'obtient simplement en multipliant la couleur ambiante de la surface par l'intensité de la lumière ambiante, deux constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
  • les autres couleurs proviennent de la réflexion de a lumière directionnelle. Elles doivent être calculées par la carte graphique, généralement avec des algorithmes compliqués qui demandent de faire des calculs entre vecteurs.
    • La couleur spéculaire est la couleur de la lumière réfléchie via la réflexion de Snell-Descartes.
    • La couleur diffuse vient du fait que la surface d'un objet diffuse une partie de la lumière qui lui arrive dessus dans toutes les directions. Cette lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. La couleur diffuse ne dépend pas vraiment de l'orientation de la caméra par rapport à la surface. Elle dépend uniquement de l'angle entre le rayon de lumière et la verticale de la surface (sa normale).
Illustration de la dispersion de la lumière diffuse par une surface, suivant sa rugosité.

Ces couleurs sont additionnées ensemble pour donner la couleur finale du sommet. Chaque composante rouge, bleu, ou verte de la couleur est traitée indépendamment des autres.

Couleurs utilisées dans l'algorithme de Phong.

Vecteurs nécessaires pour faire les calculs[modifier | modifier le wikicode]

Les calculs de réflexion de la lumière demandent de connaitre l'orientation de la surface. Pour gérer cette orientation, le sommet est fourni avec une information qui indique comment est orientée la surface : la normale. Cette normale est un simple vecteur, perpendiculaire à la surface de l'objet, dont l'origine est le sommet. Autre paramètre d'une surface : son coefficient de réflexion. Il indique si la surface réfléchit beaucoup la lumière ou pas, et dans quelles proportions. Généralement, chaque point d'une surface a trois coefficient de réflexion fournis de base : un pour la couleur diffuse, un pour la couleur spéculaire, et un pour la couleur ambiante. Outre la normale et la brillance, il faut aussi connaitre l'angle entre la normale et le trajet surface-caméra (noté w dans le schéma ci-dessous).

Normale de la surface.

La carte graphique a aussi besoin de l'angle avec lequel arrive un rayon lumineux sur la surface de l'objet. Cet angle dépend de l'orientation de la lumière et du point de surface considéré. Par orientation de la lumière, il faut savoir que la majorité des sources de lumière émet de la lumière dans une direction privilégiée, la lumière émise diminuant avec l'angle comparé à cette direction. Il existe bien quelques sources de lumière qui émettent de manière égale dans toutes les directions, mais nous passons cette situation sous silence. La direction privilégiée est notée v dans le schéma du dessous. Le trajet entre la source de lumière fait un certain angle par rapport à la direction privilégiée. Il faut donc avoir une formule qui donne l'intensité de la lumière en fonction de cet angle, angle noté L dans le schéma du dessous.

Graphics lightmodel spot

À partir de ces informations, la carte graphique calcule l'éclairage. Les anciennes cartes graphiques, entre la Geforce 256 et la Geforce FX contenaient des circuits câblés capables d'effectuer des calculs d'éclairage simples. Cette fonction de calcul de l'éclairage faisait partie intégrante d'un gros circuit nommé le T&L. Dans ce qui va suivre, nous allons voir l'algorithme d'éclairage de Phong, une version simplifiée de la méthode utilisée dans les circuits de T&L.

Calcul des couleurs spéculaire et diffuse[modifier | modifier le wikicode]

Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).

Sur la droite, vous voyez illustrés les vecteurs utiles dans le calcul de l'éclairage directionnel. De base, le vecteur L est dirigé vers la source de lumière, et sa norme est égale à l'intensité de la source de lumière (qu'on suppose connue). La normale est multipliée par la couleur diffuse du sommet, ce qui donne le vecteur de couleur diffuse. La couleur diffuse finale est calculée en effectuant le produit scalaire entre l'intensité de la source de lumière et le vecteur de couleur diffuse.

La lumière réfléchie directement par la surface est émise dans la direction R. Sa couleur est simplement égale à l'intensité de la lumière multipliée par la couleur spéculaire. Mais la caméra n'est pas forcément alignée avec cette direction. Pour calculer la lumière spéculaire, il faut prendre en compte l'angle que fait la caméra et la lumière réfléchie. Dans le schéma de droite, c'est l'angle entre les vecteurs R et V, que nous appellerons angle A. Pour calculer la couleur dans la direction V, il faut multiplier la couleur sur le rayon R par le carré du cosinus de l'angle A.

Tesselation[modifier | modifier le wikicode]

Certaines cartes graphiques gèrent des techniques de tesselation, qui permettent d'ajouter des vertices. La position et la couleur de ces vertices sont calculées à la volée par la carte graphique. Cette tesselation permet ainsi d'obtenir un fort niveau de détail géométrique, sans pour autant remplir la mémoire vidéo de vertices pré-calculées. Ces techniques de tesselation vont décomposer chaque triangle de la géométrie en sous-triangles plus petits, ce qui demande d'ajouter des sommets, et vont modifier les coordonnées des sommets créés lors de ce processus.

Tesselation.

La première carte graphique commerciale à intégrer de quoi faire de la tesselation était la Radeon 8500, de l'entreprise ATI (aujourd'hui rachetée par l'entreprise AMD). La technologie de tesselation en question était appelée la technologie TrueForm. Elle utilisait un circuit non-programmable, qui tesselait certaines surfaces et interpolait la forme de la surface entre les sommets. Le manque de versatilité de la technologie fi qu'elle n'a pas été beaucoup utilisée et est tombée en désuétude. La tesselation a eu un regain d'intérêt à l'arrivée des geometry shaders dans Direct X 10 et Open Gl 3.2. Et il y avait de quoi, de tels shaders pouvant en théorie implémenter des algorithmes de tesselation complexes sans trop de problèmes. Mais les limitations de ces shaders n'a pas permis leur usage pour de la tesselation généraliste. Il fallu attendre l'arrivée des tesselation shaders dans Open Gl 4.0 pour que des shaders adéquats arrivent sur le marché commercial.


Le rasterizeur

Rasterization.

À ce stade, les vertices ont été converties en triangle, après une éventuelle phase de tesselation. Vient alors l'étape de rasterization, durant laquelle chaque pixel de l'écran se voit attribuer un ou plusieurs triangle(s). Cela signifie que sur le pixel en question, c'est le triangle attribué au pixel qui s'affichera. C'est lors de cette phase que la perspective est gérée, en fonction de la position de la caméra. C'est aussi lors de cette étape qu'à lieu le view frustrum culling et le Back-face Culling.

Triangle setup[modifier | modifier le wikicode]

Une fois tous les triangles non-visibles éliminés, la carte graphique va attribuer les triangles restants à des pixels : c'est l'étape de Triangle Setup.

Fonction de contours[modifier | modifier le wikicode]

On peut voir un triangle comme une portion du plan délimitée par trois droites. À partir de chaque droite, on peut créer une fonction de contours, qui va prendre un pixel et va indiquer de quel côté de la droite se situe le pixel. La fonction de contours va, pour chaque point sur l'image, renvoyer un nombre entier :

  • si le point est placé sur la droite, la fonction renvoie zéro ;
  • si le point est placé d'un côté de la droite, cette fonction renvoie un nombre négatif ;
  • et enfin, si le point est placé de l'autre côté, la fonction renvoie un nombre positif.

Comment calculer cette fonction ? Tout d'abord, nous allons dire que le point que nous voulons tester a pour coordonnées x et y sur l'écran. Ensuite, nous allons prendre un des sommets du triangle, de coordonnées X et Y. L'autre sommet, placé sur cette droite, sera de coordonnées X2 et Y2. La fonction est alors égale à :

(x−X)∗(Y2−Y)−(y−Y)∗(X2−X)

Si vous appliquez cette fonction sur chaque côté du triangle, vous allez voir une chose assez intéressante :

  • à l'intérieur du triangle, les trois fonctions (une par côté) donneront un résultat positif ;
  • à l'extérieur, une des trois fonctions donnera un résultat négatif.

Pour savoir si un pixel appartient à un triangle, il suffit de tester le résultat des fonctions de contours.

Triangle traversal[modifier | modifier le wikicode]

Dans sa version la plus naïve, tous les pixels de l'écran sont testés pour chaque triangle. Si le triangle est assez petit, une grande quantité de pixels seront testés inutilement. Pour éviter cela, diverses optimisations ont été inventées. La première consiste à déterminer le plus petit rectangle possible qui contient le triangle, et à ne tester que les pixels de ce rectangle.

Smallest rectangle traversal

De nos jours, les cartes graphiques actuelles se basent sur une amélioration de cette méthode. Le principe consiste à prendre ce plus petit rectangle, et à le découper en morceaux carrés. Tous les pixels d'un carré seront testés simultanément, dans des circuits séparés, ce qui est plus rapide que les traiter uns par uns.

Tiled traversal

Interpolation des pixels[modifier | modifier le wikicode]

Interpolation des pixels.

Une fois l'étape de triangle setup terminée, on sait donc quels sont les pixels situés à l'intérieur d'un triangle donné. Mais il faut aussi remplir l'intérieur des triangles : les pixels dans le triangle doivent être coloriés, avoir une coordonnée de profondeur, etc. Pour cela, nous sommes obligés d'extrapoler la couleur et la profondeur à partir des données situées aux sommets. Cela va être fait par une étape d'interpolation, qui va calculer les informations à attribuer aux pixels qui ne sont pas pile-poil sur une vertice. Par exemple, si j'ai un sommet vert, un sommet rouge, et un sommet bleu, le triangle résultant doit être colorié comme indiqué dans le schéma de droite. Ce que l'étape de triangle setup va fournir, ce sont des informations qui précisent quelle est la couleur, la profondeur d'un pixel calculée à partir d'un triangle. Or, il est rare qu'on ne trouve qu'un seul triangle sur la trajectoire d'un pixel : c'est notamment le cas quand plusieurs objets sont l'un derrière l'autre. Si vous tracer une demi-droit dont l'origine est la caméra, et qui passe par le pixel, celle-ci intersecte la géométrie en plusieurs points : ces points sont appelés des fragments. Dans la suite, les fragments attribués à un même pixel sont combinés pour obtenir la couleur finale de ce pixel. Mais cela s'effectuera assez loin dans le pipeline graphique, et nous reviendrons dessus en temps voulu.

Pour calculer les couleurs et coordonnées de chaque fragment, on va utiliser les coordonnées barycentriques. Pour faire simple, ces coordonnées sont trois coordonnées notées u, v et w. Pour les déterminer, nous allons devoir relier le fragment aux trois autres sommets du triangle, ce qui donne trois triangles. Les coordonnées barycentriques sont simplement proportionnelles aux aires de ces trois triangles. L'aire totale du triangle, ainsi que l'aire des trois sous-triangles, sont calculées par un petit calcul tout simple, que la carte graphique peut faire toute seule. Quand je dis proportionnelles, il faut savoir que ces trois aires sont divisées par l'aire totale du triangle, qui se ramène dans l'intervalle [0, 1]. Cela signifie que la somme de ces trois coordonnées vaut 1 : u + v + w = 1. En conséquence, on peut se passer d'une des trois coordonnées dans nos calculs, vu que w = 1 - (u + v). Ces trois coordonnées permettent de faire l'interpolation directement . Il suffit de multiplier la couleur/profondeur d'un sommet par la coordonnée barycentrique associée, et de faire la somme de ces produits. Si l'on note C1, C2, et C3 les couleurs des trois sommets, la couleur d'un pixel vaut : (C1 * u) + (C2 * v) + (C3 * w).

Coordonnées barycentriques.

Le problème : la perspective n'est pas prise en compte ! Intuitivement, on pouvait le deviner : la coordonnée de profondeur (z) n'était pas prise en compte dans le calcul de l’interpolation. Pour résumer, le problème vient du fait que l’interpolation de la cordonnée z est à l'origine de la mauvaise perspective : en interpolant 1/z, et en calculant z à partir de cette valeur interpolée, les problèmes disparaissent. Le problème : la perspective n'est pas prise en compte ! Intuitivement, on pouvait le deviner : la coordonnée de profondeur (z) n'était pas prise en compte dans le calcul de l’interpolation. Pour résumer, le problème vient du fait que l’interpolation de la cordonnée z est à l'origine de la mauvaise perspective : en interpolant 1/z, et en calculant z à partir de cette valeur interpolée, les problèmes disparaissent.

Correction de perspective.


Les unités de texture

Texture mapping

Les textures sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des texels.

Plaquer une texture sur un objet consiste à attribuer une vertice à chaque texel, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque vertice contient donc des coordonnées de texture, qui indiquent quel texel appliquer sur la vertice. Ces coordonnes précisent la position du texel dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à la ligne 5, et la colonne 27 dans ma texture. Lors de la rasterization, ces coordonnées sont interpolées, et chaque pixel de l'écran se voit attribuer une coordonnée de texture, qui indique avec quel texel il doit être colorié. À partir de ces coordonnées de texture, le circuit de gestion des textures calcule l'adresse du texel qui correspond, et se charge de lire celui-ci.

Sur les anciennes cartes graphiques, les textures disposaient de leur propre mémoire, séparée de la mémoire vidéo. Mais c'est du passé : de nos jours, les textures sont stockées dans la mémoire vidéo principale. Évidemment, l'algorithme de rasterization a une influence sur l'ordre dans lequel les pixels sont envoyés aux unités de texture. Et suivant l'algorithme, les texels lus seront proches ou dispersés en mémoire. Généralement, le meilleur algorithme est celui du tiled traversal.

Filtrage[modifier | modifier le wikicode]

On pourrait croire que plaquer des textures sans autre forme de procès suffit à garantir des graphismes d'une qualité époustouflante. Mais les texels ne vont pas tomber tout pile sur un pixel de l'écran : la vertice correspondant au texel peut être un petit peu trop en haut, ou trop à gauche, etc.

Position du pixel par rapport aux texels.

Pour résoudre ce problème, on peut colorier avec le texel correspondant à la vertice la plus proche. Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Pour améliorer la qualité de l'image, la carte graphique va effectuer un filtrage de texture. Ce filtrage consiste à choisir le texel à appliquer sur un pixel du mieux possible, par un calcul mathématique assez simple. Ce filtrage est réalisé par un circuit spécialisé : le texture sampler, lui-même composé :

  • d'un circuit qui calcule les adresses mémoire des texels à lire et les envoie à la mémoire ;
  • d'un circuit qui va filtrer les textures.
Unité de texture.

Filtrage bilinéaire[modifier | modifier le wikicode]

Le plus simple de ces filtrage est le filtrage bilinéaire, qui effectue une sorte de moyenne des quatre texels les plus proches du pixel à afficher. Plus précisément, ce filtrage va effectuer ce qu'on appelle des interpolations linéaires. Pour comprendre l'idée, nous allons prendre une situation très simple, où un pixel est aligné avec deux autres texels. Pour effectuer l'interpolation linéaire entre ces deux texels, nous allons faire une première supposition : la couleur varie entre les deux texels en suivant une fonction affine. On peut alors calculer la couleur du pixel par un petit calcul mathématique d'interpolation (une simple moyenne pondérée par la distance).

Interpolation linéaire.

Seul problème, cela marche pour deux pixels, pas 4. Avec 4 pixels, nous allons devoir calculer la couleur de points intermédiaires. Le premier est celui qui se situe à l'intersection entre la droite formé par les deux texels de gauche, et la droite parallèle à l'abscisse qui passe par le pixel. Le second est celui qui se situe à l'intersection entre la droite formé par les deux texels de gauche, et la droite parallèle à abscisse qui passe par le pixel. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.

Le circuit qui permet de faire ce genre de calcul est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.

Unité de filtrage bilinéaire.

Mip-mapping[modifier | modifier le wikicode]

Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Par exemple, un objet assez lointain peut très bien ne prendre que quelques dizaines de pixels à l'écran. Dans ces conditions, plaquer une texture de 512 pixel de côté serait vraiment du gâchis en terme de performance : il faudrait charger tous les pixels de la texture, les traiter, et n'en garder que quelque uns. De plus, cela pourrait créer des artefacts visuels : les textures affichées ont tendance à pixeliser. Pour limiter la casse, les concepteurs de jeux vidéo utilisent souvent la technique du mip-mapping.

Exemples de mip-maps.

Cette technique consiste simplement à utiliser plusieurs exemplaires d'une même texture, chaque exemplaire étant adapté à une certaine distance. Ce qui différenciera ces exemplaires, ce sera leur résolution. Par exemple, une texture sera stocké dans un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32. Chaque exemplaire correspond à un niveau de détail, aussi appelé Level Of Detail en anglais (abrévié en LOD). Le bon exemplaire sera choisit lors de l'application de la texture. Ainsi, les objets proches seront rendus avec la texture la plus grande (512 par 512 dans notre exemple). Au-delà d'une certaine distance, les textures 256 par 256 seront utilisées. Encore plus loin, les textures 128 par 128 seront utilisées, etc.

Exemple de mipmapping.

Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait que chaque texture prend 4 fois de pixels que l'image immédiatement supérieure : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des mip-maps, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.

Pour choisir la bonne mipmap, les circuits de calcul d'adresse doivent connaître les adresses des différents niveaux de détails, ainsi que des informations sur la profondeur de la texture. Pour faciliter les calculs d'adresse, les mip-maps d'une texture sont stockées les unes après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chacune des mip-map : l'adresse de la plus grande, et quelques astuces arithmétiques suffisent.

Unité de texture avec mipmapping.

Filtrage trilinéaire[modifier | modifier le wikicode]

Avec le mip-mapping, des discontinuités apparaissent lorsqu'une texture est appliquée répétitivement sur une surface, comme quand on fabrique un carrelage à partir de carreaux tous identiques. Par exemple, pensez à une texture de sol : celle-ci est appliquée plusieurs fois sur toute la surface du sol. Au delà d'une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. Le filtrage trilinéaire permet d'adoucir ces transitions. Il consiste à faire « une moyenne » pondérée entre les textures des niveaux de détails adjacents. Le filtrage trilinéaire demande d'effectuer deux filtrages bilinéaires : un sur la texture du niveau de détail adapté, et un autre sur la texture de niveau de détail inférieur. Les deux textures obtenues par filtrage vont ensuite subir une interpolation linéaire.

Le circuit qui s'occupe de calculer un filtrage trilinéaire est une amélioration du circuit utilisé pour le filtrage bilinéaire. Il est constitué d'un circuit effectuant un filtrage bilinéaire, de deux registres, d'un interpolateur linéaire, et de quelques circuits de gestion, non-représentés. Son fonctionnement est simple : ce circuit charge 4 texels d'une mip-map, les filtre, et stocke le tout dans un registre. Il recommence l'opération avec les 4 texels de la mip-map de niveau de détail inférieure, et stocke le résultat dans un autre registre. Enfin, le tout passe par un circuit qui interpole les couleurs finales en tenant compte des coefficients d'interpolation linéaire, mémorisés dans des registres.

Unité de filtrage trilineaire série.

Il est possible de créer un circuit qui effectue les deux filtrages en parallèle. Seul problème : ce genre de circuit nécessite de charger 8 pixels simultanément. Qui plus est, ces 8 pixels ne sont pas consécutifs en mémoire. Utiliser ce genre de circuit nécessiterait d'adapter la mémoire et le cache, ce qui ne vaut généralement pas la peine.

Unité de filtrage trilineaire parallèle.

Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. A chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.

Filtrage anisotropique[modifier | modifier le wikicode]

D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. Pour gommer ce flou de perspective, les chercheurs ont inventé le filtrage anisotropique. En fait, je devrais plutôt dire : LES filtrages anisotropique. Il en existe plusieurs. Certains sont des algorithmes qui ne sont pas utilisés dans les cartes graphiques actuelles. Ceux-ci prennent beaucoup trop de circuits, et sont trop gourmand en accès mémoires et en calculs pour être efficaces. Il semblerait que les cartes graphiques actuelles utiliseraient des variantes de l'algorithme TEXRAM, comme l'algorithme Fast Footprint Assembly. On pourrait aussi citer l'algorithme Talisman de Microsoft, qui serait implémenté depuis Direct X 6.0. Tous vont effectuer plusieurs filtrages bilinéaires sur des texels convenablement choisis, d'une manière qui change selon l'algorithme utilisé. De plus, ces texels se verront attribuer des coefficients afin de prendre en compte certains texels en priorité. Au niveau des circuits, l'utilisation de filtrage anisotropique ne change pas grand chose au niveau des circuits de filtrage.

Exemple de filtrage anisotrope.

Compression de textures[modifier | modifier le wikicode]

Certaines textures un peu spéciales peuvent aller jusqu'au mébioctet. Pour limiter la casse, les cartes graphiques peuvent compresser les textures. La carte graphique contient alors un circuit, capable de décompresser un ou plusieurs texels. Fait important : toute la texture n'est pas décompressée : seuls les texels lus depuis la mémoire le sont. Nos cartes graphiques supportent un grand nombre de formats de compression de texture, qui entraînent souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Nous allons voir quelque algorithmes de compression de textures. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l'Ericsson Texture Compression ou l'Adaptive Scalable Texture Compression.

Palette[modifier | modifier le wikicode]

La première technique est celle de la palette, que l'on a entraperçue dans le chapitre sur les cartes graphiques 2D. Avec cette technique, chaque texture est fournie avec une table de correspondances entre numéro et couleurs : ce tableau s'appelle la palette. La texture ne contient aucune couleur, chaque pixel indiquant le numéro de sa couleur. Cependant, la table des couleurs contient un nombre limité de couleurs, ce qui fait que cette technique ne marche pas pour les textures qui utilisent beaucoup de couleurs différentes. Certains pixels se voient attribuer la couleur la plus proche qui est présente dans la palette, ce qui fait que la compression n'est pas sans pertes.

Vector quantization[modifier | modifier le wikicode]

De nos jours, la compression ne cherche pas à compresser des pixels individuels, mais ces blocs blocs de 16*16, 8*8 ou 4*4 texels. Le nombre de bit utilisé pour chaque texel peut varier, se limitant au strict minimum utile. La technique de vector quantization peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des blocs de texels. À l'intérieur de la carte graphique, on trouve une table qui stocke tous les blocs possible de 2 * 2, 3 * 3 , ou 4 * 4 texels. Chaque de ces blocs se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.

Block Truncation coding[modifier | modifier le wikicode]

La première technique de compression élaborée est celle du Block Truncation Coding. Cette méthode ne marche que pour les images en niveaux de gris, mais peut être amélioré pour gérer les images couleur. La majorité des algorithmes de compression de texture utilisés dans nos cartes graphiques sont une sorte d'amélioration de cet algorithme. Le BTC ne mémorise que deux niveaux de gris par bloc, que nous appellerons couleur 1 et couleur 2 : à l'intérieur du bloc, chaque pixel est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel dans le bloc, on utilise un bit mémoriser sa couleur : 0 pour couleur 1, et 1 pour couleur 2. Chaque bloc est donc mémorisé en mémoire par deux entiers, qui codent chacun une couleur, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple : il suffit d'utiliser deux multiplexeurs.

Block Truncation coding.

La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu d'un pixel. Dans ces conditions, chaque bloc sera séparé en trois sous-bloc : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.

Color Cell Compression[modifier | modifier le wikicode]

On peur améliorer le BTC pour qu'il gère des couleurs autre que des niveaux de gris : on obtient alors l'algorithme du Color Cell Compression, ou CCC. Ce CCC utilise deux couleurs RGBA codées sur 32 bits, au lieu de deux niveaux de gris. Le circuit de décompression est identique à celui utilisé pour le BTC.

Color Cell Compression.

S3TC / DXTC[modifier | modifier le wikicode]

Le format de compression de texture utilisé de base par Direct X s'appelle le DXTC. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en carrés de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc.

DXTC.
Dxt1.

Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel. Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :

  • 00 = Couleur1
  • 01 = Couleur2
  • 10 = (2 * Couleur1 + Couleur2) / 3
  • 11 = (Couleur1 + 2 * Couleur2) / 3

Sinon, les deux bits sont à interpréter comme suit :

  • 00 = Couleur1
  • 01 = Couleur2
  • 10 = (Couleur1 + Couleur2) / 2
  • 11 = Transparent

Le circuit de décompression du DXTC ressemble alors à ceci :

Circuit de décompression du DXTC.

DXTC 2, 3, et 4[modifier | modifier le wikicode]

Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la texture et toujours découpée en blocs de 16 texels. Seule différence : la transparence fait son apparition. Chacun de ces blocs de texels est encodé sur 128 bits. Les premiers 64 bits servent à stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.

Dxt 2 et 3.

Dans ces deux formats, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparance. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.

Dxt 4 et 5.

PVRTC[modifier | modifier le wikicode]

Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle ne crée par de cartes graphiques pour PC. Elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.

Avec le PVRTC, les textures sont encore une fois découpées en blocs de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacun de ces blocs est stocké en mémoire dans un bloc qui contient :

  • une couleur codée sur 16 bits ;
  • une couleur codée sur 15 bits ;
  • 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
  • et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.

Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.

Optimisations de l'unité de texture[modifier | modifier le wikicode]

L'accès aux textures demande d'effectuer de nombreux accès en mémoire vidéo. Si celle-ci a un débit assez important, sa latence est cependant assez forte. L'accès à une texture, sans optimisations, va ainsi bloquer l'unité de texture durant tout le temps d'accès, qui peut se mesurer en millièmes de secondes (ce qui est énorme). Pour rendre plus rapide l'accès aux textures, les cartes 3d utilisent de nombreuses optimisations assez intéressantes. La plus connue d'entre elle est certainement d'usage de caches de textures, mais celle-ci est loin d'être la seule. Les techniques de préchargement ne sont pas à mettre de côté, par exemple.

Cache de textures[modifier | modifier le wikicode]

Les cartes 3D utilisent souvent un ou plusieurs cache, spécialisés dans le traitement des textures. Lorsqu'un texel est lu pour la première fois, celui-ci est placée dans ce cache de textures. Lors des utilisations ultérieures, la carte graphique aura juste à lire le texel depuis ce cache au lieu de devoir accéder à la mémoire vidéo, ce qui est nettement plus rapide. Ceci dit, les cartes graphiques actuelles n'ont pas qu'un seul cache de textures. Toute les cartes graphiques actuelles disposent de deux caches de textures : un petit, et un gros. Les deux caches sont fortement différents. L'un est un gros cache, qui fait dans les 4 kibioctets, et l'autre est un petit cache, faisant souvent moins d'1 kibioctet.

Stockage des données dans le cache de texture[modifier | modifier le wikicode]

D'ordinaire, les textures sont décompressées après lecture dans le cache. Il est possible de décompresser les textures avant de les placer dans le cache, mais ces textures décompressées prennent beaucoup plus de cache que les textures compressées. L'utilisation du cache est alors moins optimale.

De base, les pixels d'une texture sont stockés les uns à la suite des autres, ligne par ligne. On pourrait croire que cette solution fonctionne bien pour échanger des données entre le cache de textures et la mémoire vidéo, mais elle entre en conflit avec le filtrage de texture. Comme on l'a vu précédemment, le filtrage de texture utilise souvent des carrés de texels. Dans ces conditions, mieux vaut découper la texture en carrés de N texels de côté, placés les uns à côté des autres en mémoire. Les performances sont meilleures quand chaque carré de texel permet de remplir exactement une ligne de cache.

Écritures dans le cache de texture[modifier | modifier le wikicode]

Sur la majorité des anciennes cartes graphiques, le cache de textures est accessible uniquement en lecture, pas en écriture. Simple question de coût. Seulement, les jeux vidéos 3D récents utilisent des techniques dites de render-to-texture, qui permettent de calculer certaines données et à les écrire en mémoire vidéo pour une utilisation ultérieure. La présence d'une mémoire cache en lecture seule peut alors poser des problèmes : la modification d'une texture via render-to-texture n'est pas propagée dans le cache, qui conserve l'ancienne donnée.

Une solution simple consiste à garder un cache en lecture seule, et à invalider les données mises à jour lors d'une écriture. Si la carte graphique écrit dans la mémoire, le cache vérifie si la donnée dans le cache est mise à jour, l'invalide si c'est le cas. Pour cela, notre cache contient un bit pour chaque ligne, qui indique si la donnée est invalide, qui est mis à jour lors des écritures. Cette technique peut être adaptée dans le cas où plusieurs mémoires de textures séparées existent sur une même carte graphique : les écritures doivent invalider toutes les copies dans tous les caches de texture. Cela nécessite d'ajouter des circuits qui propagent l'invalidation dans tous les autres caches.

Autre solution : rendre le cache de texture accessible en écriture. Si un seul cache de texture est présente dans la carte graphique, il n'y a pas besoin de modifications supplémentaires. Mais si il y en a plusieurs, le problème mentionné plus haut revient : les copies des autres caches doivent être invalidées. De plus, la mémoire cache qui a la bonne donnée doit fournir la bonne version de la donnée, quand les autres caches voudront la mettre à jour.

Préchargement[modifier | modifier le wikicode]

Pour améliorer la rapidité des accès, il est possible de préparer certaines lectures de texture à l'avance. En effet, un accès à une texture est composé d'un grand nombre de sous-étapes, comme déterminer le niveau de mip-map et calculer les adresses de texture, accéder à la mémoire vidéo, filtrer la texture, etc. Le but du prefetching est de calculer les adresses de lecture à l'avance, sans attendre que la lecture en cours soit terminée. Les adresses des texels sont précalculées et mises en attente dans une mémoire FIFO, en attendant que la mémoire vidéo soit libre.

Préchargement de textures.


Les processeurs de shaders

Au fur et à mesure que les procédés de fabrication devenaient de plus en plus étoffés, les cartes graphiques pouvaient incorporer un plus grand nombre de circuits. Les unités géométriques, autrefois câblées, sont devenues des unités programmables. Les unités de traitement de la géométrie deviennent donc des processeurs indépendants, capable d’exécuter des programmes sur des vertices. Ces programmes sont appelés des Vertex Shaders. Par la suite, l'étape de traitement des pixels est elle aussi devenue programmable. Des programmes capables de traiter des pixels, les pixels shaders ont fait leur apparition. Une seconde série d'unités a alors été ajoutée dans nos cartes graphiques : les processeurs de pixels shaders. Ils fonctionnent sur le même principe que les processeurs de vertex shaders, mais leur jeu d'instruction était quelque peu différent. Les premières cartes graphiques avaient des jeux d'instructions séparés pour les unités de vertex shader et les unités de pixel shader et les processeurs étaient séparés. Depuis DirectX 10, ce n'est plus le cas : le jeu d'instructions a été unifié entre les vertex shaders et les pixels shaders.

Jeu d'instruction[modifier | modifier le wikicode]

Les shaders sont souvent écrits dans un langage de haut-niveau : le HLSL pour les shaders Direct X et le GLSL pour les shaders Open Gl. Ils sont traduits (compilés) à la volée par les pilotes de la carte graphique, pour les rendre compatibles avec le processeur de vertex shaders. Au début, ces langages, ainsi que le matériel, supportaient uniquement des programmes simples. 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 processeurs de shaders disposaient de peu d'instructions. On trouvait uniquement des instructions de calcul arithmétiques, dont certaines étaient assez complexes (logarithmes, racines carrées, etc). Depuis, d'autres versions de vertex shaders ont vu le jour. Pour résumer, les améliorations ont porté sur :

  • le nombre de registres ;
  • la taille de la mémoire qui stocke les shaders ;
  • le support des branchements ;
  • l'ajout d'instructions d'appel de fonction ;
  • le support de fonctions imbriquées ;
  • l'ajout d'instructions de lecture/écriture en mémoire centrale ;
  • l'ajout d'instructions capables de traiter des nombres entiers ;
  • l'ajout d'instructions bit à bit.

Jeu d'instruction[modifier | modifier le wikicode]

Sur tous les processeurs de traitement de vertices, il est possible de traiter plusieurs morceaux de vertices à la fois. Même chose pour les processeurs de pixels shaders, qui peuvent traiter plusieurs pixels à la fois. Ces processeurs sont dit parallèles, à savoir qu'ils peuvent faire plusieurs calculs en parallèle dans des unités de calcul séparées. Il existe plusieurs types de processeurs de shaders, qui se distingue par la manière dont les calculs sont faits en parallèle :

  • les processeurs SIMD et VLIW, avec un parallélisme au niveau du jeu d'instruction ;
  • les processeurs scalaires, sans parallélisme (du moins, au niveau du jeu d'instruction).

Les processeurs VLIW étaient autrefois utilisés sur les anciennes RADEON d'AMD, mais ne sont plus vraiment utilisées aujourd'hui, la mode étant aux processeurs SIMD ou scalaires. Nous parlerons des processeurs SIMT dans la section sur la microarchitecture, ceux-ci ayant un jeu d'instruction non-parallèle, mais convertissent les instructions séries en instructions parallèles à la volée.

Processeurs SIMD[modifier | modifier le wikicode]

Les instructions des processeurs SIMD sont des instructions vectorielles : elles travaillent sur des vecteurs, des ensembles de plusieurs nombres entiers ou nombres flottants placés les uns à côté des autres, le tout ayant une taille fixe. Une instruction de calcul vectoriel va traiter chacune des données du vecteur indépendamment des autres. Par exemple, une instruction d'addition vectorielle va additionner ensemble les données qui sont à la même place dans deux vecteurs, et placer le résultat dans un autre vecteur, à la même place. Quand on exécute une instruction sur un vecteur, les données présentes dans ce vecteur sont traitées simultanément.

Instructions SIMD

La première carte graphique commerciale destinée aux gamers à disposer d'une unité de vertex programmable est la Geforce 3. Celui-ci respectait le format de vertex shader 1.1. L'ensemble des informations à savoir sur cette unité est disponible dans l'article "A user programmable vertex engine", disponible sur le net. Le processeur de cette carte était capable de gérer un seul type de données : les nombres flottants de norme IEEE754. Toutes les informations concernant la coordonnée d'une vertice, voire ses différentes couleurs, doivent être encodées en utilisant ces flottants. De nos jours, les processeurs de vertices sont capables de gérer des nombres entiers, et les instructions qui vont avec. Ce processeur est capable d’exécuter 17 instructions différentes. Voici la liste de ces instructions :

OpCode Nom Description
MOV Move vector -> vector
MUL Multiply vector -> vector
ADD Add vector -> vector
MAD Multiply and add vector -> vector
DST Distance vector -> vector
MIN Minimum vector -> vector
MAX Maximum vector -> vector
SLT Set on less than vector -> vector
SGE Set on greater or equal vector -> vector
RCP Reciprocal scalar-> replicated scalar
RSQ Reciprocal square root scalar-> replicated scalar
DP3 3 term dot product vector-> replicated scalar
DP4 4 term dot product vector-> replicated scalar
LOG Log base 2 miscellaneous
EXP Exp base 2 miscellaneous
LIT Phong lighting miscellaneous
ARL Address register load miscellaneous

Comme on le voit, ces instructions sont presque toutes des instructions arithmétiques : multiplications, additions, exponentielles, logarithmes, racines carrées, etc. À côté, on trouve des comparaisons (SDE, SLT), une instruction MOV qui déplace le contenu d'un registre dans un autre, et une instruction de calcul d'adresse. Fait intéressant, toutes ces instructions peuvent s’exécuter en un seul cycle d'horloge. On remarque que parmi toutes ces instructions arithmétiques, la division est absente. Il faut dire que la contrainte qui veut que toutes ces instructions s’exécutent en un cycle d'horloge pose quelques problèmes avec la division, qui est une opération plutôt lourde en hardware. À la place, on trouve l'instruction RCP, capable de calculer 1/x, avec x un flottant. Cela permet ainsi de simuler une division : pour obtenir Y/X, il suffit de calculer 1/X avec RCP, et de multiplier le résultat par Y.

S'il n'y avait aucune instruction d'accès à la mémoire sur le processeur de la Geforce 3, la situation a changé depuis : les cartes graphiques récentes peuvent aller lire certaines données depuis la mémoire vidéo. Généralement, les shaders lisent des textures en mémoire vidéo, textures qui servent à colorier les pixels ou à configurer les calculs d'éclairage. Et la plupart des cartes graphiques suivant la Geforce 2 incorporait des instructions de lecture de texture. Les écritures, plus rares, sont venues après, afin de faciliter certaines techniques de rendu dont je ne parlerais pas ici.

Autre manque : les instructions de branchement. C'est un fait, ce processeur ne peut pas effectuer de branchements. À la place, il doit simuler ceux-ci en utilisant des instructions arithmétiques. C'est très complexe, et cela limite un peu les possibilités de programmation. À l'époque, ces branchements n'étaient pas utiles, sans compter que les environnements de programmation ne permettaient pas d'utiliser de branchements lors de l'écriture de shaders. De nos jours, les cartes graphiques récentes peuvent effectuer des branchements, ou du moins, des instructions similaires. On pourrait croire que l’absence de branchements pose problème, mais les concepteurs de processeur ont implémenté diverses solutions pour palier ce manque.

  • Certains processeurs utilisent des instructions à prédicats, des instructions "annulables" qui ne s’exécutent que si une condition est remplie.
  • D'autres instructions ne modifient un élément d'un vecteur que si celui-ci remplit une condition. Pour cela, le processeur de traitement de vertices contient un Vector Mask Register. Celui-ci permet de stocker des informations qui permettront de sélectionner certaines données et pas d'autres pour faire notre calcul. Il est mis à jour par des instructions de comparaison. Ce Vector Mask Register va stocker un bit pour chaque flottant présent dans le vecteur à traiter, bit qui indique s'il faut appliquer l'instruction sur ce flottant. Si ce bit est à 1, notre instruction doit s’exécuter sur la donnée associée à ce bit. Sinon, notre instruction ne doit pas la modifier. On peut ainsi traiter seulement une partie des registres stockant des vecteurs SIMD.
Vector mask register

Processeurs VLIW[modifier | modifier le wikicode]

Sur les processeurs VLIW, les instructions sont regroupées dans ce qu'on appelle des Bundles, des sortes de super-instructions. Les instructions d'un bundle peuvent s'exécuter en parallèle sur différentes unités de calcul, mais le bundle est chargé en une seule fois depuis la mémoire. Chaque instruction d'un faisceau doit expliciter quelle unité de calcul doit la prendre en charge. Vu que chaque instruction sera attribué à une unité de calcul différente, le compilateur peut se débrouiller pour que les instructions dans un bundle soient indépendantes. Mais il se peut que le compilateur ne puisse pas remplir tout le bundle avec des instructions indépendantes. Sur les anciens processeurs VLIW, les bundles étaient de taille fixe, ce qui forçait le compilateur à remplir d'éventuels vides avec des NOP (des instructions qui ne font rien), diminuant la densité de code. La majorité des processeurs VLIW récents utilise des bundles de longueur variable, supprimant ces NOP.

Dans la majorité des cas, ces unités VLIW sont capables de traiter deux instructions arithmétiques en parallèles : une qui sera appliquée aux couleurs R, G, et B, et une autre qui sera appliquée à la couleur de transparence. Cette possibilité s'appelle la co-issue.

Jeu de registres[modifier | modifier le wikicode]

Un processeur de shaders contient beaucoup de registres, sans quoi il ne pourrait pas faire son travail efficacement. Sur les processeurs de vertices des anciennes cartes 3D, il existait un grand nombre de registres spécialisés. Certains registres étaient spécialisés dans le stockage des vertices, d'autres dans le stockage des résultats de calculs, d'autres dans le stockage de constante, etc. Il y avait bien quelques registres généraux, sans fonction préétablie, mais ils étaient secondés par un grand nombre de registres spécialisés assez nombreux. De nos jours, ce n'est plus le cas : tous les registres sont banalisés et peuvent stocker toute donnée utile. Les registres spécialisés ont disparu. Le fait est que l'usage de registres spécialisés a perdu de son intérêt avec l'unification des shaders, certains registres n'ayant de sens que pour les vertices et rien d'autre.

Les registres des processeurs de vertices peuvent se classer en plusieurs types :

  • des registres généraux, qui peuvent mémoriser tout type de données ;
  • des registres d'entrée, qui réceptionnent les vertices ou pixels ;
  • des registres de sortie, dans lesquelles le processeur stocke ses résultats finaux ;
  • des registres de constantes qui, comme leur nom l'indique, servent à stocker des constantes.

Un processeur de vertices contenait, en plus des registres généraux, des registres de constantes pour stocker les matrices servant aux différentes étapes de transformation ou d'éclairage. Ces constantes sont placées dans ces registres lors du chargement du vertex shader dans la mémoire vidéo : les constantes sont chargées un peu après. Toutefois, le vertex shader peut écrire dans ces registres, au prix d'une perte de performance particulièrement violente. Le choix du registre de constante à utiliser s'effectue en utilisant un registre d'adresse de constante. Celui-ci va permettre de préciser quel est le registre de constante à sélectionner dans une instruction. Une instruction peut ainsi lire une constante depuis les registres constants, et l'utiliser dans ses calculs.

Architecture d'un processeur de shaders avec accès aux textures.

Microarchitecture[modifier | modifier le wikicode]

Outre le jeu d’instruction, la conception interne (aussi appelée microarchitecture) des processeurs de shaders possède quelques particularités idiosyncratiques. Rien de bien déroutant pou qui a déjà étudié les architectures à parallélisme de données, mais quelques rappels ou explications ne peuvent pas faire de mal.

Les cartes graphiques sont des architectures massivement parallèles, à savoir qu'elles sont capables de faire un très grand nombre de calculs simultanés, durant le même cycle d'horloge. Il faut dire que chaque vertice ou pixel peut être traité indépendamment des autres, ce qui rend le traitement 3D fortement parallèle. Cela a quelques conséquences sur le nombre de processeurs et d'unités de calcul, ainsi que sur la hiérarchie mémoire. L'architecture doit être conçue pour pouvoir effectuer un maximum de calculs en parallèle, quitte à simplifier fortement la puissance pour des calculs séquentiels (ceux qu'effectue un processeur). La hiérarchie mémoire doit aussi gérer un grand nombre d'accès mémoire simultanés : qui dit plein d'instructions en parallèle qui travaillent sur des données indépendantes dit aussi plein de données indépendantes à lire ou écrire !

L'architecture d'une carte grahique récente est illustrée ci-dessous. On y voit plusieurs caractéristiques typiques : un grand nombre de processeurs/cœurs, un grand nombre d'unités de calculs par cœur, et une hiérarchie mémoire assez étagée avec des mémoires locales en complément de la mémoire vidéo principale.

Ce schéma illustre l'architecture d'un GPU en utilisant la terminologie NVIDIA. Comme on le voit, la carte graphique contient plusieurs cœurs de processeur distincts. Chacun d'entre eux contient plusieurs unités de calcul généralistes, appelées processeurs de threads, qui s'occupent de calculs simples (en bleu). D'autres calculs plus complexes sont pris en charge par une unité de calcul spécialisée (en rouge). Ces cœurs sont alimentés en instructions par le processeur de commandes, ici appelé Thread Execution Control Unit, qui répartit les différents shaders sur chaque cœur. Enfin, on voit que chaque cœur a accès à une mémoire locale dédiée, en plus d'une mémoire vidéo partagée entre tous les cœurs.

Un grand nombre de processeurs et d'unités de calculs[modifier | modifier le wikicode]

Comparaison du nombre de processeurs et de cœurs entre CPU et GPU.

Pour profiter au mieux des opportunités de parallélisme, une carte graphique contient de nombreux processeurs, qui eux-mêmes contiennent plusieurs unités de calcul. Savoir combien de cœurs contient une carte graphique est cependant compliqué, les services marketing gardant un certain flou sur le sujet. Il n'est pas rare que ceux-ci appellent cœurs ou processeurs de simples unités de calcul, histoire de gonfler les chiffres. Et on peut généraliser à la majorité de la terminologie utilisée par les fabricants, que ce soit pour les termes warps processor, ou autre, qui ne sont pas aisés à interpréter. D'ordinaire, ce qui est appelé processeur de thread sur une carte graphique correspond en réalité à une unité de calcul.

Vu le grand nombre d'unités de calcul, les autres circuits ne peuvent pas trop prendre de place. En conséquence, les unités de décodage et/ou de contrôle sont relativement simples, peu complexes. Leurs fonctionnalités sont limitées au strict minimum, avec cependant quelques optimisations sur les cartes graphiques récentes. On n'y retrouve pas les fioritures des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées.

Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.

Des unités de calcul autrefois spécialisée, puis devenus banalisées[modifier | modifier le wikicode]

Même les premiers processeurs de shaders disposaient de plusieurs unités de calculs séparées, capables de faire des calculs en parallèle. Un bon exemple est le processeur de vertices de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient trois unités de calculs séparées : une unité généraliste de calcul sur les vertices, une unité pour les opérations mathématiques complexes (division, racine carrée, racine carrée inverse, autres) et enfin une unité pour le calcul Multiply-And-Add (une multiplication suivie d'une addition, opération très courante en 3D). On voit que les unités de calculs sont assez spécialisées, avec une ou plusieurs unités généralistes, secondée par des unités capables de faire des calculs spécialisés.

Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.

Cela n'a pas changé avec l'unification des shaders, si ce n'est que les unités de calcul ne sont pas aussi spécialisées que dans l'exemple précédent. De nos jours, les unités de calculs dédiées à des opérations bien précises sont plus rares. On ne trouve plus d'unité spécialisée pour les opérations complexes, comme dans l'exemple précédent, de telles opérations pouvant être émulées par une suite d'opérations plus simples. Au lieu de mettre une unité spécialisée utile pour une opération sur 10/20, autant mettre une unité généraliste pour accélérer les calculs simples et fréquents, quitte à émuler les calculs complexes. De plus, les unités de calculs sont devenues beaucoup plus nombreuses.

Les GPU modernes disposent d'une flopée d'unités de calcul SIMD identiques, à savoir qu'elles calculent des instructions SIMD complètes, chaque pixel étant traité en parallèle.

Unités de calculs SIMD.

Mitigation de la latence mémoire[modifier | modifier le wikicode]

Tous les pixels doivent accéder à une texture pour être coloriés, certains traitements devant être effectués ensuite par un pixel shader. Mais un accès à une texture, c'est long : une bonne centaine de cycles d'horloges lors d'un accès à une texture est un minimum si celle-ci est lue depuis la mémoire vidéo. Pour éviter que le processeur de shaders attende la mémoire, celui-ci dispose de techniques élaborées.

Une forme limitée d’exécution dans le désordre[modifier | modifier le wikicode]

L'unité de texture est située dans le processeur de shaders, à côté des unités de calcul. L'unité de texture peut fonctionner en parallèle des unités de calcul, comme toute unité d'accès mémoire. Ainsi, on peut poursuivre l’exécution du shader en parallèle de l'accès mémoire, à condition que les calculs soient indépendants de la donnée lue. Dans ces conditions, un shader peut masquer a latence de l'acccès mémoire en exécutant une grande quantité d'instructions à exécuter en parallèle : si un accès mémoire dure 200 cycles d'horloge, le processeur de shader doit disposer de 200 instructions à exécuter pour masquer totalement l'accès à la texture. De plus, le shader effectue souvent plusieurs accès mémoire assez rapprochés : si l'unité de texture ne peut pas gérer plusieurs lectures en parallèle, la lecture la plus récente est mise en attente et bloque toutes les instructions qui la suivent.

Multi-threading matériel[modifier | modifier le wikicode]

Trouver suffisamment d’instructions indépendantes d'une lecture dans un shader n'est donc pas une chose facile. Les améliorations au niveau du compilateur de shaders des drivers peuvent aider, mais la marge est vraiment limitée. Pour trouver des instructions indépendantes d'une lecture en mémoire, le mieux est encore d'aller chercher dans d'autres shaders… Sans la technique qui va suivre, chaque shader correspond à un programme qui s’exécute sur toute une image. Avec les techniques de multi-threading matériel, chaque shader est dupliqué en plusieurs copies indépendantes, des threads, qui traitent chacun un morceau de l'image. Un processeur de shader peut traiter plusieurs threads, et répartir les instructions de ces threads sur l'unité de calcul suivant les besoins : si un thread attend la mémoire, il laisse l'unité de calcul libre pour un autre.

SIMT[modifier | modifier le wikicode]

Les processeurs plus récents fonctionnent comme des processeurs SIMD au niveau de l'unité de calcul, mais ce fonctionnement est masqué au niveau du jeu d'instruction. Ces processeurs poussent la logique des threads jusqu'au bout : chaque instance de shader (thread) ne manipule qu'un seul pixel ou vertex. Ces threads sont rassemblés en groupes de 16 à 32 threads qui exécutent la même instruction, en même temps, mais sur des pixels différents. En clair, ces processeurs vont découvrir à l’exécution qu'ils peuvent exécuter la même instruction sur des pixels différents, et fusionner leurs instructions en instructions vectorielles. L'instruction vectorielle née de cette fusion est appelée un warp. On parle de Single Instruction Multiple Threads.

Chaque thread se voit attribuer un Program Counter, des registres, et un identifiant qui permet de l'identifier parmi tous les autres. Un circuit spécialisé fusionne les pixels des threads en vecteurs qu'il distribue aux unités de calcul. Sur certaines cartes graphiques récentes, le processeur peut démarrer l'exécution de plusieurs warps à la fois. Il faut noter que si un branchement ne donne pas le même résultat dans différents threads d'un même warp, le processeur se charge d'effectuer la prédication en interne : il utilise quelque chose qui fait le même travail que des instructions de prédication qui utilisent vector mask register. Dans ce cas, chaque thread est traité un par un par l'unité de calcul. Ce mécanisme se base sur une pile matérielle qui mémorise les threads à exécuter, dans un certain ordre.


Les Render Output Target

R.O.P des GeForce 6800.

Pour rappel, nos fragments ne sont pas tout à fait des pixels. Il s'agit de données qui vont permettre, une fois combinées, d'obtenir la couleur finale d'un pixel. Ceux-ci contiennent diverses informations, comme leur position à l'écran, leur profondeur, leur couleur, ainsi que quelques autres informations potentiellement utiles. Une fois que nos fragments se sont vus appliquer une texture, il faut les enregistrer dans la mémoire, afin de les afficher. On pourrait croire qu'il s'agit là d'une opération très simple, mais ce n'est pas le cas. Il reste encore un paquet d’opérations à effectuer sur nos pixels : la profondeur des fragments doit être gérée, de même que la transparence, etc. Elles sont réalisées dans un circuit qu'on nomme le Render Output Target. Celui-ci est le tout dernier circuit, celui qui enregistre l'image finale dans la mémoire vidéo. Ce chapitre va aborder ce circuit dans les grandes lignes. Dans ce chapitre, nous allons voir celui-ci.

La gestion de la profondeur (tests de visibilité)[modifier | modifier le wikicode]

Pour commencer, il va falloir trier les fragments par leur profondeur, pour gérer les situations où un triangle en cache un autre (quand un objet en cache un autre, par exemple). Prenons un mur rouge qui cache un mur bleu. Dans ce cas, un pixel de l'écran sera associé à deux fragments : un pour le mur rouge, et un pour le bleu. De tous les fragments, un seul doit être choisi : celui du mur qui est devant. Pour cela, la profondeur d'un fragment dans le champ de vision est calculée à la rasterization. Cette profondeur est appelée la coordonnée z. Par convention, plus la coordonnée z est petite, plus l'objet est prêt de l'écran. Cette coordonnée z est un nombre, codé sur plusieurs bits.

Petite précision : il est assez rare qu'un objet soit caché seulement par un seul objet. En moyenne, un objet est caché par 3 à 4 objets dans un rendu 3d de jeu vidéo.

Le Z-buffer[modifier | modifier le wikicode]

Z-buffer correspondant à un rendu

Pour savoir quels fragments sont à éliminer (car cachés par d'autres), notre carte graphique va utiliser ce qu'on appelle un tampon de profondeur. Il s'agit d'un tableau, stocké en mémoire vidéo, qui va mémoriser la coordonnée z de l'objet le plus proche déjà rendu pour chaque pixel. On peut préciser qu'il existe des variantes du tampon de profondeur, qui utilisent un codage de la coordonnée de profondeur assez différent. Ils se distinguent du tampon de profondeur par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Avec eux, la précision est meilleure pour les fragments proches de la caméra, et plus faible pour les fragments éloignés. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence.

Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les objets seront calculés, la coordonnée z stockée dans ce tampon de profondeur sera mise à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un pixel à calculer a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu, et il n'a pas à être calculé. Dans le cas contraire, le fragment reçu est plus près de la caméra, et il est rendu : sa coordonnée z va remplacer l'ancienne valeur z dans le tampon de profondeur.

Illustration du processus de mise à jour du Z-buffer.

Si deux objets sont suffisamment proches, le tampon de profondeur n'aura pas la précision suffisante pour discriminer les deux objets : pour lui, les deux objets seront à la même place. Conséquence : il faut bien choisir un des deux objets. Si l'objet choisi est le mauvais, des artefacts visuels apparaissent. Voici ce que cela donne :

Z-fighting

Le circuit de gestion de la profondeur[modifier | modifier le wikicode]

La profondeur est gérée par un circuit spécialisé. La sortie du pixel shader fournit la coordonnée z d'un fragment et d'autres coordonnées. A partir de ces informations, le ROP lit la coordonnée z correspondante dans le tampon de profondeur, compare celle-ci avec la coordonnée z du fragment reçu et décide s'il faut mettre à jour le frame-buffer et le tampon de profondeur. En conséquence, ce circuit va devoir effectuer des lectures et des écritures en mémoire vidéo. Or, la mémoire est déjà mise à rude épreuve avec les lectures de vertices et de textures. Diverses techniques existent pour limiter l'utilisation de la mémoire, en diminuant la quantité de mémoire vidéo utilisée et le nombre de lectures et écritures dans celle-ci.

AMD HyperZ

Une première solution consiste à compresser le tampon de profondeur. Évidemment, les données devront être compressées avant d'être stockée ou lue dans le tampon de profondeur. Pour donner un exemple, nous allons prendre la z-compression des cartes graphiques ATI radeon 9800. Cette technique de compression découpait des morceaux de 8 * 8 fragments, et les encodait avec un algorithme nommé DDPCM : Differential differential pulse code modulation. Ce découpage du tampon de profondeur en morceaux carrés est souvent utilisé dans la majorité des circuits de compression et de décompression de la profondeur. Toutefois, il arrive que certains de ces blocs ne soient pas compressés : tout dépend si la compression permet de gagner de la place ou pas . On trouve un bit au tout début de ce bloc qui indique s'il est compressé ou non.

Entre deux images, le tampon de profondeur doit être remis à zéro. La technique la moins performante consiste à réécrire tout son contenu avec la valeur maximale. Pour éviter cela, chaque bloc contient un bit : si ce bit est positionné à 1, alors le ROP va faire comme si le bloc avait été remis à zéro. Ainsi, au lieu de réécrire tout le bloc, il suffit de récrire un bit par bloc. Le gain en nombre d'accès mémoire peut se révéler assez impressionnant.

Une dernière optimisation possible consiste à ajouter une mémoire cache qui stocke les derniers blocs de coordonnées z lues ou écrites depuis la mémoire. Comme cela, pas besoin de les recharger plusieurs fois : on charge un bloc une fois pour toutes, et on le conserve pour gérer les fragments qui suivent.

La gestion de la transparence (Blending)[modifier | modifier le wikicode]

En plus de la profondeur, il faut aussi gérer la transparence, une sorte de couleur ajoutée aux composantes RGB qui indique si un pixel est plus ou moins transparent. Et là, c'est le drame : que se passe-il si un fragment transparent est placé devant un autre fragment ? Je vous le donne en mille : la couleur du pixel calculée avec l'aide du tampon de profondeur ne sera pas la bonne, vu que le pixel transparent ne cache pas totalement l'autre. Sur le principe, la couleur sera un mélange de la couleur du fragment transparent, et de la couleur du (ou des) fragments placé·s derrière. Le calcul à effectuer est très simple, et se limite en une simple moyenne pondérée par la transparence de la couleur des deux pixels.

Application de textures.

Les pixels étant calculés uns par uns par les unités de texture et de shaders, le calcul des couleurs est effectué progressivement. Pour cela, la carte graphique doit mettre en attente les résultats temporaires des mélanges pour chaque pixel. C'est le rôle du tampon de couleur, une portion de la mémoire vidéo. À chaque fragment envoyé dans le ROP, celui-ci va lire la couleur dans le tampon de couleur, faire la moyenne pondérée avec le fragment reçu et enregistrer le résultat.

Certaines vieilles cartes graphiques possédaient une « optimisation » assez intéressante : l'alpha test. Cette technique consistait à ne pas enregistrer en mémoire les fragments dont la couleur alpha était inférieure à un certain seuil. De nos jours, cette technologie est devenue obsolète.

Les effets de brouillard[modifier | modifier le wikicode]

Le ROP peut aussi ajouter des effets de brouillard dans notre scène 3D. Ce brouillard sera simplement modélisé par une couleur, la couleur de brouillard, qui est mélangée avec la couleur du pixel calculée par un simple calcul de moyenne. La carte graphique stocke une couleur de brouillard de base, sur laquelle elle effectuera un calcul pour déterminer la couleur de brouillard à appliquer au pixel. En dessous d'une certaine distance fogstart, la couleur de brouillard est nulle : il n'y a pas de brouillard. Au-delà d'une certaine distance fogend, l'objet est intégralement dans le brouillard : seul le brouillard est visible. Entre les deux, la couleur du brouillard et de l'objet devront toutes les deux être prises en compte dans les calculs. Les premières cartes graphiques calculaient une couleur de brouillard pour chaque vertice, dans les unités de vertices. Sur les cartes plus récentes la couleur de brouillard définitivement était calculée dans les ROP, en fonction de la coordonnée de profondeur du fragment.

Les ROP de couleur[modifier | modifier le wikicode]

Ces opérations de test et de blending sont effectuées par un circuit spécialisé qui travaille en parallèle du Depth-ROP : le Color ROP. Il va ainsi mélanger et tester nos couleurs pendant que le Depth-ROP effectue ses comparaisons entre coordonnées z. Et comme toujours, les lectures et écritures de couleurs peuvent saturer la mémoire vidéo. On peut diminuer la charge de la mémoire vidéo en ajoutant une mémoire cache, ou en compressant les couleurs. Il est à noter que sur certaines cartes graphiques, l'unité en charge de calculer les couleurs peut aussi servir à effectuer des comparaisons de profondeur. Ainsi, si tous les fragments sont opaques, on peut traiter deux fragments à la fois. C'était le cas sur la Geforce FX de Nvidia, ce qui permettait à cette carte graphique d'obtenir de très bonnes performances dans le jeu DOOM3.

L'antialiasing[modifier | modifier le wikicode]

Le ROP prend en charge l'anti-aliasing, une technologie qui permet d'adoucir les bords des objets. Le fait est que dans les jeux vidéos, les bords des objets sont souvent pixelisés, ce qui leur donne un effet d'escalier. Le filtre d'anti-aliasing rajoute une sorte de dégradé pour adoucir les bords des lignes.

Anti-aliasing demo

Les différents types d'anti-aliasing[modifier | modifier le wikicode]

Supersampling

Il existe un grand nombre de techniques d'antialiasing différentes. Toutes ont des avantages et des inconvénients en termes de performances ou de qualité d'image. La première de ces techniques, le SSAA - Super Sampling Anti Aliasing - calcule l'image à une résolution supérieure, avant de la réduire. Par exemple, si je veux rendre une image en 1280*1024, la carte graphique va calculer une image en 2560 * 2048, avant de la réduire. Pour effectuer la réduction de l'image, notre ROP va découper l'image en blocs de 4, 8, 16 pixels, et va effectuer un « mélange » des couleurs de tout le bloc. Ce « mélange » est en réalité une série d'interpolations linéaires, comme montré dans le chapitre sur le filtrage des textures, mais avec des couleurs de fragments. Si vous regardez les options de vos pilotes de carte graphique, vous verrez qu'il existe plusieurs réglages pour l'antialiasing : 2X, 4X, 8X, etc. Cette option signifie que l'image calculé par la carte graphique contiendra respectivement 2, 4, ou 8 fois plus de pixels que l'image originale. Cette technique filtre toute l'image, y compris l'intérieur des textures, mais augmente la consommation de mémoire vidéo et de processeur (on calcule 4 fois plus de pixels).

Pour réduire la consommation de mémoire induite par le SSAA, il est possible d'améliorer celui-ci pour faire en sorte qu'il ne filtre pas toute l'image, mais seulement les bords des objets, seuls endroit où l'effet d'escalier se fait sentir. On parle alors de Multi-Sampling Anti-Aliasing, abrévié en MSAA. Avec le MSAA, l'image à afficher est rendue dans une résolution supérieure, mais les fragments sont regroupés en carrés qui correspondent à un pixel. Avec le SSAA, chaque sous-pixel se verrait appliquer un morceau de texture. Avec le MSAA, les textures ne s'appliquent pas aux sous-pixels, mais à un bloc complet. La couleur finale dépend de la position du sous-pixel : est-il dans le triangle qui lui a donné naissance (à l'étape de rasterization), ou en dehors ? Si le sous-pixel est complétement dans le triangle, sa couleur sera celle de la texture. Si le sous-pixel est en dehors du triangle, sa couleur est mise à zéro. Pour obtenir la couleur finale du pixel à afficher, le ROP va faire la moyenne des couleurs des sous-pixels du bloc. Niveau avantages, le MSAA n'utilise qu'un seul filtrage de texture par pixel, et non par sous-pixel comme avec le SSAA. Mais le MSAA ne filtre pas l'intérieur des textures, ce qui pose problème avec les textures transparentes. Pour résoudre ce problème, les fabricants de cartes graphiques ont créé diverses techniques pour appliquer l'antialiasing à l'intérieur des textures alpha.

Comme on l'a vu, le MSAA utilise une plus grande quantité de mémoire vidéo. Le Fragment Anti-Aliasing, ou FAA, cherche à diminuer la quantité de mémoire vidéo utilisée par le MSAA. Il fonctionne sur le même principe que le MSAA, à un détail prêt : il ne stocke pas les couleurs pour chaque sous-pixel, mais utilise à la place un masque. Dans le color-buffer, le MSAA stocke une couleur par sous-pixels, couleur qui peut prendre deux valeurs : soit la couleur calculée lors du filtrage de texture, soit la couleur noire (par défaut). A la place, le FAA stockera une couleur, et un petit groupe de quelques bits. Chacun de ces bits sera associé à un des sous-pixels du bloc, et indiquera sa couleur : 0 si le sous-pixel a la couleur noire (par défaut) et 1 si la couleur est à lire depuis le color-buffer. Le ROP utilisera ce masque pour déterminer la couleur du sous-pixel correspondant. Avec le FAA, la quantité de mémoire vidéo utilisée est fortement réduite, et la quantité de donnée à lire et écrire pour effectuer l'antialiasing diminue aussi fortement. Mais le FAA a un défaut : il se comporte assez mal sur certains objets géométriques, donnait naissance à des artefacts visuels.

La position des sous-pixels[modifier | modifier le wikicode]

Un point important concernant la qualité de l'antialiasing concerne la position des sous-pixels sur l'écran. Comme vous l'avez vu dans le chapitre sur la rasterization, notre écran peut être vu comme une sorte de carré, dans lequel on peut repérer des points. Reste que l'on peut placer ces pixels n'importe où sur l'écran, et pas forcément à des positions que les pixels occupent réellement sur l'écran. Pour des pixels, il n'y a aucun intérêt à faire cela, sinon à dégrader l'image. Mais pour des sous-pixels, cela change tout. Toute la problématique peut se résumer en une phrase : où placer nos sous-pixels pour obtenir une meilleure qualité d'image possible.

  • La solution la plus simple consiste à placer nos sous-pixels à l'endroit qu'ils occuperaient si l'image était réellement rendue avec la résolution simulée par l'antialiasing. Cette solution gère mal les lignes pentues, le pire cas étant les lignes penchées de 45 degrés par rapport à l'horizontale ou la verticale.
  • Pour mieux gérer les bords penchés, on peut positionner nos sous-pixels comme suit. Les sous-pixels sont placés sur un carré penché (ou sur une ligne si l'on dispose seulement de deux sous-pixels). Des mesures expérimentales montrent que la qualité optimale semble être obtenue avec un angle de rotation de arctan(1/2) (26,6 degrés), et d'un facteur de rétrécissement de √5/2.
  • D'autres dispositions sont possibles, notamment une disposition aléatoire ou de type Quincunx.


L'élimination précoce des pixels cachés

Précédemment, dans ce cours, nous avons vu que certaines portions d'une scène 3D ne sont pas affichées à l'écran. Soit parce qu'elles sont en-dehors du champ de vision de la caméra, soit parce qu'elles sont masquées par un autre objet, ou encore parce que la face avant d'un objet cache la partie arrière. Il est primordial ne ne pas texturer et calculer les portions de l'image qui ne sont pas visibles à l'écran, en utilisant des algorithmes de culling adaptés, mais ce n'est pas facile à faire. Il est possible d'éliminer rapidement les parties arrières cachées des objets, ainsi que ce qui est hors du champ de vision de la caméra, dès l'étape de rasterisation, soit assez tôt. Mais les objets cachés par d'autres plus proches posent problèmes, car ils sont éliminés par le z-buffering, qui est effectué dans les ROP. Et c'est sans compter les effets de transparence, qui font qu'un objet devant un autre peut ne pas le masquer. En conséquence, de nombreux pixels inutiles seront coloriés et éclairés par les pixels shaders, sans qu'on puisse y faire grand-chose.

Cependant, la profondeur d'un pixel est connue dès la fin de l'étape de rasterisation, ce qui permet en théorie de détecter précocement quel pixel est devant un autre. Mais si le z-buffering est effectué dans les ROPs, c'est qu'il y a une bonne raison. En effet, sur les cartes graphiques usuelles, l'ordre de soumission des triangles est aléatoire. Un objet peut en cacher un autre sans que ces deux objets soient rendus consécutivement. Si on utilise le Z-buffering, cela ne pose aucun problème, car il est conçu pour en tenir compte. Mais effectuer un test de profondeur lors de la rasterization ne fonctionne que si les triangles arrivent du plus loin au plus proche. En théorie, cela ne pose pas de problèmes si l'on trie les triangles en fonction de leur profondeur, mais effectuer ce tri est nettement plus lent que d'effectuer le test de profondeur dans les ROP. Il existe cependant des solutions alternatives et optimisations qui permettent de régler partiellement ce problème, et elles font l'objet de ce chapitre.

Le Tiled rendering[modifier | modifier le wikicode]

Tiled rendering architecture.

La première solution demande d'utiliser une classe de carte 3D légèrement différente de celles vues précédemment. Sur ces architectures, l'écran/image à rendre est découpé en rectangles, rendus indépendamment, uns par uns. Ces rectangles sont appelés des tiles, d'où le nom d'architectures à tiles donné à ce type de cartes graphiques.

Le rendu se fait tile par tile. Le rendu commence par calculer tout ce qui a trait à la géométrie associée à une tile, avant d'en rendre les pixels. Cette séparation entre rendu de la géométrie et traitement des pixels n'est pas anodin et ressemble à ce qui se fait avec la rasterisation normale. Sauf qu'ici, le calcul de la géométrie ne se fait pas triangle par triangle mais tile par tile. La carte 3D calcule toute la géométrie d'une tile, puis mémorise le résultat en mémoire vidéo et effectue ensuite le traitement des pixels de la tile. Cette séparation est intéressante car elle permet à la carte graphique de calculer la géométrie de plusieurs tiles pendant que le traitement des pixels est en train de se faire. Les tiles calculées en avance sont placées dans une file d'attente en mémoire vidéo, dont elles sortent pour subir l'étape de traitement des pixels.

L'élimination des pixels et triangles cachés s'effectue dès que la profondeur est disponible, c'est à dire à l'étape de rasterization. Sur ces architectures, le résultat des calculs géométriques est mémorisé en mémoire vidéo, avant d'être traité tile par tile. Chaque tile se voit attribuer la liste des triangles qu'elle contient : cette liste est appelée la Display List, et elle est enregistrée en mémoire vidéo. Par la suite, il suffit de rasterizer, placer les textures et exécuter les shaders chaque tile, avant d'envoyer le tout aux ROP. L'architecture globale d'une carte graphique à tiles change peu comparé à une carte à rasterization, si ce n'est que le rasterizer est modifié et qu'il est suivi d'une unité d'élimination des pixels cachés : l'Image Synthesis Processor, ou ISP.

Le rasterizer se voit ajouter un nouveau rôle : décider dans quelle tile se trouve un triangle. Pour cela, le rasterizer va calculer le rectangle qui contient un triangle (souvenez-vous le chapitre sur la rasterization), et va vérifier das quelle tile celui-ci est inclus : cela demande de faire quelques comparaison entre les sommets du rectangle et les sommets des tiles. L'Image Synthesis Processor remplace en quelque sorte le Z-Buffer et les circuits d'élimination des pixels cachés. Une architecture à tile a juste besoin d'un Z-Buffer pour la tile en cours de traitement, là où les cartes graphiques normales ont besoin d'un Z-buffer pour toute l'image. De plus, les tiles sont tellement petites que l'on peut stocker tout le Z-Buffer dans une mémoire tampon integrée dans l'ISP. Cette mémoire tampon réduit fortement les besoins en bande passante et en débit mémoire, ce qui rend inutile de nombreuses optimisations, comme la compression du Z-buffer.

Early-Z[modifier | modifier le wikicode]

Les architectures à base de Tiles ne sont pas la seule solution pour éviter le calcul des pixels cachés. Les concepteurs de cartes graphiques usuelles (sans tiled rendering) ont inventé des moyens pour détecter une partie des pixels qui ne seront pas visibles, avant que ceux-ci n'entrent dans l'unité de texture. Ces techniques sont des techniques d'early-Z. Mais ces techniques nuisent au rendu si les shaders peuvent bidouiller la profondeur ou la transparence d'un pixel. Pour éliminer tout problème, les drivers de la carte graphique doivent analyser les shaders et décider si le test de profondeur précoce peut être effectué ou non. Il existe plusieurs techniques d'early-Z, qui sont présentes depuis belle lurette dans nos cartes graphiques. Celles-ci peuvent être classées en deux catégories : le zmax, et le zmin. Il est parfaitement possible d'utiliser le zmax conjointement avec le zmin, ce qui donne des techniques hybrides.

Z-Max[modifier | modifier le wikicode]

Les deux techniques z-max et z-min découpent l'écran en tiles. Le Z-max consiste à vérifier si la tile à rendre est situé derrière des tiles déjà rendues pour la masquer cas échéant. Pour cela, il suffit de savoir quelle est la tile la plus profonde déjà rendue. Précisémment, il suffit de conserver la profondeur de cette tile et de faire les vérifications de profondeur. Le zmax consiste donc à vérifier si le triangle à rendre est situé derrière le pixel le plus profond de la tile. Ces techniques ont un gros défaut : il faut calculer la valeur maximale des pixels de la tile, ce qui demande de comparer les profondeurs de tous les pixels. Ce genre de chose s'effectue dans les ROPs, et demande parfois de lire les profondeurs depuis la mémoire vidéo…

La première technique de Z-Max est celle du Hierarchical Z. Dans les grandes lignes, cette technique consiste à conserver dans une mémoire cache (rarement en mémoire vidéo) une copie basse-résolution du tampon de profondeur, qui mémorise la valeur maximale de la profondeur pour chaque tile. Cette copie basse-résolution est mise à jour par les ROPs, en même temps que le Z-Buffer. Il existe d'autres techniques qui permettent d'éliminer ce genre de problèmes, comme le Depth Filter ou le Mid-texturing.

Z-Min[modifier | modifier le wikicode]

Avec le Z-min, on utilise la profondeur maximale des sommets du triangle dans les calculs. Cette valeur est comparée avec la valeur de profondeur minimale dans la tile. Si la profondeur du pixel à rendre est plus petite, cela veut dire que le pixel n'est pas caché et qu'il n'y a pas besoin d'effectuer de test de profondeur dans les ROPs. Le calcul de la profondeur minimale de la tile est très simple : il suffit de mémoriser la plus petite valeur rencontrée et la mettre à jour à chaque rendu de pixel. Par besoin de lire toutes les profondeurs de la tile d'un seul coup, ou quoique ce soit d'autre, comme avec le zmax. Cette méthode est particulièrement adaptée aux rendus dans lesquels les recouvrements de triangles sont relativement rares. Il faut dire que cette méthode ne rejette pas beaucoup de pixels comparé à la technique du zmax. En contrepartie, elle n'utilise pas beaucoup de circuits comparé au zmax : c'est pour cela qu'elle est surtout utilisée dans les cartes graphiques pour mobiles.

GFDL GFDL Vous avez la permission de copier, distribuer et/ou modifier ce document selon les termes de la licence de documentation libre GNU, version 1.2 ou plus récente publiée par la Free Software Foundation ; sans sections inaltérables, sans texte de première page de couverture et sans texte de dernière page de couverture.