Les cartes graphiques/Les cartes graphiques : architecture de base
Dans ce chapitre, nous allons voir l'architecture de base d'une carte accélératrice 3D, et voir quelle est la distinction entre une carte accélératrice et un GPU. Dans ce chapitre, nous allons faire le lien avec le rendu tel que décrit dans le chapitre précédent. Les cartes graphiques modernes implémentent des circuits programmables, qui seront partiellement laissé de côté dans ce chapitre. Nous allons aussi nous concentrer sur les cartes graphiques à placage de texture inverse, le placage de texture direct ayant déjà été abordé dans le chapitre précédent.
L'architecture d'une carte graphique 3D
[modifier | modifier le wikicode]Une carte accélératrice 3D est un carte d'affichage à laquelle on aurait rajouté des circuits de rendu 3D. Elle incorpore donc tous les circuits présents sur une carte d'affichage : un VDC, une interface avec le bus, une mémoire vidéo, des circuits d’interfaçage avec l'écran, un contrôleur DMA, etc. Le VDC s'occupe de l'affichage et éventuellement du rendu 2D, mais ne s'occupe pas du traitement de la 3D. Du moins, c'est le cas sur les cartes à placage de texture inverse. Le placage de texture direct utilise au contraire un VDC avec accélération 2D très performant, comme nous l'avons vu au chapitre précédent. Mais nous mettons ce cas particulier de côté.
La carte accélératrice 3D reçoit des commandes graphiques, qui proviennent du pilote de la carte graphique, exécuté sur le processeur. les commandes en question sont très variées, avec des commandes de rendu 3D, de rendu 2D, de décodage/encodage vidéo, des transferts DMA, et bien d'autres. Mais nous allons nous concentrer sur les commandes de rendu 3D, qui demandent à la carte accélératrice 3D de faire une opération de rendu 3D. Pour cela, elles précisent quel tampon de sommet utiliser, quelles textures utiliser, quels shaders sont nécessaires, etc.
La carte accélératrice 3D traite ces commandes grâce à deux circuits : des circuits de rendu 3D, et un chef d'orchestre qui dirige ces circuits de rendu pour qu'ils exécutent la commande demandée. Le chef d'orchestre s'appelle le processeur de commandes, et il sera vu en détail dans quelques chapitres. Pour le moment, nous allons juste dire qu'il s'occupe de la logistique, de la répartition du travail. Pour les commandes de rendu 3D, il commande les différentes étapes du pipeline graphique et s'assure que les étapes s’exécutent dans le bon ordre.

Les circuits de rendu 3D regroupent des circuits hétérogènes, aux fonctions fort différentes. Dans le cas le plus simple, il y a un circuit pour chaque étape du pipeline graphique. De tels circuits sont appelés des unités de traitement graphique. On trouve ainsi une unité pour le placage de textures, une unité de traitement de la géométrie, une unité de rasterization, une unité d'enregistrement des pixels en mémoire appelée ROP, etc. Les anciennes cartes graphiques fonctionnaient ainsi, mais on verra que les cartes graphiques modernes font un petit peu différemment.
Pour simplifier les explications, nous allons séparer la carte graphique en deux gros circuits bien distincts. En réalité, ils sont souvent séparés en sous-circuits plus petits, mais laissons cela de côté pour le moment.
- Les unités géométriques pour les calculs géométriques ;
- Les pipelines de pixel qui rastérisent l'image, plaquent les textures, et autres.
Les unités géométriques manipulent des triangles, sommets ou polygones, donc des données géométriques. Les unités de pixel font tout le reste, mais le gros de leur travail est de manipuler des pixels ou des texels.
Les unités géométriques sont soit des processeurs de shaders dédiés, soit des circuits fixes (non-programmables). Leur conception a beaucoup évolué dans le temps. Les toutes premières cartes graphiques, dans les années 80 et 90, utilisaient des processeurs dédiés, programmés avec un firmware dédié. Les cartes grand public du début des années 2000 utilisaient quant à elle des circuits fixes, non-programmables. Et par la suite, les cartes ultérieures sont revenues à des processeurs, mais cette fois-ci programmables directement avec des shaders et non un firmware.
Les pipelines de pixels, quant à eux, ont eu une évolution bien plus simple. Avant le milieu des années 2000, elles étaient réalisées par des circuits fixes, non-programmables. Il y avait bien quelques exceptions, mais c'était la norme. Ce n'est qu'avec l'arrivée des pixel shaders que les pipelines de pixels sont devenus programmables. Ils ont alors été implémentés avec plusieurs circuits, dont un processeur de shaders et d'autres circuits non-programmables. Et il est intéressant de voir quels sont ces circuits.
Les circuits de traitement des pixels
[modifier | modifier le wikicode]Parlons un peu plus en détail des pipelines de pixels. Pour mieux comprendre ce qu'elles font, il est intéressant de regarder ce qu'il y a dans un pipeline de pixel. Un pipeline de pixel effectue plusieurs opérations les unes à la suite, dans un ordre bien précis. Et cela explique l'usage du terme "pipeline" pour les désigner. Et ces opérations sont souvent réalisées par des circuits séparés, qui sont :
- Un rastériseur qui fait le lien entre triangles et pixels ;
- Une unité de texture qui lit les textures et les plaque sur les modèles 3D ;
- Un ROP (Raster Operation Pipeline), qui gère grossièrement le tampon de profondeur (z-buffer).
Le circuit de rastérisation prend en charge la rastérisation proprement dite. Pour rappel, la rastérisation projette une scène 3D sur l'écran. Elle fait passer d'une scène 3D à un écran en 2D avec des pixels. Lors de la rastérisation, chaque sommet est associé à un ou plusieurs pixels, à savoir les pixels qu'il occupe à l'écran. Elle fournit aussi diverses informations utiles pour la suite du pipeline graphique : la profondeur du sommet associé au pixel, les coordonnées de textures qui permettent de colorier le pixel.
L'étape de placage de texture lit la texture associée au modèle 3D et identifie le texel adéquat avec les coordonnées textures, pour colorier le pixel. On travaille pixel par pixel, on récupère le texel associé à chaque pixel. Soit l'inverse du placage de texture direct, qui traversait une texture texel par texel, pour recopier le texel dans le pixel adéquat.
Après l'étape de placage de textures, la carte graphique enregistre le résultat en mémoire. Lors de cette étape, divers traitements de post-traitement sont effectués et divers effets peuvent être ajoutés à l'image. Un effet de brouillard peut être ajouté, des tests de profondeur sont effectués pour éliminer certains pixels cachés, l'antialiasing est ajouté, on gère les effets de transparence, etc. Un chapitre entier sera dédié à ces opérations.

Les circuits d'élimination des pixels cachés
[modifier | modifier le wikicode]L'élimination des surfaces cachées élimine les triangles invisibles à l'écran, car cachés par un objet opaque. En théorie, elle est prise en charge à la toute fin du pipeline, dans les ROPs, car cela permet de gérer la transparence. En effet, on ne sait pas si une texture transparente sera plaquée sur le triangle ou non. En clair, on doit éliminer les triangles invisibles après le placage de textures, et donc dans les ROP. Les ROPs se chargent à la fois de l’élimination des pixels cachées et de la transparence, les deux s’influençant l'un l'autre.

Il y a cependant des cas où on sait d'avance que les textures ne sont pas transparentes. Dans ce cas, la carte graphique utilise les circuits d'élimination des pixels cachés juste après la rastérisation. Cela permet d'éliminer à l'avance les triangles dont on sait qu'ils ne seront pas rendus.

Les deux possibilités coexistent sur les cartes graphiques modernes. Une carte graphique moderne peut éliminer les surfaces cachées avant et après la rastérisation, grâce à des techniques d'early-z dont nous parlerons plus tard, dans un chapitre dédié sur la rastérisation.
Les circuits d'éclairage
[modifier | modifier le wikicode]
Les explications précédentes décrivent une carte graphique très simple, qui ne gère pas les techniques d'éclairage. Mais elles ont disparues depuis plusieurs décennies, toutes les cartes graphiques gèrent l'éclairage en matériel depuis les années 2000. Et ces GPU des années 2000 géraient différemment l'éclairage par pixel et l'éclairage par sommet. Pour rappel, l'éclairage par sommet attribue une couleur et une luminosité à chaque sommet. L'éclairage par pixel est plus fin, car il attribue une luminosité pour chaque pixel de l'écran. Les deux étaient gérés autrefois dans des circuits distincts, comme illustré ci-contre.
Les circuits d'éclairage par sommet
[modifier | modifier le wikicode]L'éclairage par sommet est grossièrement calculé dans l'unité géométrique, le circuit de calculs géométriques. L’unité de traitement géométrique peut se mettre en œuvre de deux manières.
- La première utilise un circuit non-programmable, appelé le circuit de Transform & Lightning, qui effectue les calculs d'éclairage par sommet (d'où le L de T&L), en plus des calculs de transformation (le T de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256.
- Une seconde solution utilise un processeur dédié, qui exécute tous les calculs géométriques. Pour cela, il faut fournir un programme qui émule le pipeline géométrique, appelé un vertex shader, dont nous reparlerons d'ici quelques chapitres.
Intuitivement, on se dit que l'unité géométrique calcule une luminosité pour chaque triangle/sommet, comprise entre 0 (très sombre) et 1 (très brillant). Mais en réalité, l'unité de traitement géométrique calcule une couleur RGB pour chaque sommet/triangle, cette couleur de sommet indiquant quelle est sa luminosité. L'avantage est que cela simplifie la combinaison avec les textures et permet d'avoir des lumières colorées.
L'unité de traitement géométrique calcul donc une couleur de sommet, qui est envoyée à l'unité de rastérisation. L'unité de rastérisation calcule la couleur du pixel à partir des trois couleurs de sommet. Pour cela, il y a deux méthodes principales, qui correspondent à l'éclairage plat et l'éclairage de Gouraud, qu'on a vu dans le chapitre précédent. La première méthode attribue la même couleur à chaque pixel d'un triangle, typiquement la moyenne des trois couleurs de sommet. La seconde méthode, celle de l'éclairage de Gouraud, calcule une couleur différente pour chaque pixel du triangle. Le calcul en question est une interpolation, à savoir une sorte de moyenne pondérée.
L'éclairage de Gouraud demande donc d'ajouter un circuit d'interpolation pour les couleurs des sommets. Il fait normalement partie du circuit de rastérisation, comme on le verra plus tard dans le chapitre dédié. Pour donner un exemple, la console de jeu Playstation 1 gérait l'éclairage de Gouraud directement en matériel, mais seulement partiellement. Elle n'avait pas de circuit de T&L, ni de vertex shaders, mais intégrait un circuit pour interpoler les couleurs de chaque sommet.
Enfin, il faut prendre en compte les textures. Pour cela, le pixel texturé est multiplié par la luminosité/couleur calculée par l'unité géométrique. Il y a donc un circuit de combinaison situé après l'unité de texture qui effectue la combinaison/multiplication. Le circuit de combinaison est parfois configurable, à savoir qu'on peut remplacer la multiplication par une addition ou d'autres opérations. Un tel circuit de combinaison s'appelle alors un combiner, dans la vieille nomenclature graphique de l'époque des années 90-2000.

Les circuits d'éclairage par pixel
[modifier | modifier le wikicode]L'éclairage par pixel est implémenté d'une manière totalement différente. Une implémentation naïve ajoute un circuit d'éclairage par pixel dédié, après l'unité de texture. Le circuit d’éclairage par pixel n'utilise pas la couleur de sommet, mais d'autres informations nécessaires pour calculer la luminosité d'un pixel.
Il a existé quelques rares cartes graphiques capables de faire de l'éclairage de Phong en matériel. Un exemple est celui de la Geforce 3, dont l'unité géométrique implémentait des instructions dédiées pour l'algorithme de Phong. L'unité géométrique de la Geforce 3 était programmable, et elle avait une instruction Phong, qui envoyait les normales au rastériseur. Les normales étaient alors interpolées par l'unité de rastérisation, puis utilisées par une unité d'éclairage par pixel dédié, fixe, non-programmable.
La technique précédente doit être adaptée pour implémenter le bump-mapping et le normal-mapping, qui mémorisent des informations d'éclairage dans une texture en mémoire vidéo. La texture contient des informations de relief pour le bump-mapping, des normales précalculées pour le normal-mapping. Pour cela, l'unité d'éclairage par pixel doit être reliée à l'unité de texture, mais l'implémentation matérielle n'est pas aisée.
Un exemple de carte graphique capable de faire cela est celle de la Nintendo DS, la PICA200. Créée par une startup japonaise, elle incorporait un circuit de T&L, un éclairage de Phong, du cel shading, des techniques de normal-mapping, de Shadow Mapping, de light-mapping, du cubemapping, de nombreux effets de post-traitement (bloom, effet de flou cinétique, motion blur, rendu HDR, et autres).

De nos jours, les circuits d'éclairage par pixel ont été remplacés par un processeur de pixel shader. Les processeurs de shaders sont des processeurs très simples, qui exécutent des algorithmes d'éclairage par pixel appelés des pixel shaders. L'avantage est que les programmeurs peuvent coder l'algorithme d'éclairage de leur choix et l'exécuter sur le GPU. Pas besoin d'avoir une unité dédiée par algorithme d'éclairage, on a un processeur de shader à tout faire.
Les processeurs de shaders récupèrent les pixels émis par le rastériseur, exécutent un pixel shader dessus, puis envoient le résultat à la suite du pipeline (aux ROPs). L'unité de texture est inclue dans le processeur de shader, ce qui permet au processeur de shader de lire des textures en mémoire vidéo. Le processeur de shader peut faire ce qu'il veut avec les texels lus, cela va bien au-delà d'opérations de combinaison avec une couleur de sommet. Notez que cela permet de grandement faciliter l'implémentation du bump-mapping et du normal-mapping.
Sur les anciens GPUs, l'unité de texture était le seul moyen pour un processeur de shader d'accéder à la mémoire vidéo, ce qui faisait que les pixels shaders pouvaient lire des textures, rien de plus. Mais de nos jours, les processeurs de shaders sont directement connectés à la mémoire vidéo et peuvent lire ou écrire dedans sans passer par l'unité de texture, ce qui peut servir pour divers algorithmes complexes.

Les cartes graphiques avec plusieurs unités parallèles
[modifier | modifier le wikicode]Plus haut, nous avons décrit une carte graphique basique, très basique, avec seulement quatre unités. Une unité pour les calculs géométriques, un rastériseur, une unité pour les pixels/textures et un ROP. Cependant, les cartes graphiques ayant cette architecture sont très rares, pour ne pas dire inexistantes. Il n'est pas impossible que les toutes premières cartes graphiques aient suivi à la lettre cette architecture, mais même cela n'est pas sur. La raison : toutes les cartes graphiques dupliquent les circuits précédents pour gagner en performance, mais aussi pour s'adapter aux contraintes du rendu 3D.
L'amplification des pixels et son impact sur les cartes graphiques
[modifier | modifier le wikicode]Un triangle prend une certaine place à l'écran, il recouvre un ou plusieurs pixels lors de l'étape de rastérisation. Le nombre de pixels recouvert dépend fortement du triangle, de sa position, de sa profondeur, etc. Un triangle peut donner quelques pixels lors de l'étape de rastérisation, alors qu'un autre va couvrir 10 fois de pixels, un autre seulement trois fois plus, un autre seulement un pixel, etc. Le cas où un triangle ne recouvre qu'un seul pixel est rare, encore que la tendance commence à changer avec les jeux vidéos récents de la décennie 2020 utilisant l'Unreal Engine et la technologie Nanite.
La conséquence est qu'il y a plus de travail à faire sur les pixels que sur les sommets, ce qui a reçu le nom d'amplification des pixels. La conséquence est qu'une unité géométrique prendra un triangle en entrée, l'enverra au rastériseur, qui fournira en sortie un ou plusieurs pixels à éclairer/texturer. Et cette règle un triangle = 1,N pixels fait qu'il y a un déséquilibre entre les calculs géométriques et ce qui suit, que ce soit le placage de textures, l'éclairage par pixel ou l'enregistrement des pixels dans le framebuffer. Et ce déséquilibre a un impact sur la manière dont un conçoit une carte graphique, ancienne comme moderne.
S'il y a une seule unité de texture/pixels, alors le rastériseur envoie chaque pixel à texturer/éclairé un par un à l'unité de pixel. Le rastériseur produits ces pixels un par un, avec un algorithme adapté pour. L'unité géométrique attendra le temps que la rastérisation ait fini de traiter tous les pixels du triangle précédent. Elle calculera le prochain triangle pendant ce temps, mais cela ne fera que limiter la casse si beaucoup de pixels sont générés.
Mais il est possible de profiter de l'amplification des pixels pour gagner en performances. L'idée est que le rastériseur produit plusieurs pixels en même temps, qui sont envoyés à plusieurs unités de texture et d'éclairage par pixel. Un exemple est illustré ci-dessous, avec une seule unité géométrique, mais quatre unités de texture, quatre unités d'éclairage par pixel, et quatre ROPs. Le rastériseur est conçu pour générer quatre pixels d'un seul coup si nécessaire.

La carte graphique précédente a des performances optimales quand un triangle recouvre 4 pixels : tout est fait en une seule passe. Si un triangle ne recouvre que 1, 2 ou 3 pixels, alors le rastériseur produira 1, 2 ou 3 et certaines unités suivant le rastériseur seront inutilisées. Mais si un triangle recouvre plus de 4 pixels, alors les pixels sont générés, texturés, éclairés et enregistrés en RAM par paquets de 4. En clair, la carte graphique peut s'adapter à l'amplification des pixels, mais pas parfaitement. Les GPU récents ont résolu partiellement ce problème avec un système de shaders unifiés, mais qu'on ne peut pas expliquer pour le moment.
Pour donner un exemple du monde réel, les premières cartes graphique de l'entreprise SGI était de ce type. SGI a été une entreprise pinière dans le domaine du rendu en 3D, qui a opéré dans les années 80-90, avant de progressivement décliner et fermer. Elle a conçu de nombreux systèmes de type workstation, donc destinés aux professionnels, avec des cartes graphiques dédiées. le grand public n'avait pas accès à ce genre de matériel, qui était très cher, vu qu'on n'était qu'au tout début de l'informatique. Nous ne détaillerons pas ces systèmes, car ils géraient leur mémoire vidéo d'une manière assez bizarre : elle était éclatée en plusieurs morceaux fusionnés chacun avec un ROP... Mais ils avaient tous une unité géométrique unique reliée à un rastériseur, qui alimentait plusieurs unités de texture/pixel et ROPs.
Plus proche de nous, certaines cartes graphiques pour PC étaient aussi dans ce cas. Les toutes premières cartes graphiques pour PC n'avaient même pas de circuits géométriques, et se contentaient d'un rastériseur, d'unités de texture et de ROPs. Par la suite, la Geforce 256 a introduit une unité géométrique appelée l'unité de T&L. Les cartes graphiques de l'époque ont suivi le mouvement et ont aussi intégrée une unité géométrique presque identique. La Geforce 256 avait une unité géométrique, mais 4 unités de texture, 4 unités d'éclairage par pixel et 4 ROPs.
Le multitexturing : dupliquer les unités de texture
[modifier | modifier le wikicode]Le multi-texturing est une technique très importante pour le rendu 3D moderne. L'idée est de permettre à plusieurs textures de se superposer sur un objet. Divers effets graphiques demandent d'ajouter des textures par-dessus d'autres textures, pour ajouter des détails, du relief, sur une surface pré-existante. Un exemple intéressant vient des jeux de tir : ajouter des impacts de balles sur les murs. Pour cela, on plaque une texture d'impact de balle sur le mur, à la position du tir. Il s'agit là d'un exemple de decals, des petites textures ajoutées sur les murs ou le sol, afin de simuler de la poussière, des impacts de balle, des craquelures, des fissures, des trous, etc.
Le multi-texturing implique que calculer un pixel implique de lire plusieurs textures. En général, un pixel avec multi-texturing demande de lire deux textures, rarement plus. La carte graphique doit alors être capable d'accéder à deux textures en même temps, ou du moins faire semblant que. De plus, elle doit combiner les deux textures pour générer le pixel voulu, ce qui demande d'ajouter un circuit qui combine deux texels (des pixels de texture) pour donner un pixel. La solution la plus simple est de doubler les unités de texture et de combiner les textures dans l'unité d'éclairage par pixel. Résultat : pour une unité d'éclairage par pixel, on a deux unités de textures.
La Geforce 2 et 3 utilisaient cette solution, dont le seul défaut est que la seconde unité de texture était utilisée seulement pour les objets sur lesquels le multi-texturing était utilisé. Les cartes ATI, le concurrent de l'époque de NVIDIA, aujourd'hui racheté par AMD, triplait les unités de texture. Mais cette possibilité était peu utilisée, la majorité des jeux se dépassant pas deux texture max par pixel. C'est sans doute pour cette raison que ce triplement a été abandonné à la génération suivante, les Radeon 9000 et 8500 se contentant de doubler les unités de texture.
| Nom de la carte graphique | Unités géométriques | Unité de texture | Unités de pixel | ROPs |
|---|---|---|---|---|
| Geforce 2 d'entrée de gamme | 1 | 2 | 4 | 2 |
| Geforce 2 milieu/haut de gamme, Geforce 3 | 1 | 4 | 8 | 4 |
| Radeon R100 bas de gamme | 1 | 1 | 3 | 1 |
| Radeon R100 autres | 1 | 2 | 6 | 2 |
L'usage de plusieurs unités géométriques
[modifier | modifier le wikicode]Pour encore augmenter les performances, il est possible d'utiliser plusieurs circuits de calcul géométriques, plusieurs unités géométriques. Et ce peu importe que ces unités soient des processeurs ou des circuits fixes non-programmables. Et pour cela, il existe deux grandes implémentations : utiliser plusieurs processeurs placés en série, ou les mettre en parallèle. Comprendre la première implémentation demande de faire quelques rappels sur les calculs géométriques.
L'usage d'un pipeline géométrique proprement dit
[modifier | modifier le wikicode]Pour rappel, le pipeline géométrique regroupe les quatre étapes suivantes :
- L'étape de chargement des sommets/triangles, qui sont lus depuis la mémoire vidéo et injectés dans le pipeline graphique.
- L'étape de transformation effectue deux changements de coordonnées pour chaque sommet.
- Premièrement, elle place les objets au bon endroit dans la scène 3D, ce qui demande de mettre à jour les coordonnées de chaque sommet de chaque modèle. C'est la première étape de calcul : l'étape de transformation des modèles 3D.
- Deuxièmement, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de transformation de la caméra.
- La phase d'éclairage (en anglais lighting) attribue une couleur à chaque sommet, qui définit son niveau de luminosité : est-ce que le sommet est fortement éclairé ou est-il dans l'ombre ?
- La phase d'assemblage des primitives regroupe les sommets en triangles.
- Les phases de clipping ou le culling agissent sur des sommets/triangles/primitives, même si elles sont souvent regroupées dans l'étape de rastérisation.
Si on met de côté le chargement des sommets/triangles, il est possible de faire tous ces calculs en bloc, dans un seul processeur ou une seule unité de T&L. Mais une autre idée, plus simple, attribue un processeur/circuit pour chaque étape. En faisant cela, on peut traiter plusieurs triangles/sommets en même temps, chacun étant dans une étape différente, chacun dans un processeur/circuit. Ceux qui auront déjà lu un cours d'architecture des ordinateurs reconnaitront la fameuse technique du pipeline, mais appliquée ici à un algorithme plus conséquent.
Les processeurs sont en série, et chaque processeur reçoit les résultats du processeur précédent, et envoie son résultat au processeur suivant. Sauf en début ou en bout de chaine, évidemment. Pour donner un exemple, les premières cartes graphiques de SGI utilisaient 10/12 processeurs enchainés l'un à la suite de l'autre. Les 4 premiers géraient les étapes de transformation, les 6 suivants faisaient les opérations de clipping/culling, les deux derniers faisaient la rastérisation proprement dite.
Pour lisser les transferts de données, il est possible d'ajouter des mémoires FIFOs entre les processeurs. Comme ça, si un processeur est bloqué par un calcul un peu trop long, cela ne bloque pas les processeurs précédents. A la place, le processeur précédent accumule des résultats dans la mémoire FIFOs, qui seront consommé ultérieurement.
En théorie, on peut s'attendre à ce que la performance soit multipliée par le nombre de processeurs. En réalité, les étapes sont rarement équilibrées, certaines étapes prennent beaucoup plus de temps que les autres, ce qui fait que la répartition des calculs n'est pas idéale : certains processeurs attendent que le processeur suivant ait finit son travail. De plus, l'organisation en pipeline entraine des couts de transmission/communication entre étapes, notamment si on utilise des mémoires FIFOs entre processeurs, ce qui est toujours le cas.
Cette implémentation n'a été utilisée que sur les toutes premières cartes graphiques, avant l'apparition des PC grand public. Les systèmes SGI, utilisés pour des stations de travail, utilisaient cette architecture, par exemple. Mais elle est totalement abandonnée depuis les années 90.
L'usage de plusieurs unités géométriques en parallèle
[modifier | modifier le wikicode]La seconde solution utilise plusieurs unités géométriques en parallèle. Chaque unité géométrique traite toutes les étapes vues plus haut, elle traite un triangle/sommet de bout en bout. Mais vu qu'il y en a plusieurs, on peut traiter plusieurs triangle/sommet : un dans chaque unité géométrique. C'est la solution retenue sur toutes les cartes graphiques depuis les années 90. Mais elle entraine un problème majeur : comment répartir les triangles/sommets sur les différentes unités géométriques ?
La répartition du travail sur les unités géométriques est déléguée au processeur de commandes. Le processeur de commande envoie les triangles/sommets aux différentes unités géométriques avec un algorithme généralement assez simple. La méthode la plus simple est d'utiliser les unités géométriques à tour de rôle : on envoie le premier triangle à la première unité, le second triangle à la seconde unité, le troisième triangle à la troisième, etc. Il s'agit de ce que l'on appelle l'algorithme du tourniquet, qui est assez efficace malgré sa simplicité. Il marche assez bien quand tous les triangles/sommets mettent approximativement le même temps pour être traités.
Si le temps de calcul varie beaucoup d'un triangle/sommet à l'autre, une autre solution tout aussi simple est utilisée. L'idée est que le processeur de commande sait à chaque instant quelles unités géométriques sont libres ou occupées. Pour cela, les unités géométriques préviennent quand elles ont fini leur travail. Le processeur de commande applique alors l'algorithme du tourniquet uniquement sur les unités géométriques libres. Ou il envoie simplement le triangle/sommet à la première géométrique libre, c'est une autre solution qui marche tout aussi bien.
Un autre problème survient cette fois-ci en sortie des unités géométriques. Comment connecter plusieurs unités géométriques au reste de la carte graphique ? Évidemment, la carte graphique contient plusieurs unités de texture/pixel et plusieurs ROPs. Elle tient compte de l'amplification des pixels, ce qui fait qu'il y a moins d'unités géométriques que d'autres circuits, entre 2 à 8 fois moins environ. Pour créer une carte graphique avec plusieurs unités géométriques, il y a plusieurs solutions, que nous allons détailler dans ce qui suit. Pour les explications, nous allons prendre l'exemple de cartes graphiques avec 2 unités géométriques et 8 unités de texture/pixel, et autant de ROPs.
La première solution serait simplement de dupliquer les circuits précédents, en gardant leurs interconnexions. Pour l'exemple, on aurait 2 unités géométriques, chacune connectée à 4 unités de textures/pixels. L'unité géométrique est suivie par un rastériseur qui alimente 4 unités de texture/pixel, comme c'était le cas dans la section précédente. L'implémentation est alors très simple : on a juste à dupliquer les circuits et à modifier le processeur de commande. Il faut aussi modifier les connexions des ROPs à la mémoire vidéo. Mais les interconnexions avec le rastériseur ne sont pas modifiées.
Un désavantage est que l'amplification des pixels n'est pas gérée au mieux. Imaginez que l'on ait deux triangles à rastériser, qui génèrent 8 pixels en tout : un qui génère 6 pixels à la rastérisation, l'autre seulement 2. Il n'est pas possible de traiter les 8 pixels générés. Le triangle générant deux pixels va alimenter deux unités de texture/pixels et en laisser deux inutilisées, l'autre triangle sera traité en deux fois (4 pixels, puis 2). La duplication bête et méchante n'utilise donc pas à la perfection les unités de texture/pixel.
Une autre solution permet de gérer à la perfection l'amplification des pixels. Elle consiste à utiliser un seul rastériseur à haute performance, sur lequel on connecte les unités géométriques et les unités de texture/pixel. L'idée est que le rastériseur peut recevoir N triangles à la fois et alimenter M unités de texture/pixels. Le rastériseur unique s'occupe de faire plusieurs rastérisations de triangles à la fois, et répartit automatiquement les pixels générés sur les unités de texture/pixel. Pour donner un exemple, le GPU Geforce 6800 de NVIDIA avait 6 unités géométriques, 16 unités faisant à la fois placage de textures et éclairage par pixel, et 16 ROPs. Un point important avec ce GPU est qu'il n'avait qu'un seul rastériseur, détail sur lequel on reviendra dans ce qui suit !

Les cartes graphiques en mode immédiat et à tuile
[modifier | modifier le wikicode]Il est courant de dire qu'il existe deux types de cartes graphiques : celles en mode immédiat, et celles avec un rendu en tuiles (tiles). Il s'agit là des deux types principaux de cartes graphiques à l'heure actuelle, mais quelques architectures faisaient autrement dans le passé. Une autre classification, plus générale, sépare les GPU en GPU sort-last, sort-first et sort-middle. Les GPU en mode immédiat correspondent aux GPU en mode immédiat, alors que le rendu à tuile est une sous-catégorie des GPU sort-middle. La différence entre les deux est liée à la manière dont les pixels/primitives sont répartis sur l'écran.
Les GPU sort-first ont plusieurs pipelines séparés, chacun traitant une partie de l'écran. Ils déterminent la position des triangles à l'écran, puis répartissent les triangles dans les pipelines adéquats. Par exemple, on peut imaginer un GPU sort-first avec quatre unités séparées, chacune traitant un quart de l'écran. Au tout début du rendu, une unité de répartition détermine la position d'un triangle à l'écran, et l'envoie à l'unité adéquate. Si le triangle est dans le coin inférieur gauche, il sera envoyé à l'unité dédiée à ce coin. S'il est situé au milieu de l'écran, il sera envoyé aux quatre unités, chacune ne traitant les pixels que pour son coin à elle.
Les GPU sort-middle découpent l'écran en carrés de 4, 8, 16, 32 pixels de côté , qui sont rendus séparément les uns des autres. Les morceaux d'image en question sont appelés des tiles en anglais, mot que nous avons décidé de ne pas traduire pour ne pas le confondre avec les tuiles du rendu 2D. Il y a une assignation stricte entre une unité de pixel/texture et une tile. Par exemple, sur un système avec deux unités de texture/pixel, la première unité traitera les tiles paires, l'autre unité les tiles impaires.
Les GPU sort-last sont l'extrême inverse. Ils ont des unités banalisées qui se moquent de l'endroit où se trouve un pixel à l'écran. Leurs unités géométriques traitent des polygones sans se préoccuper de leur place à l'écran. Le rastériseur envoie les pixels aux unités de textures/ROPs sans se soucier de leur place à l'écran. Encore que quelques optimisations s'en mêlent pour profiter au mieux des caches de texture et des caches intégrés aux ROPs, mais l'essentiel est qu'il n'y a pas de répartition fixe. Il n'y a pas de logique du type : ce pixel ou ce triangle est à tel endroit à l'écran, on l'envoie vers telle unité de texture/ROP. Ce sont les ROPs qui se chargent d'enregistrer les pixles finaux au bon endroits dans le framebuffer. La gestion de la place des pixels à l'écran se fait donc à la toute fin du pipeline, d'où le nom de sort-last.
Pour résumer, les trois types de GPU se distinguent suivant l'endroit où les triangles/pixels sont répartis suivant leur place à l'écran. Avec le sort-first, ce sont les triangles qui sont triés suivant leur place à l'écran. Le tri a donc lieu avant les unités géométriques. Avec le sort-middle, ce sont les fragments générés par la rastérisation qui sont triés suivant leur place à l'écran, d'où l'existence de tiles. Le tri a lieu entre les unités géométriques et le rastériseur. Les unités géométriques se moquent de la place à l'écran des primitives qu'ils traitent, mais pas les rastériseurs et les unités de texture. Enfin, avec le sort-last, ce sont les pixels finaux qui sont triés selon leur place à l'écran, seuls les ROPs se préoccupent de cette place à l'écran.
Concrètement, les GPU de type sort-first sont très rares, l'auteur de ce cours n'en connait aucun exemple. Les deux autres types de GPU sont eux beaucoup plus communs. Reste à voir ce qu'il y a à l'intérieur d'un GPU sort-middle et/ou sort-last. Pour simplifier les explications, nous allons regrouper les circuits de traitement des pixels dans un seul gros circuits appelé le rastériseur, par abus de langage. La carte graphique est donc composée de deux circuits : l'unité géométrique et le mal-nommé rastériseur. Les cartes graphiques ajoutent des mémoires caches pour la géométrie et les textures, afin de rendre leur accès plus rapide.

Les GPU sort-last, en mode immédiat
[modifier | modifier le wikicode]Les cartes graphiques en mode immédiat implémentent le pipeline graphique d'une manière assez évidente. L'unité géométrique envoie des triangles au rastériseur, qui lui-même envoie les pixels à l'unité de texture, qui elle-même envoie le pixel texturé au ROP. Elles effectuent le rendu 3D triangle par tringle, pixel par pixel. Un point important est que pendant que le pixel N est dans les ROP, les pixels N+1 est dans l'unité de texture, le pixel N+2 est dans le rastériseur et le triangle suivant est dans l'unité géométrique. En clair, on n'attend pas qu'un triangle soit affiché pour en démarrer un autre.
Un problème est qu'un triangle dans une scène 3D correspond souvent à plusieurs pixels, ce qui fait que la rastérisation prend plus de temps de calcul que la géométrie. En conséquence, il arrive fréquemment que le rastériseur soit occupé, alors que l'unité de géométrie veut lui envoyer des données. Pour éviter tout problème, on insère une petite mémoire entre l'unité géométrique et le rastériseur, qui porte le nom de tampon de primitives. Elle permet d'accumuler les sommets calculés quand le rastériseur est occupé.

Le tout peut s'adapter à la présence de plusieurs unités géométriques, de plusieurs unités de texture ou processeurs de shaders, tant qu'on conserve un rastériseur unique. Il suffit alors d'adapter le tampon de primitive et le rastériseur. Si on veut rajouter des unités de texture ou des processeurs de pixel shaders, le tampon de primitives n'est pas concerné : il suffit que le rastériseur ait plusieurs sorties, une par unité de texture/pixel shader. Par contre, la présence de plusieurs unités géométriques impacte le tampon de primitive.
Avec plusieurs unités géométriques, il y a deux solutions : soit on garde un tampon de primitive unique partagé, soit il y a un tampon de primitive par unité géométrique. Avec la première solution, toutes les unités géométriques sont reliées à un tampon de primitives unique. Le tampon de primitive est conçu pour qu'on puisse écrire plusieurs primitives dedans en même temps. Le rastériseur n'a pas à être modifié. Une autre solution utilise un tampon de primitive par unité géométrique. Le rastériseur peut alors piocher dans plusieurs tampons de primitive, ce qui demande de modifier le rastériseur. Il y a alors un système d'arbitrage, pour que le rastériseur pioche des primitive équitablement dans tous les tampons de primitive, pas question que l'un d'entre eux soit ignoré durant trop longtemps.
Les GPU sort-middle des années 90
[modifier | modifier le wikicode]Voyons maintenant les architectures sort-middle utilisée dans les années 80-90, à une époque où les cartes graphiques grand public n'existaient pas encore. Les cartes graphiques de l’entreprise SGI sont dans ce cas, mais aussi le Pixel Planes 5, et de nombreux autres systèmes graphiques. Elles utilisaient un rendu à tile assez original. Dans ce qui suit, nous allons décrire l'architecture des systèmes SGI, qui sont représentatifs.
L'idée était que l'image était découpée en un nombre de tiles qui variait selon le système utilisé, mais qui était au minimum de 5 et pouvait aller jusqu'à 20. Et chaque tile avait sa propre unité de traitement, qui contenait un rastériseur, une unité de texture, un ROP, etc. En clair, la carte graphique contenait entre 5 et 20 unités de traitement séparées, chacune dédiée à une tile.
Les triangles sortant des unités géométriques étaient envoyés à toutes les unités de traitement, sans exception. Une fois le triangle réceptionné, l'unité de traitement déterminait si le triangle s'affichait dans la tile associée ou non. Si c'est le cas, le rastériseur rastérise le triangle, génère les pixels, les textures sont lues, puis le tout est enregistré en mémoire vidéo. Si ce n'est pas le cas, elle abandonne le polygone/triangle reçu. Si le triangle est partiellement dans la tile, le rastériseur génère les pixels qui sont dans la tile, par les autres.
Précisons que les cartes de ce styles incorporaient un tampon de primitive, ce qui permettait de simplifier la conception de la carte graphique. Sur la carte Infinite Reality, le tampon de primitive faisait 4 méga-octets de RAM, ce permettait de mémoriser 65 536 sommets. Sur la carte Reality Engine, il y avait même plusieurs tampons de primitives, un par unité géométrique. Les polygones sortaient des unités géométriques, étaient accumulés dans les tampons de primitives, puis étaient broadcastés à toutes les unités de traitement. Pour cela, le bus en bleu dans le schéma précédent est en réalité un réseau crossbar avec un système de broadcast.
Une caractéristique de ces architectures est qu'elles mettent le framebuffer à part de la mémoire vidéo. De plus, ce framebuffer est lui-même découpée en tile. Sur la carte Reality Engine, le framebuffer est découpé en 5 à 20 sous-framebuffer, un par tile. Et chaque mini-framebuffer est placé dans l'unité de traitement de la tile associée ! Ainsi, au lieu de connecter 5-20 ROPs à une mémoire vidéo unique, chaque ROP contient une RAM tile, qui mémorise la tile en cours de traitement. Évidemment, cela pose quelques problèmes pour la connexion au VDC, en raison de l'absence de framebuffer unique, mais rien d'insurmontable. L'architecture est illustrée ci-dessous.
- Le Pixel Planes 5 avait un système similaire, mais avait en plus un framebuffer complet, dans lequel les sous-framebuffer étaient recopiés pour obtenir l'image finale.

Un autre détail de l'architecture est lié à la mémoire pour les textures. Les concepteurs de SGI ont décidé de séparer les textures dans une mémoire à part du reste de la mémoire vidéo. Il n'y a pour ainsi dire pas de mémoire vidéo proprement dit : la géométrie à rendre est dans une mémoire à part, idem pour les textures, et pour le framebuffer. On s'attendrait à ce que la mémoire de texture soit reliée aux 5-20 unités de texture, mais les concepteurs ont décidé de faire autrement. A la place, chaque unité de texture contient une copie de la mémoire de texture, qui est donc dupliquée en 5-20 exemplaires ! Difficile de comprendre la raison de ce choix, mais cela simplifiait sans doute les interconnexions internes de la carte graphique, au prix d'un cout en RAM assez important.
Les GPU à rendu à tile
[modifier | modifier le wikicode]Les GPU de SGI, vus précédemment, disposent de d'une unité de traitement par tile. Faire ainsi permet de nombreuses optimisations, comme éclater le framebuffer en plusieurs RAM tile. Mais le cout en matériel est conséquent. Pour économiser des circuits, l'idéal serait d'utiliser moins d'unités de traitement pour les pixels/fragments/textures. Mais pour cela, il faut profondément modifier l'architecture précédente. On perd forcément le lien entre une unité de traitement et une tile. Et cela impose de revoir totalement la manière dont les unités géométriques communiquent avec les unités de traitement.
La solution retenue est celle des GPU à rendu en tile proprement dit, aussi appelés GPU TBR (Tile Based Rendering). Les plus simples n'utilisent qu'une seule unité de traitement et n'ont qu'une seule RAM tile. En conséquence, les tiles sont rendues l'une après l'autre. Au lieu de rendre chaque triangle/polygone l'un après l'autre, la géométrie est intégralement rendue avant de faire la rastérisation. Les triangles sont enregistrés dans la mémoire vidéo et regroupés par tile, avant la rastérisation. La mémoire vidéo contient donc plusieurs paquets de triangles, avec un paquet par tile. Les paquets/tiles sont envoyées au rastériseur un par un, la rastérisation se fait tile par tile.
La RAM tile existe toujours, même si son utilité est différente. La RAM tile accélère le rendu d'une tile, car tout ce qui est nécessaire pour rendre une tile est mémorisé dedans : la tile, le tampon de profondeur, le tampon de stencil et plein d'autres trucs. Pas besoin d’accéder à un gigantesque z-buffer pour toute l'image, juste d'un minuscule z-buffer pour la tile en cours de traitement, qui tient totalement dans la SRAM.
- Il faut noter que les tiles sont généralement assez petites : 16 ou 32 pixels de côté, rarement plus. En comparaison, les tiles faisaient 128 pixels de côté pour les cartes de SGI.

Il est possible pour un GPU TBR de traiter plusieurs tiles en même temps, en parallèle, dans des unités séparées. Un exemple est celui du GPU ARM Mali 400, qui dispose d'une unité géométrique (un processeur de vertex), mais 4 processeurs de pixels. Il peut donc traiter quatre tiles en même temps, chacune étant rendue dans un processeur de pixel dédié. Les 4 processeurs de pixels ont chacun leur propre RAM tile rien qu'à eux.
La présence d'une RAM tile a de nombreux avantages et impacte grandement l'architecture de la carte graphique. En premier lieu, les ROPs sont drastiquement modifiés. De nombreux GPU TBR n'ont même pas de ROPs ! A la place, les ROPs sont émulés par les processeurs de pixel shader. Les pixel shaders peuvent lire ou écrire directement dans le framebuffer, sur les GPU TBR, ce qui leur permet d'émuler les ROPs avec des instructions mathématique/mémoire. Le driver patche automatiquement les pixel shader pour ajouter de quoi émuler les ROPs à la fin des pixel shaders. Cela garantit une économie de circuits non-négligeable.
La présence d'une RAM tile fait que le tampon de profondeur disparait. Par contre, les GPU de type TBR doivent enregistrer les triangles en mémoire vidéo, et les trier par paquets. Cela compense partiellement, totalement, ou sur-compense, les économies liée à la RAM tile. Le regroupement des triangles par tile s'accompagne de quelques optimisations assez sympathiques. Par exemple, les GPU TBR modernes peuvent trier les triangles selon leur profondeur, directement lors du regroupement en paquets. L'avantage est que cela permet à l'élimination des pixels cachés de fonctionner au mieux. L'élimination des pixels cachés fonctionne à la perfection quand les triangles sont triés du plus proche au plus lointain, pour les objets opaques. Les GPU en mode immédiat ne peuvent pas faire ce tri, mais les GPU TBR peuvent le faire, soit totalement, soit partiellement.
Un autre avantage est que l’antialiasing est plus rapide. Pour ceux qui ne le savent pas, l'antialiasing est une technique qui améliore la qualité d’image, en simulant une résolution supérieure. Une image rendue avec antialiasing aura la même résolution que l'écran, mais n'aura pas certains artefacts liés à une résolution insuffisante. Et l'antialiasing a lieu dans et après la rastérisation, et augmente la résolution du tampon de profondeur et du z-buffer. Les GPU en mode immédiat disposent d'optimisations pour limiter la casse, mais les ROP font malgré tout beaucoup d'accès mémoire. Avec le rendu en tiles, l'antialising se fait dans la RAM tile, n'a pas besoin de passer par la mémoire vidéo et est donc plus rapide.
Des compromis différents
[modifier | modifier le wikicode]Les cartes graphiques des ordinateurs de bureau ou portables sont toutes en mode immédiat, alors que celles des appareils mobiles, smartphones et autres équipements embarqués ont un rendu en tiles. Les raisons à cela sont multiples, mais la principale est que le rendu en tiles marche beaucoup mieux pour le rendu en 2D, comparé aux architectures en mode immédiat, ce qui se marie bien aux besoins des smartphones et autres objets connectés.
La performance d'une carte graphique est limitée par la quantité d'accès mémoire par seconde. Autant dire que les économiser est primordial. Et les cartes en mode immédiat et par tile ne sont pas égales de ce point de vue. En mode immédiat, le tampon de primitives évite de passer par la mémoire vidéo, mais le z-buffer et le framebuffer sont très gourmand en accès mémoire. Avec les architectures à tile, c'est l'inverse : la géométrie est enregistrée en mémoire vidéo, mais le tampon de profondeur n'utilise pas la RAM vidéo.
Au final, les deux architectures sont optimisées pour deux types de rendus différents. Les cartes à rendu en tile brillent quand la géométrie n'est pas trop compliquée, et que la résolution est grande ou que l'antialising est activé. Les cartes en mode immédiat sont elles douées pour les scènes géométriquement lourdes, mais avec peu d'accès aux pixels. Le tout est limité par divers caches qui tentent de rendre les accès mémoires moins fréquents, sur les deux types de cartes, mais sans que ce soit une solution miracle.