Wikilivres frwikibooks https://fr.wikibooks.org/wiki/Accueil MediaWiki 1.46.0-wmf.22 first-letter Média Spécial Discussion Utilisateur Discussion utilisateur Wikilivres Discussion Wikilivres Fichier Discussion fichier MediaWiki Discussion MediaWiki Modèle Discussion modèle Aide Discussion aide Catégorie Discussion catégorie Transwiki Discussion Transwiki Wikijunior Discussion Wikijunior TimedText TimedText talk Module Discussion module Event Event talk Histoire du clavier alphabétique 0 59089 763052 716454 2026-04-06T18:03:57Z ~2026-21309-63 123437 /* Application */ 763052 wikitext text/x-wiki {{ébauche}} {{Histoire du clavier alphabétique}} Ce livre '''en cours de rédaction''' vise à faire découvrir au lecteur l' '''Histoire du clavier alphabétique''', depuis sa naissance à l'époque de la machine à écrire et du télégraphe, jusqu'aux temps présent où l'informatique et la téléphonie mobile ont induit des claviers plus sophistiqués. La notion de clavier alphabétique couvre dans ce livre les claviers servant à écrire, par opposition aux claviers musicaux du piano, et de l'accordéon. Elle couvre tous les claviers permettant de manipuler un alphabet, sans se limiter au seul clavier dont l'ordre serait l'ordre lexicographique. Si la disposition dite Azerty ou Qwerty des claviers reste pour partie mystérieuse, il souhaite ouvrir la porte aux théories les plus plausibles. == Sommaire == ===Avant-propos=== *[[/Préface|Préface]] *[[/Objectifs|Objectifs de cette méthode]] ===L'écriture avant le clavier=== :Ici, nous détaillerons les modes opératoires permettant d'écrire avant l'invention du clavier. , notamment pour les besoins de la publicité des débats dans les parlements et dans les tribunaux. Les concepts à couvrir sont donc la tachygraphie, la dactylographie, la sténographie, etc etc. *[[/Présentation|Besoin d'écrire]] ===Contexte de l'époque=== :Ici, nous verrons quelles étaient les technologies existantes et concernées par le développement des claviers alphabétiques. ====Technologies de l'époque==== =====L'imprimerie et la typographie===== =====Le télégraphe===== =====Instruments de musique à clavier===== <small>Les instruments de musique à clavier peuvent être plus nombreux que ce que peut croire le néophyte. On trouvera notamment une liste de ces instruments sur wikipedia. http://fr.wikipedia.org/wiki/Liste_des_instruments_%C3%A0_clavier_en_musique_classique </small> Dans ce livre on se consacrera essentiellement aux technologies ayant préexisté aux claviers alphabétiques. *[[/Instruments de musique à clavier|Clavier d'instrument à corde]] (clavecorde & piano) *[[/Clavier pour instrument à vent|Clavier pour instrument à vent]] (accordéon) ====Alphabet et ordre des lettres==== ** Ordre de l'alphabet d'Emile Baudot ** Ordre de l'alphabet de Donald Murray ** Ordre de l'alphabet de Morse ** Ordre de l'alphabet du Linotype ===Naissance de la machine à écrire === ===Naissance du télétype === ===Le clavier informatique === ** Le nombre de touches ** Les dispositions régionales ** Page d'un livre étranger: [[Windows/Raccourcis clavier]] ===L'internationalisation === ===Le clavier de téléphone portable === ===Le clavier d'ordiphone ''(smartphone)'' === ===Application=== ;Ici, nous détaillerons l'utilité des claviers dans la vie présente (secrétariat, communication, édition de documents, etc.) faux ==Annexes== {{...}} [[Catégorie:Accordéon chromatique (livre)|{{SUBPAGENAME}}]] 9lddup1z2dsacu1bt9apkl1121i553t 763072 763052 2026-04-06T19:03:22Z DavidL 1746 Révocation d’une modification de [[Special:Contributions/~2026-21309-63|~2026-21309-63]] ([[User talk:~2026-21309-63|discussion]]) vers la dernière version de [[User:SGlad|SGlad]] 716454 wikitext text/x-wiki {{ébauche}} {{Histoire du clavier alphabétique}} Ce livre '''en cours de rédaction''' vise à faire découvrir au lecteur l' '''Histoire du clavier alphabétique''', depuis sa naissance à l'époque de la machine à écrire et du télégraphe, jusqu'aux temps présent où l'informatique et la téléphonie mobile ont induit des claviers plus sophistiqués. La notion de clavier alphabétique couvre dans ce livre les claviers servant à écrire, par opposition aux claviers musicaux du piano, et de l'accordéon. Elle couvre tous les claviers permettant de manipuler un alphabet, sans se limiter au seul clavier dont l'ordre serait l'ordre lexicographique. Si la disposition dite Azerty ou Qwerty des claviers reste pour partie mystérieuse, il souhaite ouvrir la porte aux théories les plus plausibles. == Sommaire == ===Avant-propos=== *[[/Préface|Préface]] *[[/Objectifs|Objectifs de cette méthode]] ===L'écriture avant le clavier=== :Ici, nous détaillerons les modes opératoires permettant d'écrire avant l'invention du clavier. , notamment pour les besoins de la publicité des débats dans les parlements et dans les tribunaux. Les concepts à couvrir sont donc la tachygraphie, la dactylographie, la sténographie, etc etc. *[[/Présentation|Besoin d'écrire]] ===Contexte de l'époque=== :Ici, nous verrons quelles étaient les technologies existantes et concernées par le développement des claviers alphabétiques. ====Technologies de l'époque==== =====L'imprimerie et la typographie===== =====Le télégraphe===== =====Instruments de musique à clavier===== <small>Les instruments de musique à clavier peuvent être plus nombreux que ce que peut croire le néophyte. On trouvera notamment une liste de ces instruments sur wikipedia. http://fr.wikipedia.org/wiki/Liste_des_instruments_%C3%A0_clavier_en_musique_classique </small> Dans ce livre on se consacrera essentiellement aux technologies ayant préexisté aux claviers alphabétiques. *[[/Instruments de musique à clavier|Clavier d'instrument à corde]] (clavecorde & piano) *[[/Clavier pour instrument à vent|Clavier pour instrument à vent]] (accordéon) ====Alphabet et ordre des lettres==== ** Ordre de l'alphabet d'Emile Baudot ** Ordre de l'alphabet de Donald Murray ** Ordre de l'alphabet de Morse ** Ordre de l'alphabet du Linotype ===Naissance de la machine à écrire === ===Naissance du télétype === ===Le clavier informatique === ** Le nombre de touches ** Les dispositions régionales ** Page d'un livre étranger: [[Windows/Raccourcis clavier]] ===L'internationalisation === ===Le clavier de téléphone portable === ===Le clavier d'ordiphone ''(smartphone)'' === ===Application=== ;Ici, nous détaillerons l'utilité des claviers dans la vie présente (secrétariat, communication, édition de documents, etc.) ==Annexes== {{...}} [[Catégorie:Accordéon chromatique (livre)|{{SUBPAGENAME}}]] o1ha8zgbjy05qffu72zno33gagpxz0l Les cartes graphiques/Le pipeline géométrique d'avant DirectX 10 0 67393 763045 760907 2026-04-06T16:59:23Z Mewtow 31375 /* L'input assembler */ 763045 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge, les processeurs sont en "gris". {|class="wikitable" |- ! colspan="4" | Systèmes graphiques des années 70-80 |- | ''Input assembly'' | ''Transformation'', éclairage parfois présent | ''Primitive assembly'' |} Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Par la suite, entre la la Geforce 256 de NVIDIA a et la Geforce 3, les cartes graphiques intégraient un pipeline géométrique limité, qui n'étaient pas programmable du tout. C’était l'époque ancienne des '''circuits de ''Transform & Lightning'''''. Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} La seconde période a été très courte : à peine deux générations de cartes graphiques. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique, celui de la première et de la seconde période. Nous allons nous concentrer sur l'''input assembler'' et l'assemblage de primitives. Nous avons déjà vu les étapes de transformation et l'éclairage dans le chapitre sur les bases du rendu 3D, nous n'allons pas revenir dessus dans le détail. Toujours est-il que l'ancien pipeline géométrique ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ===L'''input assembler''=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Les objets géométriques et 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'''. 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. [[File:Cube colored.png|vignette|Cube en 3D]] 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. 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. [[File:Vertex-Vertex Meshes (VV).png|centre|vignette|upright=1.5|Vertex-Vertex Meshes (VV)]] 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 technique 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. Mais il existe des méthodes pour que les informations sur les arêtes soient codées de manière explicite. 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 des ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] La technique des '''triangles fan''' fonctionne comme pour le triangle 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. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. ===L'étape de T&L et les ''vertex shaders''=== L'étape de transformation-projection regroupe plusieurs manipulations différentes, mais qui ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène 3D du point de vue de la caméra, et un autre qui corrige la perspective. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes, et que la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions de détail. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. L'unité de T&L est devenue programmable dès la Geforce 3, première carte graphique à supporter les ''vertex shaders''. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. ===L'assemblage de primitives=== En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} i1fv41l5eybsekjfim772g5oj1arzns 763046 763045 2026-04-06T17:00:58Z Mewtow 31375 /* L'input assembler */ 763046 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge, les processeurs sont en "gris". {|class="wikitable" |- ! colspan="4" | Systèmes graphiques des années 70-80 |- | ''Input assembly'' | ''Transformation'', éclairage parfois présent | ''Primitive assembly'' |} Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Par la suite, entre la la Geforce 256 de NVIDIA a et la Geforce 3, les cartes graphiques intégraient un pipeline géométrique limité, qui n'étaient pas programmable du tout. C’était l'époque ancienne des '''circuits de ''Transform & Lightning'''''. Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} La seconde période a été très courte : à peine deux générations de cartes graphiques. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique, celui de la première et de la seconde période. Nous allons nous concentrer sur l'''input assembler'' et l'assemblage de primitives. Nous avons déjà vu les étapes de transformation et l'éclairage dans le chapitre sur les bases du rendu 3D, nous n'allons pas revenir dessus dans le détail. Toujours est-il que l'ancien pipeline géométrique ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ===L'''input assembler''=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Les objets géométriques et 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'''. 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. [[File:Cube colored.png|vignette|Cube en 3D]] 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. 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. [[File:Vertex-Vertex Meshes (VV).png|centre|vignette|upright=1.5|Vertex-Vertex Meshes (VV)]] 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 technique 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. Mais il existe des méthodes pour que les informations sur les arêtes soient codées de manière explicite. 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 des ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] La technique des '''triangles fan''' fonctionne comme pour le triangle 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. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. ===L'étape de T&L et les ''vertex shaders''=== L'étape de transformation-projection regroupe plusieurs manipulations différentes, mais qui ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène 3D du point de vue de la caméra, et un autre qui corrige la perspective. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes, et que la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions de détail. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. L'unité de T&L est devenue programmable dès la Geforce 3, première carte graphique à supporter les ''vertex shaders''. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. ===L'assemblage de primitives=== En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} exy0umk3urc25j812go7xuisladf613 763047 763046 2026-04-06T17:05:25Z Mewtow 31375 /* L'input assembler */ Retrait d'une technique non-utilisée par les cartes graphiques, anciennes comme modernes. 763047 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge, les processeurs sont en "gris". {|class="wikitable" |- ! colspan="4" | Systèmes graphiques des années 70-80 |- | ''Input assembly'' | ''Transformation'', éclairage parfois présent | ''Primitive assembly'' |} Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Par la suite, entre la la Geforce 256 de NVIDIA a et la Geforce 3, les cartes graphiques intégraient un pipeline géométrique limité, qui n'étaient pas programmable du tout. C’était l'époque ancienne des '''circuits de ''Transform & Lightning'''''. Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} La seconde période a été très courte : à peine deux générations de cartes graphiques. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique, celui de la première et de la seconde période. Nous allons nous concentrer sur l'''input assembler'' et l'assemblage de primitives. Nous avons déjà vu les étapes de transformation et l'éclairage dans le chapitre sur les bases du rendu 3D, nous n'allons pas revenir dessus dans le détail. Toujours est-il que l'ancien pipeline géométrique ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ===L'''input assembler''=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Les objets géométriques et 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'''. 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. [[File:Cube colored.png|vignette|Cube en 3D]] 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 premières solutions forçaient une représentation bien précise, où deux sommets consécutifs dans le tampon de sommet sont 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 des ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] La technique des '''triangles fan''' fonctionne comme pour le triangle 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. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. ===L'étape de T&L et les ''vertex shaders''=== L'étape de transformation-projection regroupe plusieurs manipulations différentes, mais qui ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène 3D du point de vue de la caméra, et un autre qui corrige la perspective. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes, et que la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions de détail. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. L'unité de T&L est devenue programmable dès la Geforce 3, première carte graphique à supporter les ''vertex shaders''. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. ===L'assemblage de primitives=== En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} 7yeb090y1y6obnebuyr5tcijtvowyuj 763048 763047 2026-04-06T17:18:18Z Mewtow 31375 /* L'input assembler */ 763048 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge, les processeurs sont en "gris". {|class="wikitable" |- ! colspan="4" | Systèmes graphiques des années 70-80 |- | ''Input assembly'' | ''Transformation'', éclairage parfois présent | ''Primitive assembly'' |} Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Par la suite, entre la la Geforce 256 de NVIDIA a et la Geforce 3, les cartes graphiques intégraient un pipeline géométrique limité, qui n'étaient pas programmable du tout. C’était l'époque ancienne des '''circuits de ''Transform & Lightning'''''. Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} La seconde période a été très courte : à peine deux générations de cartes graphiques. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique, celui de la première et de la seconde période. Nous allons nous concentrer sur l'''input assembler'' et l'assemblage de primitives. Nous avons déjà vu les étapes de transformation et l'éclairage dans le chapitre sur les bases du rendu 3D, nous n'allons pas revenir dessus dans le détail. Toujours est-il que l'ancien pipeline géométrique ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ===L'''input assembler''=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Les objets géométriques et 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'''. 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. [[File:Cube colored.png|vignette|Cube en 3D]] 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. Nous les appellerons des représentations compressées, bien que ce terme soit un peu trompeur. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Avec elles, deux sommets consécutifs dans le tampon de sommet sont 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''. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] La technique des '''triangles fan''' fonctionne comme pour le triangle 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. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. ===L'étape de T&L et les ''vertex shaders''=== L'étape de transformation-projection regroupe plusieurs manipulations différentes, mais qui ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène 3D du point de vue de la caméra, et un autre qui corrige la perspective. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes, et que la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions de détail. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. L'unité de T&L est devenue programmable dès la Geforce 3, première carte graphique à supporter les ''vertex shaders''. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. ===L'assemblage de primitives=== En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} hstkcp88fjc8wdx2d3s1i0dz1euz635 763049 763048 2026-04-06T17:49:42Z Mewtow 31375 /* L'input assembler */ 763049 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge, les processeurs sont en "gris". {|class="wikitable" |- ! colspan="4" | Systèmes graphiques des années 70-80 |- | ''Input assembly'' | ''Transformation'', éclairage parfois présent | ''Primitive assembly'' |} Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Par la suite, entre la la Geforce 256 de NVIDIA a et la Geforce 3, les cartes graphiques intégraient un pipeline géométrique limité, qui n'étaient pas programmable du tout. C’était l'époque ancienne des '''circuits de ''Transform & Lightning'''''. Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} La seconde période a été très courte : à peine deux générations de cartes graphiques. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique, celui de la première et de la seconde période. Nous allons nous concentrer sur l'''input assembler'' et l'assemblage de primitives. Nous avons déjà vu les étapes de transformation et l'éclairage dans le chapitre sur les bases du rendu 3D, nous n'allons pas revenir dessus dans le détail. Toujours est-il que l'ancien pipeline géométrique ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ===L'''input assembler''=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Les objets géométriques et 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'''. 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. [[File:Cube colored.png|vignette|Cube en 3D]] 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. Nous les appellerons des représentations compressées, bien que ce terme soit un peu trompeur. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Avec cette représentation, un triangle est définit par le sommet partagé, puis deux sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. ===L'étape de T&L et les ''vertex shaders''=== L'étape de transformation-projection regroupe plusieurs manipulations différentes, mais qui ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène 3D du point de vue de la caméra, et un autre qui corrige la perspective. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes, et que la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions de détail. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. L'unité de T&L est devenue programmable dès la Geforce 3, première carte graphique à supporter les ''vertex shaders''. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. ===L'assemblage de primitives=== En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} p1q7udz5z8iyowj19dfmipjaogl865d 763050 763049 2026-04-06T17:56:37Z Mewtow 31375 /* L'input assembler */ 763050 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge, les processeurs sont en "gris". {|class="wikitable" |- ! colspan="4" | Systèmes graphiques des années 70-80 |- | ''Input assembly'' | ''Transformation'', éclairage parfois présent | ''Primitive assembly'' |} Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Par la suite, entre la la Geforce 256 de NVIDIA a et la Geforce 3, les cartes graphiques intégraient un pipeline géométrique limité, qui n'étaient pas programmable du tout. C’était l'époque ancienne des '''circuits de ''Transform & Lightning'''''. Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} La seconde période a été très courte : à peine deux générations de cartes graphiques. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique, celui de la première et de la seconde période. Nous allons nous concentrer sur l'''input assembler'' et l'assemblage de primitives. Nous avons déjà vu les étapes de transformation et l'éclairage dans le chapitre sur les bases du rendu 3D, nous n'allons pas revenir dessus dans le détail. Toujours est-il que l'ancien pipeline géométrique ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ===L'''input assembler''=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Les objets géométriques et 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'''. 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. [[File:Cube colored.png|vignette|Cube en 3D]] 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. Nous les appellerons des représentations compressées, bien que ce terme soit un peu trompeur. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. ===L'étape de T&L et les ''vertex shaders''=== L'étape de transformation-projection regroupe plusieurs manipulations différentes, mais qui ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène 3D du point de vue de la caméra, et un autre qui corrige la perspective. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes, et que la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions de détail. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. L'unité de T&L est devenue programmable dès la Geforce 3, première carte graphique à supporter les ''vertex shaders''. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. ===L'assemblage de primitives=== En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} qrhnj0x48i4wyjgz48brbwda7geezva 763051 763050 2026-04-06T18:00:20Z Mewtow 31375 /* L'input assembler */ 763051 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge, les processeurs sont en "gris". {|class="wikitable" |- ! colspan="4" | Systèmes graphiques des années 70-80 |- | ''Input assembly'' | ''Transformation'', éclairage parfois présent | ''Primitive assembly'' |} Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Par la suite, entre la la Geforce 256 de NVIDIA a et la Geforce 3, les cartes graphiques intégraient un pipeline géométrique limité, qui n'étaient pas programmable du tout. C’était l'époque ancienne des '''circuits de ''Transform & Lightning'''''. Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} La seconde période a été très courte : à peine deux générations de cartes graphiques. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique, celui de la première et de la seconde période. Nous allons nous concentrer sur l'''input assembler'' et l'assemblage de primitives. Nous avons déjà vu les étapes de transformation et l'éclairage dans le chapitre sur les bases du rendu 3D, nous n'allons pas revenir dessus dans le détail. Toujours est-il que l'ancien pipeline géométrique ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ===L'''input assembler''=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Les objets géométriques et 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'''. 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. [[File:Cube colored.png|vignette|Cube en 3D]] 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. Nous les appellerons des représentations compressées, bien que ce terme soit un peu trompeur. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. ===L'étape de T&L et les ''vertex shaders''=== L'étape de transformation-projection regroupe plusieurs manipulations différentes, mais qui ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène 3D du point de vue de la caméra, et un autre qui corrige la perspective. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes, et que la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions de détail. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. L'unité de T&L est devenue programmable dès la Geforce 3, première carte graphique à supporter les ''vertex shaders''. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. ===L'assemblage de primitives=== En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} q6i2zn6hqvt1elyvie6kb3vbq39le8q 763053 763051 2026-04-06T18:10:08Z Mewtow 31375 /* Les cartes accélératrices des PC grand publics : les années 90-2000 */ 763053 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge, les processeurs sont en "gris". {|class="wikitable" |- ! colspan="4" | Systèmes graphiques des années 70-80 |- | ''Input assembly'' | ''Transformation'', éclairage parfois présent | ''Primitive assembly'' |} Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Il y a plus de documentation sur le pipeline géométrique de cette période. Et surtout, le pipeline géométrique était un peu différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. Voyons en quoi ils consistent. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | ''Input assembly'' | ''Transform & Lighting'' | ''Primitive assembly'' |} ===Le pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] ===L'implémentation matérielle du pipeline géométrique=== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique, celui de la première et de la seconde période. Nous allons nous concentrer sur l'''input assembler'' et l'assemblage de primitives. Nous avons déjà vu les étapes de transformation et l'éclairage dans le chapitre sur les bases du rendu 3D, nous n'allons pas revenir dessus dans le détail. Toujours est-il que l'ancien pipeline géométrique ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ===Les représentations des maillages : les optimisations=== Les objets géométriques et 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'''. 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. [[File:Cube colored.png|vignette|Cube en 3D]] 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. Nous les appellerons des représentations compressées, bien que ce terme soit un peu trompeur. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} 4fxwbh7oxaj82p44peq8tyujvn8tlvu 763056 763053 2026-04-06T18:11:33Z Mewtow 31375 /* Les cartes accélératrices des PC grand publics : les années 90-2000 */ 763056 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge, les processeurs sont en "gris". {|class="wikitable" |- ! colspan="4" | Systèmes graphiques des années 70-80 |- | ''Input assembly'' | ''Transformation'', éclairage parfois présent | ''Primitive assembly'' |} Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Il y a plus de documentation sur le pipeline géométrique de cette période. Et surtout, le pipeline géométrique était un peu différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. Voyons en quoi ils consistent. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC |- | ''Input assembly'' | ''Transform & Lighting'' | ''Primitive assembly'' |} ===Le pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] ===L'implémentation matérielle du pipeline géométrique=== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique, celui de la première et de la seconde période. Nous allons nous concentrer sur l'''input assembler'' et l'assemblage de primitives. Nous avons déjà vu les étapes de transformation et l'éclairage dans le chapitre sur les bases du rendu 3D, nous n'allons pas revenir dessus dans le détail. Toujours est-il que l'ancien pipeline géométrique ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ===Les représentations des maillages : les optimisations=== Les objets géométriques et 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'''. 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. [[File:Cube colored.png|vignette|Cube en 3D]] 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. Nous les appellerons des représentations compressées, bien que ce terme soit un peu trompeur. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} ndeuaht74pio408lupbsi0zb0tip8z1 763057 763056 2026-04-06T18:12:52Z Mewtow 31375 /* Les cartes accélératrices des PC grand publics : les années 90-2000 */ 763057 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge, les processeurs sont en "gris". {|class="wikitable" |- ! colspan="4" | Systèmes graphiques des années 70-80 |- | ''Input assembly'' | ''Transformation'', éclairage parfois présent | ''Primitive assembly'' |} Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Il y a plus de documentation sur le pipeline géométrique de cette période. Et surtout, le pipeline géométrique était un peu différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. Voyons en quoi ils consistent. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC |- | class="f_bleu" | ''Input assembly'' | Transformation et projection | class="f_bleu" | ''Primitive assembly'' |} ===Le pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] ===L'implémentation matérielle du pipeline géométrique=== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique, celui de la première et de la seconde période. Nous allons nous concentrer sur l'''input assembler'' et l'assemblage de primitives. Nous avons déjà vu les étapes de transformation et l'éclairage dans le chapitre sur les bases du rendu 3D, nous n'allons pas revenir dessus dans le détail. Toujours est-il que l'ancien pipeline géométrique ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ===Les représentations des maillages : les optimisations=== Les objets géométriques et 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'''. 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. [[File:Cube colored.png|vignette|Cube en 3D]] 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. Nous les appellerons des représentations compressées, bien que ce terme soit un peu trompeur. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} lueekm4rvqgg0k1gr7fhecravxqsetq 763058 763057 2026-04-06T18:26:58Z Mewtow 31375 /* Les représentations des maillages : les optimisations */ 763058 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge, les processeurs sont en "gris". {|class="wikitable" |- ! colspan="4" | Systèmes graphiques des années 70-80 |- | ''Input assembly'' | ''Transformation'', éclairage parfois présent | ''Primitive assembly'' |} Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Il y a plus de documentation sur le pipeline géométrique de cette période. Et surtout, le pipeline géométrique était un peu différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. Voyons en quoi ils consistent. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC |- | class="f_bleu" | ''Input assembly'' | Transformation et projection | class="f_bleu" | ''Primitive assembly'' |} ===Le pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] ===L'implémentation matérielle du pipeline géométrique=== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique, celui de la première et de la seconde période. Nous allons nous concentrer sur l'''input assembler'' et l'assemblage de primitives. Nous avons déjà vu les étapes de transformation et l'éclairage dans le chapitre sur les bases du rendu 3D, nous n'allons pas revenir dessus dans le détail. Toujours est-il que l'ancien pipeline géométrique ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ===Les représentations des maillages : les optimisations=== Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Les représentations compressées n'utilisent pas une liste de triangle, mais une liste de sommets. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. 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. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} sder15ic5aqbcgb4xc67zbdw3suevgw 763059 763058 2026-04-06T18:44:00Z Mewtow 31375 /* Les cartes accélératrices des PC grand publics : les années 90-2000 */ 763059 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge, les processeurs sont en "gris". {|class="wikitable" |- ! colspan="4" | Systèmes graphiques des années 70-80 |- | ''Input assembly'' | ''Transformation'', éclairage parfois présent | ''Primitive assembly'' |} Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Il y a plus de documentation sur le pipeline géométrique de cette période. Et surtout, le pipeline géométrique était un peu différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. Voyons en quoi ils consistent. Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Les représentations compressées n'utilisent pas une liste de triangle, mais une liste de sommets. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. 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. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|Représentation face-sommet.]] ===Le cache de sommets=== 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. ===Le pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] ===L'implémentation matérielle du pipeline géométrique=== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique, celui de la première et de la seconde période. Nous allons nous concentrer sur l'''input assembler'' et l'assemblage de primitives. Nous avons déjà vu les étapes de transformation et l'éclairage dans le chapitre sur les bases du rendu 3D, nous n'allons pas revenir dessus dans le détail. Toujours est-il que l'ancien pipeline géométrique ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} ck0fsm3ji24zv1vnfabm0y2h3q63ptg 763060 763059 2026-04-06T18:44:24Z Mewtow 31375 /* Les cartes accélératrices des PC grand publics : les années 90-2000 */ 763060 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge, les processeurs sont en "gris". {|class="wikitable" |- ! colspan="4" | Systèmes graphiques des années 70-80 |- | ''Input assembly'' | ''Transformation'', éclairage parfois présent | ''Primitive assembly'' |} Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. Voyons en quoi ils consistent. Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Les représentations compressées n'utilisent pas une liste de triangle, mais une liste de sommets. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. 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. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|Représentation face-sommet.]] ===Le cache de sommets=== 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. ===Le pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] ===L'implémentation matérielle du pipeline géométrique=== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique, celui de la première et de la seconde période. Nous allons nous concentrer sur l'''input assembler'' et l'assemblage de primitives. Nous avons déjà vu les étapes de transformation et l'éclairage dans le chapitre sur les bases du rendu 3D, nous n'allons pas revenir dessus dans le détail. Toujours est-il que l'ancien pipeline géométrique ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} ed6apexpb4g3k9t4lm27nv3f28y6t51 763061 763060 2026-04-06T18:44:39Z Mewtow 31375 /* L'implémentation matérielle du pipeline géométrique */ 763061 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge, les processeurs sont en "gris". {|class="wikitable" |- ! colspan="4" | Systèmes graphiques des années 70-80 |- | ''Input assembly'' | ''Transformation'', éclairage parfois présent | ''Primitive assembly'' |} Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. Voyons en quoi ils consistent. Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Les représentations compressées n'utilisent pas une liste de triangle, mais une liste de sommets. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. 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. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|Représentation face-sommet.]] ===Le cache de sommets=== 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. ===Le pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] ===L'implémentation matérielle du pipeline géométrique=== Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique, celui de la première et de la seconde période. Nous allons nous concentrer sur l'''input assembler'' et l'assemblage de primitives. Nous avons déjà vu les étapes de transformation et l'éclairage dans le chapitre sur les bases du rendu 3D, nous n'allons pas revenir dessus dans le détail. Toujours est-il que l'ancien pipeline géométrique ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} lqmldhwh7cgx9zm3e2oiioiq4uhw5r9 763062 763061 2026-04-06T18:45:24Z Mewtow 31375 /* L'implémentation matérielle du pipeline géométrique */ 763062 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge, les processeurs sont en "gris". {|class="wikitable" |- ! colspan="4" | Systèmes graphiques des années 70-80 |- | ''Input assembly'' | ''Transformation'', éclairage parfois présent | ''Primitive assembly'' |} Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. Voyons en quoi ils consistent. Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Les représentations compressées n'utilisent pas une liste de triangle, mais une liste de sommets. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. 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. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|Représentation face-sommet.]] ===Le cache de sommets=== 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. ===Le pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique, celui de la première et de la seconde période. Nous allons nous concentrer sur l'''input assembler'' et l'assemblage de primitives. Nous avons déjà vu les étapes de transformation et l'éclairage dans le chapitre sur les bases du rendu 3D, nous n'allons pas revenir dessus dans le détail. Toujours est-il que l'ancien pipeline géométrique ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} 94ixvbale0oqbqjvmtegw1f1o2crjbo 763063 763062 2026-04-06T18:45:50Z Mewtow 31375 /* Les cartes accélératrices des PC grand publics : les années 90-2000 */ 763063 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge, les processeurs sont en "gris". {|class="wikitable" |- ! colspan="4" | Systèmes graphiques des années 70-80 |- | ''Input assembly'' | ''Transformation'', éclairage parfois présent | ''Primitive assembly'' |} Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. Voyons en quoi ils consistent. Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Les représentations compressées n'utilisent pas une liste de triangle, mais une liste de sommets. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. 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. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|Représentation face-sommet.]] ===Le pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique, celui de la première et de la seconde période. Nous allons nous concentrer sur l'''input assembler'' et l'assemblage de primitives. Nous avons déjà vu les étapes de transformation et l'éclairage dans le chapitre sur les bases du rendu 3D, nous n'allons pas revenir dessus dans le détail. Toujours est-il que l'ancien pipeline géométrique ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} hl5dvgzk74z30dfrsg43iracjbxmfws 763064 763063 2026-04-06T18:47:43Z Mewtow 31375 /* Les processeurs à virgule flottante pour la géométrie */ 763064 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. Voyons en quoi ils consistent. Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Les représentations compressées n'utilisent pas une liste de triangle, mais une liste de sommets. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. 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. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|Représentation face-sommet.]] ===Le pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique, celui de la première et de la seconde période. Nous allons nous concentrer sur l'''input assembler'' et l'assemblage de primitives. Nous avons déjà vu les étapes de transformation et l'éclairage dans le chapitre sur les bases du rendu 3D, nous n'allons pas revenir dessus dans le détail. Toujours est-il que l'ancien pipeline géométrique ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} fdlwrztc389ruumvwt2miqxoftk2nbv 763065 763064 2026-04-06T18:49:35Z Mewtow 31375 /* Le pipeline géométrique */ 763065 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. Voyons en quoi ils consistent. Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Les représentations compressées n'utilisent pas une liste de triangle, mais une liste de sommets. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. 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. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|Représentation face-sommet.]] ===L’implémentation matérielle du pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===L'évolution du pipeline géométrique des cartes 3D=== Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} oge00ody30pyk9h5awf1p8c6l2hx0bm 763066 763065 2026-04-06T18:50:23Z Mewtow 31375 763066 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. Voyons en quoi ils consistent. Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Les représentations compressées n'utilisent pas une liste de triangle, mais une liste de sommets. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. 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. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|Représentation face-sommet.]] ===L’implémentation matérielle du pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===L'évolution du pipeline géométrique des cartes 3D=== Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} baqm4mxtk80r06x8dq5ht86bagr6v4p 763067 763066 2026-04-06T18:52:10Z Mewtow 31375 /* L'évolution du pipeline géométrique des cartes 3D */ 763067 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. Voyons en quoi ils consistent. Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Les représentations compressées n'utilisent pas une liste de triangle, mais une liste de sommets. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. 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. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|Représentation face-sommet.]] ===L’implémentation matérielle du pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} kaew5go20cgvoxey1632txwtyo5c3zn 763068 763067 2026-04-06T18:52:47Z Mewtow 31375 /* L'évolution du pipeline géométrique des cartes 3D */ 763068 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. Voyons en quoi ils consistent. Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Les représentations compressées n'utilisent pas une liste de triangle, mais une liste de sommets. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. 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. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|Représentation face-sommet.]] ===L’implémentation matérielle du pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} 6u2abcl1n5dp3awbe31dyjvhnk8nanh 763069 763068 2026-04-06T18:53:37Z Mewtow 31375 /* Les cartes accélératrices des PC grand publics : les années 90-2000 */ 763069 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Les représentations compressées n'utilisent pas une liste de triangle, mais une liste de sommets. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. reste à reconstituer les triangles à partir de cette liste de sommets. 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. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|Représentation face-sommet.]] ===L’implémentation matérielle du pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} lmzpxydxwrh3ru672bs4p6qgvckifaw 763070 763069 2026-04-06T18:57:45Z Mewtow 31375 /* Les représentations des maillages : les optimisations */ 763070 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|Représentation face-sommet.]] ===L’implémentation matérielle du pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} q520fe4850tpa8y9oki91nv58efy0rg 763071 763070 2026-04-06T18:59:43Z Mewtow 31375 /* Les représentations des maillages : les optimisations */ 763071 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Avec l'arrivée de Direct X 7 et des versions contemporaines d'Open GL, une nouvelle représentation compressée est apparue : 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 quant à elles mémorisées dans un tableau séparé appelé le ''tampon d'indices''. Ce dernier n'est rien de plus qu'une liste de triangles. 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. Mais l'astuce tient au codage des données dans le tampon d'indices. Dans le tampon d'indices, un sommet n'est pas codé par ses trois coordonnées. Les sommets étant partagés entre plusieurs triangles, il y aurait beaucoup de redondance avec cette méthode. Pour un sommet partagé entre N triangles, on aurait N copies du sommet, une par triangle. Pour éviter cela, chaque sommet est codé par un indice, un numéro qui indique la position du sommet dans le tampon de sommet. 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. L'astuce est 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 sommet, et 7 exemplaires 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. [[File:Mesh fv.jpg|centre|vignette|upright=2|Représentation face-sommet.]] ===L’implémentation matérielle du pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} 9h6oczpylaj1cmh53qf938vrv87vwzr 763073 763071 2026-04-06T19:08:07Z Mewtow 31375 /* Les représentations des maillages : les optimisations */ 763073 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Depuis Direct X 7 et les versions équivalentes d'Open GL, les cartes graphiques utilisent massivement la '''représentation indicée'''. Elle stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. L'idée est que les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Le numéro est aussi appelé l'indice du sommet, et ceux qui savent ce qu'est un tableau en programmation devraient voir où je veux en dire. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. D'ailleurs, la liste de triangles est appelée le ''tampon d'indices''. Ainsi, si un sommet est présent en N exemplaires dans un maillage, il n'y aura qu'un seul exemplaire du sommet dans le tampon de sommet, mais N exemplaires de l'indice dans le tampon d'indice. L'astuce est 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 sommet, et 7 exemplaires 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Mesh fv.jpg|centre|vignette|upright=2|Représentation face-sommet.]] ===L’implémentation matérielle du pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} i6b3eokuw9d8f6uabv5yp6p7bc7xagr 763074 763073 2026-04-06T19:11:54Z Mewtow 31375 /* Les représentations des maillages : les optimisations */ 763074 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Depuis Direct X 7 et les versions équivalentes d'Open GL, les cartes graphiques utilisent massivement la '''représentation indicée'''. Elle stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. L'idée est que les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Le numéro est aussi appelé l'indice du sommet, et ceux qui savent ce qu'est un tableau en programmation devraient voir où je veux en dire. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. D'ailleurs, la liste de triangles est appelée le ''tampon d'indices''. Ainsi, si un sommet est présent en N exemplaires dans un maillage, il n'y aura qu'un seul exemplaire du sommet dans le tampon de sommet, mais N exemplaires de l'indice dans le tampon d'indice. L'astuce est 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 sommet, et 7 exemplaires 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Mesh fv.jpg|centre|vignette|upright=2|Représentation face-sommet.]] ===L’implémentation matérielle du pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} aqxt1mzuj1qysd39g48g39jm4afchf7 763075 763074 2026-04-06T19:17:23Z Mewtow 31375 /* Les représentations des maillages : les optimisations */ 763075 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] Les ''triangle fans'' et celle des ''triangle strips'' étaient les toutes premières représentations compressées utilisées. Depuis Direct X 7 et les versions équivalentes d'Open GL, les cartes graphiques utilisent massivement la '''représentation indicée'''. Elle stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. L'idée est que les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Le numéro est aussi appelé l'indice du sommet, et ceux qui savent ce qu'est un tableau en programmation devraient voir où je veux en dire. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. D'ailleurs, la liste de triangles est appelée le ''tampon d'indices''. Ainsi, si un sommet est présent en N exemplaires dans un maillage, il n'y aura qu'un seul exemplaire du sommet dans le tampon de sommet, mais N exemplaires de l'indice dans le tampon d'indice. L'astuce est 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 sommet, et 7 exemplaires 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] ===L’implémentation matérielle du pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} nvz58lsgt8ol32p8jc3tnnsol96lhhp 763076 763075 2026-04-06T19:19:42Z Mewtow 31375 /* Les représentations des maillages : les optimisations */ 763076 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. L'idée est que les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Le numéro est aussi appelé l'indice du sommet, et ceux qui savent ce qu'est un tableau en programmation devraient voir où je veux en dire. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. D'ailleurs, la liste de triangles est appelée le ''tampon d'indices''. Ainsi, si un sommet est présent en N exemplaires dans un maillage, il n'y aura qu'un seul exemplaire du sommet dans le tampon de sommet, mais N exemplaires de l'indice dans le tampon d'indice. L'astuce est 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 sommet, et 7 exemplaires 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] ===L’implémentation matérielle du pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} 2cqhx37fsuvfrup8k2vedc86rcwfmzv 763077 763076 2026-04-06T19:21:51Z Mewtow 31375 /* Les représentations des maillages : les optimisations */ 763077 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Ainsi, si un sommet est présent en N exemplaires dans un maillage, il n'y aura qu'un seul exemplaire du sommet dans le tampon de sommet, mais N exemplaires de l'indice dans le tampon d'indice. L'astuce est 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 sommet, et 7 exemplaires 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. Un numéro entier est plus court qu'un pointeur complet. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] ===L’implémentation matérielle du pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} kye6clc1opseo9jv1hkx80fbiryw1cl 763078 763077 2026-04-06T19:31:15Z Mewtow 31375 /* Les représentations des maillages : les optimisations */ 763078 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et d'autres optimisations permettent d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par les trois sommets, le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. La technique des '''triangles strip''' optimise 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. L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} 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. [[File:Triangle strip.png|centre|vignette|Triangle strip]] ===L’implémentation matérielle du pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} i9ysgas4apfjsd42tclcfr94wbw7fom 763079 763078 2026-04-06T19:33:38Z Mewtow 31375 /* Les représentations des maillages : les optimisations */ 763079 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.png|centre|vignette|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} ===L’implémentation matérielle du pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). C'est une unité d'accès mémoire un peu particulière, 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 de 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). Ces informations sont mémorisées dans des registres, qui sont configurés par le pilote de périphérique. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} g489wy71a6eetyz1igglt5ed1gmepv2 763080 763079 2026-04-06T19:35:20Z Mewtow 31375 /* L’implémentation matérielle du pipeline géométrique */ 763080 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.png|centre|vignette|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} ===L’implémentation matérielle du pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. C'est une unité d'accès mémoire un peu particulière, 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 de 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). [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} 1odyqpddkmsdhgsg2uc2salz7xd2cgm 763081 763080 2026-04-06T19:37:23Z Mewtow 31375 /* L’implémentation matérielle du pipeline géométrique */ 763081 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.png|centre|vignette|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} ===L’implémentation matérielle du pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. C'est une unité d'accès mémoire un peu particulière, 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 de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} s8gs2xfxj8sjiy0ys5wz36pdjk67hs9 763082 763081 2026-04-06T19:39:05Z Mewtow 31375 /* Les représentations des maillages : les optimisations */ 763082 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.png|centre|vignette|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. ===L’implémentation matérielle du pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. C'est une unité d'accès mémoire un peu particulière, 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 de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} f9320tiw6w668wwdfki7z8g6yvxsot3 763083 763082 2026-04-06T19:40:09Z Mewtow 31375 /* Les représentations des maillages : les optimisations */ 763083 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. ===L’implémentation matérielle du pipeline géométrique=== L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. C'est une unité d'accès mémoire un peu particulière, 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 de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} q5dgt7xw4dun4fn8bqunr0zh5m9tucd 763084 763083 2026-04-06T20:02:14Z Mewtow 31375 /* L’implémentation matérielle du pipeline géométrique */ 763084 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. C'est une unité d'accès mémoire un peu particulière, 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 de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===Les caches de l'ancien pipeline géométrique=== 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. Le cache de sommets se situe avant les unités géométriques, que ce soit des unités de ''vertex shaders'' ou des unités de T&L. Un autre cache existe, mais cette fois-ci en sortie de l'unité géométrique. Il mémorise lui aussi des sommets, mais qui ont été transformés et éclairés. Idéalement, il faire les calculs géométriques une seule fois par sommet, le ''vertex shader'' doit être exécuté une seule fois par sommet. Pour cela, une possibilité serait de traiter chaque sommet, puis d'assembler des triangles. Par exemple, la carte graphique pourrait transformer/éclairer les sommets un par un, en parcourant le tampon de sommets, avant d'utiliser le tampon d'indice pour assembler des triangles lors de l'assemblage de primitives. Mais le problème est que cela ne marcherait bien que si le résultat du ''vertex shader'' est enregistré en mémoire, puis relu lors de l'assemblage de primitives. Or, ce n'est pas le cas. Pour compenser, divers caches ont été ajoutés au pipeline géométrique, afin de réduire l'impact sur les performances. Premièrement, une fois qu'un sommet a été transformé/éclairé, il est possible de mémoriser celui-ci dans une mémoire cache, appelée le '''''Post Transform Cache'''''. Le cache est utilisé avec la coopération de l''input assembler'' et de l'assemblage de primitives. Et pour cela, la carte graphique utilise les indices de la représentation indicée. La carte graphique détecte qu'un sommet a déjà été traité en regardant l'indice utilisé (et l'adresse du tampon d'indice utilisé, mais passons cela sous silence). Lorsque l'''input assembler'' charge un sommet, il envoie l'indice au cache. Le cache répond alors en disant : j'ai une copie du sommet correspondant à cet indice, ou je n'ai pas la copie. Si le cache dispose d'une copie du sommet, il n'est pas chargé. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} fac6wadai7por90ggppmlvetyjbfj7c 763085 763084 2026-04-06T20:11:27Z Mewtow 31375 /* Les caches de l'ancien pipeline géométrique */ 763085 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. C'est une unité d'accès mémoire un peu particulière, 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 de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===Les caches de l'ancien pipeline géométrique=== Le pipeline géométrique a des défauts, qui font que son implémentation matérielle est souvent peu performante. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués. Les sommets sont chargés depuis la mémoire vidéo, traités par le ''vertex shader'', puis envoyés à l'assemblage de primitives. Je rappelle qu'avec cette représentation non-compressée, les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées sont source de nombreuses optimisations, qui permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. L'usage d'un tampon d'indice présente aussi des opportunités d'optimisation, qui demandent cependant d'ajouter des mémoires caches à la carte graphique. 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. Le cache de sommets se situe avant les unités géométriques, que ce soit des unités de ''vertex shaders'' ou des unités de T&L. Un autre cache existe, mais cette fois-ci en sortie de l'unité géométrique. Il mémorise lui aussi des sommets, mais qui ont été transformés et éclairés. Idéalement, il faire les calculs géométriques une seule fois par sommet, le ''vertex shader'' doit être exécuté une seule fois par sommet. Pour cela, une possibilité serait de traiter chaque sommet, puis d'assembler des triangles. Par exemple, la carte graphique pourrait transformer/éclairer les sommets un par un, en parcourant le tampon de sommets, avant d'utiliser le tampon d'indice pour assembler des triangles lors de l'assemblage de primitives. Mais le problème est que cela ne marcherait bien que si le résultat du ''vertex shader'' est enregistré en mémoire, puis relu lors de l'assemblage de primitives. Or, ce n'est pas le cas. Pour compenser, divers caches ont été ajoutés au pipeline géométrique, afin de réduire l'impact sur les performances. Premièrement, une fois qu'un sommet a été transformé/éclairé, il est possible de mémoriser celui-ci dans une mémoire cache, appelée le '''''Post Transform Cache'''''. Le cache est utilisé avec la coopération de l''input assembler'' et de l'assemblage de primitives. Et pour cela, la carte graphique utilise les indices de la représentation indicée. La carte graphique détecte qu'un sommet a déjà été traité en regardant l'indice utilisé (et l'adresse du tampon d'indice utilisé, mais passons cela sous silence). Lorsque l'''input assembler'' charge un sommet, il envoie l'indice au cache. Le cache répond alors en disant : j'ai une copie du sommet correspondant à cet indice, ou je n'ai pas la copie. Si le cache dispose d'une copie du sommet, il n'est pas chargé. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} b1d4sd0nhh4fz3lewivkt84tntlkcg7 763086 763085 2026-04-06T20:13:17Z Mewtow 31375 /* Les représentations des maillages : les optimisations */ 763086 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. C'est une unité d'accès mémoire un peu particulière, 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 de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===Les caches de l'ancien pipeline géométrique=== Le pipeline géométrique a des défauts, qui font que son implémentation matérielle est souvent peu performante. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués. Les sommets sont chargés depuis la mémoire vidéo, traités par le ''vertex shader'', puis envoyés à l'assemblage de primitives. Je rappelle qu'avec cette représentation non-compressée, les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées sont source de nombreuses optimisations, qui permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. L'usage d'un tampon d'indice présente aussi des opportunités d'optimisation, qui demandent cependant d'ajouter des mémoires caches à la carte graphique. 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Notons que ce cache a disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. Le cache de sommets se situe avant les unités géométriques, que ce soit des unités de ''vertex shaders'' ou des unités de T&L. Un autre cache existe, mais cette fois-ci en sortie de l'unité géométrique. Il mémorise lui aussi des sommets, mais qui ont été transformés et éclairés. Idéalement, il faire les calculs géométriques une seule fois par sommet, le ''vertex shader'' doit être exécuté une seule fois par sommet. Pour cela, une possibilité serait de traiter chaque sommet, puis d'assembler des triangles. Par exemple, la carte graphique pourrait transformer/éclairer les sommets un par un, en parcourant le tampon de sommets, avant d'utiliser le tampon d'indice pour assembler des triangles lors de l'assemblage de primitives. Mais le problème est que cela ne marcherait bien que si le résultat du ''vertex shader'' est enregistré en mémoire, puis relu lors de l'assemblage de primitives. Or, ce n'est pas le cas. Pour compenser, divers caches ont été ajoutés au pipeline géométrique, afin de réduire l'impact sur les performances. Premièrement, une fois qu'un sommet a été transformé/éclairé, il est possible de mémoriser celui-ci dans une mémoire cache, appelée le '''''Post Transform Cache'''''. Le cache est utilisé avec la coopération de l''input assembler'' et de l'assemblage de primitives. Et pour cela, la carte graphique utilise les indices de la représentation indicée. La carte graphique détecte qu'un sommet a déjà été traité en regardant l'indice utilisé (et l'adresse du tampon d'indice utilisé, mais passons cela sous silence). Lorsque l'''input assembler'' charge un sommet, il envoie l'indice au cache. Le cache répond alors en disant : j'ai une copie du sommet correspondant à cet indice, ou je n'ai pas la copie. Si le cache dispose d'une copie du sommet, il n'est pas chargé. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} oc4y5hcw61zjebgaxb08ontr5p6hc08 763087 763086 2026-04-06T20:17:14Z Mewtow 31375 /* Les caches de l'ancien pipeline géométrique */ 763087 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. C'est une unité d'accès mémoire un peu particulière, 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 de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===Les caches de l'ancien pipeline géométrique=== Le pipeline géométrique a des défauts, qui font que son implémentation matérielle est souvent peu performante. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués. Les sommets sont chargés depuis la mémoire vidéo, traités par le ''vertex shader'', puis envoyés à l'assemblage de primitives. Je rappelle qu'avec cette représentation non-compressée, les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées sont source de nombreuses optimisations, qui permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. L'usage d'un tampon d'indice présente aussi des opportunités d'optimisation, qui demandent cependant d'ajouter des mémoires caches à la carte graphique. 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. Et c'était de plus un cache très simple, allant d'une simple mémoire FIFO à des caches basiques (pas de politique de remplacement complexe, caches directement adressé, ...). [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Le cache de sommets se situe avant les unités géométriques, que ce soit des unités de ''vertex shaders'' ou des unités de T&L. Un autre cache existe, mais cette fois-ci en sortie de l'unité géométrique. Il mémorise lui aussi des sommets, mais qui ont été transformés et éclairés. Idéalement, il faire les calculs géométriques une seule fois par sommet, le ''vertex shader'' doit être exécuté une seule fois par sommet. Pour cela, une possibilité serait de traiter chaque sommet, puis d'assembler des triangles. Par exemple, la carte graphique pourrait transformer/éclairer les sommets un par un, en parcourant le tampon de sommets, avant d'utiliser le tampon d'indice pour assembler des triangles lors de l'assemblage de primitives. Mais le problème est que cela ne marcherait bien que si le résultat du ''vertex shader'' est enregistré en mémoire, puis relu lors de l'assemblage de primitives. Or, ce n'est pas le cas. Pour compenser, une fois qu'un sommet a été transformé/éclairé, il est possible de mémoriser celui-ci dans une mémoire cache, appelée le '''''Post Transform Cache'''''. Le cache est utilisé avec la coopération de l''input assembler'' et de l'assemblage de primitives. Pour utiliser ces caches, la carte graphique utilise les indices de la représentation indicée. La carte graphique détecte qu'un sommet a déjà été traité en regardant l'indice utilisé (et l'adresse du tampon d'indice utilisé, mais passons cela sous silence). Lorsque l'''input assembler'' charge un sommet, il envoie l'indice au cache. Le cache répond alors en disant : j'ai une copie du sommet correspondant à cet indice, ou je n'ai pas la copie. Si le cache dispose d'une copie du sommet, il n'est pas chargé. Notons que ces caches ont disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} nw53pjmorsont5xtnc45ippsx4a63pl 763088 763087 2026-04-06T20:30:36Z Mewtow 31375 /* Les caches de l'ancien pipeline géométrique */ 763088 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. C'est une unité d'accès mémoire un peu particulière, 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 de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===Le cache de sommet=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais il arrive que ce ne soit pas le cas, si des sommets sont dupliqués. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. L'usage d'un tampon d'indice présente aussi des opportunités d'optimisation, qui demandent cependant d'ajouter des mémoires caches à la carte graphique. Une possibilité serait de traiter les sommets un par un, en parcourant le tampon de sommets. Le ''vertex shader'' est alors executé, puis enregistre sont résultat en mémoire. Puis, l'assemblage de primitives utiliserait le tampon d'indice pour assembler des triangles lors de l'assemblage de primitives. Mais le problème est que cela ne marcherait pas très bien en termes de performance, vu qu'on enregistre le résultat des shaders dans la mémoire vidéo. Une solution alternative est utilisée, qui n'accède pas à la mémoire vidéo. L'idée est que le résultat du ''vertex shader'' est enregistré dans un '''cache de sommets''', aussi appelé le '''''Post Transform Cache'''''. Le cache de sommet est une mémoire FIFO, qui mémorise les N derniers sommets calculés. Elle se situe entre l'unité de ''vertex shader''/T&L, et l'unité d'assemblage des primitives. Le cache est utilisé avec la coopération de l''input assembler'' et de l'assemblage de primitives. [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Lorsque l'''input assembler'' charge un sommet, il réserve une place dans le cache de sommet. Une fois que le ''vertex shader'' a terminé son travail, il enregistre le résultat dans cette place réservé. L'assemblage de primitive consulte ce cache et peut lire le sommet directement dedans. La carte graphique détecte qu'un sommet a déjà été traité en regardant l'indice utilisé (et l'adresse du tampon d'indice utilisé, mais passons cela sous silence). Lorsque l'''input assembler'' charge un sommet, il envoie l'indice au cache. Le cache répond alors en disant : j'ai une copie du sommet correspondant à cet indice, ou je n'ai pas la copie. Si le cache dispose d'une copie du sommet, il n'est pas chargé. Notons que ces caches ont disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} ch5wd3l21f4m6fe8y99zxduf3zriyl9 763089 763088 2026-04-06T20:31:38Z Mewtow 31375 /* Le cache de sommet */ 763089 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. C'est une unité d'accès mémoire un peu particulière, 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 de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===Le cache de sommet=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais il arrive que ce ne soit pas le cas, si des sommets sont dupliqués. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. L'usage d'un tampon d'indice présente aussi des opportunités d'optimisation, qui demandent cependant d'ajouter des mémoires caches à la carte graphique. Une possibilité serait de traiter les sommets un par un, en parcourant le tampon de sommets. Le ''vertex shader'' est alors executé, puis enregistre sont résultat en mémoire. Puis, l'assemblage de primitives utiliserait le tampon d'indice pour assembler des triangles lors de l'assemblage de primitives. Mais le problème est que cela ne marcherait pas très bien en termes de performance, vu qu'on enregistre le résultat des shaders dans la mémoire vidéo. Une solution alternative est utilisée, qui n'accède pas à la mémoire vidéo. L'idée est que le résultat du ''vertex shader'' est enregistré dans un '''cache de sommets''', aussi appelé le '''''Post Transform Cache'''''. Le cache de sommet est une mémoire FIFO, qui mémorise les N derniers sommets calculés. Elle se situe entre l'unité de ''vertex shader''/T&L, et l'unité d'assemblage des primitives. Le cache est utilisé avec la coopération de l''input assembler'' et de l'assemblage de primitives. [[File:Vertex cache.png|centre|vignette|upright=2.0|Cache de sommets.]] Lorsque l'''input assembler'' charge un sommet, il réserve une place dans le cache de sommet. Une fois que le ''vertex shader'' a terminé son travail, il enregistre le résultat dans cette place réservé. L'assemblage de primitive consulte ce cache et peut lire le sommet directement dedans. La carte graphique détecte qu'un sommet a déjà été traité en regardant l'indice utilisé (et l'adresse du tampon d'indice utilisé, mais passons cela sous silence). Lorsque l'''input assembler'' charge un sommet, il envoie l'indice au cache. Le cache répond alors en disant : j'ai une copie du sommet correspondant à cet indice, ou je n'ai pas la copie. Si le cache dispose d'une copie du sommet, il n'est pas chargé. Notons que ces caches ont disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. Je recommande la lecture de l'article "Revisiting The Vertex Cache: Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} dvxgny8lz923kkjpzpjuamnn9fv89d0 763090 763089 2026-04-06T20:39:50Z Mewtow 31375 /* Le cache de sommet */ 763090 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. C'est une unité d'accès mémoire un peu particulière, 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 de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===Le cache de sommet=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais il arrive que ce ne soit pas le cas, si des sommets sont dupliqués. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. L'usage d'un tampon d'indice présente aussi des opportunités d'optimisation, qui demandent cependant d'ajouter des mémoires caches à la carte graphique. Une possibilité serait de traiter les sommets un par un, en parcourant le tampon de sommets. Le ''vertex shader'' est alors executé, puis enregistre sont résultat en mémoire. Puis, l'assemblage de primitives utiliserait le tampon d'indice pour assembler des triangles lors de l'assemblage de primitives. Mais le problème est que cela ne marcherait pas très bien en termes de performance, vu qu'on enregistre le résultat des shaders dans la mémoire vidéo. Une solution alternative est utilisée, qui n'accède pas à la mémoire vidéo. L'idée est que le résultat du ''vertex shader'' est enregistré dans un '''cache de sommets''', aussi appelé le '''''Post Transform Cache'''''. Le cache de sommet est une mémoire FIFO, qui mémorise les N derniers sommets calculés. Elle se situe entre l'unité de ''vertex shader''/T&L, et l'unité d'assemblage des primitives. Le cache est utilisé avec la coopération de l''input assembler'' et de l'assemblage de primitives. La carte graphique détecte qu'un sommet a déjà été traité en regardant l'indice utilisé (et l'adresse du tampon d'indice utilisé, mais passons cela sous silence). Notons que ces caches ont disparu depuis que les unités de ''vertex shader'' ont été fusionnées avec les unités de ''pixel shaders''. Un tel cache se mariait bien avec des unités géométriques séparées des circuits de gestion des pixels, en raison de sa spécialisation. Je recommande la lecture de l'article "Revisiting The Vertex Cache: Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} mrqrtejrtf9rat7lsc6brcay50kddkx 763091 763090 2026-04-06T20:46:00Z Mewtow 31375 /* Le cache de sommet */ 763091 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. C'est une unité d'accès mémoire un peu particulière, 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 de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===Le cache de sommet=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais il arrive que ce ne soit pas le cas, si des sommets sont dupliqués. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. L'usage d'un tampon d'indice présente aussi des opportunités d'optimisation, qui demandent cependant d'ajouter des mémoires caches à la carte graphique. Une possibilité serait de traiter les sommets un par un, en parcourant le tampon de sommets. Le ''vertex shader'' est alors executé, puis enregistre sont résultat en mémoire. Puis, l'assemblage de primitives utiliserait le tampon d'indice pour assembler des triangles lors de l'assemblage de primitives. Mais le problème est que cela ne marcherait pas très bien en termes de performance, vu qu'on enregistre le résultat des shaders dans la mémoire vidéo. Une solution alternative est utilisée, qui n'accède pas à la mémoire vidéo. L'idée est que le résultat du ''vertex shader'' est enregistré dans un '''cache de sommets''', aussi appelé le '''''Post Transform Cache'''''. Le cache de sommet est une mémoire FIFO, qui mémorise les N derniers sommets calculés. Elle se situe entre l'unité de ''vertex shader''/T&L, et l'unité d'assemblage des primitives. Le cache est utilisé avec la coopération de l''input assembler'' et de l'assemblage de primitives. La carte graphique détecte qu'un sommet a déjà été traité en regardant l'indice utilisé (et l'adresse du tampon d'indice utilisé, mais passons cela sous silence). Notons que ce cache a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache: Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Il faut noter qu'il existe aussi un '''''pre-transform cache''''', situé avant l'unité de ''vertex shader'', ou l'unité de T&L. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire. En réalité, l'''input assembler'' charge les sommets par blocs, par paquets de 32 à 64 sommets. Il mémorise alors ces sommets dans le cache de sommets, et ceux-ci sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. Les sommets ont donc été préchargés en avance. [[File:Vertex cache.png|centre|vignette|upright=2.5|''Pre-transform cache'']] ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} c3tmbul1fwro4sfdu2l4jdxvftvdcl3 763092 763091 2026-04-06T20:48:17Z Mewtow 31375 /* Le cache de sommet */ 763092 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. C'est une unité d'accès mémoire un peu particulière, 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 de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===Le cache de sommet=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais il arrive que ce ne soit pas le cas, si des sommets sont dupliqués. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. L'usage d'un tampon d'indice présente aussi des opportunités d'optimisation, qui demandent cependant d'ajouter des mémoires caches à la carte graphique. Une possibilité serait de traiter les sommets un par un, en parcourant le tampon de sommets. Le ''vertex shader'' est alors executé, puis enregistre sont résultat en mémoire. Puis, l'assemblage de primitives utiliserait le tampon d'indice pour assembler des triangles lors de l'assemblage de primitives. Mais le problème est que cela ne marcherait pas très bien en termes de performance, vu qu'on enregistre le résultat des shaders dans la mémoire vidéo. Une solution alternative est utilisée, qui n'accède pas à la mémoire vidéo. L'idée est que le résultat du ''vertex shader'' est enregistré dans le '''''Post Transform Cache'''''. Le ''Post Transform Cache'' est une mémoire FIFO, qui mémorise les N derniers sommets calculés. Elle se situe entre l'unité de ''vertex shader''/T&L, et l'unité d'assemblage des primitives. Ce cache est utilisé avec la coopération de l''input assembler'' et de l'assemblage de primitives. La carte graphique détecte qu'un sommet a déjà été traité en regardant l'indice utilisé (et l'adresse du tampon d'indice utilisé, mais passons cela sous silence). : Notons que ce cache a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache: Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Il faut noter qu'il existe aussi un '''''pre-transform cache''''', situé avant l'unité de ''vertex shader'', ou l'unité de T&L. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire. En réalité, l'''input assembler'' charge les sommets par blocs, par paquets de 32 à 64 sommets. Il mémorise alors ces sommets dans le cache de sommets, et ceux-ci sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. Les sommets ont donc été préchargés en avance. [[File:Vertex cache.png|centre|vignette|upright=2.5|''Pre-transform cache'']] ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} ekjja3nbjx62c2auw1nhyc7iz51jvfv 763093 763092 2026-04-06T20:55:24Z Mewtow 31375 /* Le cache de sommet */ 763093 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. C'est une unité d'accès mémoire un peu particulière, 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 de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===Le cache de sommet=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais il arrive que ce ne soit pas le cas, si des sommets sont dupliqués. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. L'usage d'un tampon d'indice présente aussi des opportunités d'optimisation, qui demandent cependant d'ajouter des mémoires caches à la carte graphique. Une possibilité serait de traiter les sommets un par un, en parcourant le tampon de sommets. Le ''vertex shader'' est alors executé, puis enregistre sont résultat en mémoire. Puis, l'assemblage de primitives utiliserait le tampon d'indice pour assembler des triangles lors de l'assemblage de primitives. Mais le problème est que cela ne marcherait pas très bien en termes de performance, vu qu'on enregistre le résultat des shaders dans la mémoire vidéo. Une solution alternative est utilisée, qui n'accède pas à la mémoire vidéo. L'idée est que le résultat du ''vertex shader'' est enregistré dans le '''''Post Transform Cache'''''. Le ''Post Transform Cache'' est une mémoire FIFO, qui mémorise les N derniers sommets calculés. Elle se situe entre l'unité de ''vertex shader''/T&L, et l'unité d'assemblage des primitives. Ce cache est utilisé avec la coopération de l''input assembler'' et de l'assemblage de primitives. La carte graphique détecte qu'un sommet a déjà été traité en regardant l'indice utilisé (et l'adresse du tampon d'indice utilisé, mais passons cela sous silence). Il faut noter qu'il existe aussi un '''''pre-transform cache''''', situé avant l'unité de ''vertex shader'', ou l'unité de T&L. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire. En réalité, l'''input assembler'' charge les sommets par blocs, par paquets de 32 à 64 sommets. Il mémorise alors ces sommets dans le cache de sommets, et ceux-ci sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. Les sommets ont donc été préchargés en avance. [[File:Vertex cache.png|centre|vignette|upright=2.5|''Pre-transform'' et ''Post-transform cache''.]] Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache: Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} b56iuh6027vw6ckq8p0mg17n8tcff3s 763094 763093 2026-04-06T20:57:40Z Mewtow 31375 /* Le cache de sommet */ 763094 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. C'est une unité d'accès mémoire un peu particulière, 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 de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===Le cache de sommet=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais il arrive que ce ne soit pas le cas, si des sommets sont dupliqués. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. L'usage d'un tampon d'indice présente aussi des opportunités d'optimisation, qui demandent cependant d'ajouter des mémoires caches à la carte graphique. Il y a précisément deux caches, qui sont collectivement appelés des '''caches de sommets'''. La première optimisation mémorise le résultat du ''vertex shader'' dans un cache dédié : le '''''Post Transform Cache'''''. Le ''Post Transform Cache'' est une mémoire FIFO, qui mémorise les N derniers sommets calculés. Elle se situe entre l'unité de ''vertex shader''/T&L, et l'unité d'assemblage des primitives. Ce cache est utilisé avec la coopération de l''input assembler'' et de l'assemblage de primitives. La carte graphique détecte qu'un sommet a déjà été traité en regardant l'indice utilisé (et l'adresse du tampon d'indice utilisé, mais passons cela sous silence). Une seconde optimisation mémorise les sommets chargés par l'''input assemblerr'' dans un cache séparé : le '''''pre-transform cache''''', situé avant l'unité de ''vertex shader'', ou l'unité de T&L. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire. En réalité, l'''input assembler'' charge les sommets par blocs, par paquets de 32 à 64 sommets. Il mémorise alors ces sommets dans le cache de sommets, et ceux-ci sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. Les sommets ont donc été préchargés en avance. [[File:Vertex cache.png|centre|vignette|upright=2.5|''Pre-transform'' et ''Post-transform cache''.]] Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache: Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} 6zss23d8oijqsqvbdx0eakabsmlhxmx 763095 763094 2026-04-06T20:59:45Z Mewtow 31375 /* Le cache de sommet */ 763095 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. C'est une unité d'accès mémoire un peu particulière, 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 de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===Le cache de sommet=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais il arrive que ce ne soit pas le cas, si des sommets sont dupliqués. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. L'usage d'un tampon d'indice présente aussi des opportunités d'optimisation, qui demandent cependant d'ajouter des mémoires caches à la carte graphique. Il y a précisément deux caches, qui sont collectivement appelés des '''caches de sommets'''. La carte graphique consulte ces caches en leur envoyant l'indice du sommet (et l'adresse du tampon d'indice utilisé, pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard ). La première optimisation mémorise le résultat du ''vertex shader'' dans un cache dédié : le '''''Post Transform Cache'''''. Le ''Post Transform Cache'' est une mémoire FIFO, qui mémorise les N derniers sommets calculés. Elle se situe entre l'unité de ''vertex shader''/T&L, et l'unité d'assemblage des primitives. Ce cache est utilisé avec la coopération de l'''input assembler'' et de l'assemblage de primitives. Une seconde optimisation mémorise les sommets chargés par l'''input assembler'' dans un cache séparé : le '''''pre-transform cache''''', situé avant l'unité de ''vertex shader''. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire. En réalité, l'''input assembler'' charge les sommets par blocs, par paquets de 32 à 64 sommets. Il mémorise alors ces sommets dans le cache de sommets, et ceux-ci sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. Les sommets ont donc été préchargés en avance. [[File:Vertex cache.png|centre|vignette|upright=2.5|''Pre-transform'' et ''Post-transform cache''.]] Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache: Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} qdir5fa52q6xl5vk0pmgsygatf0tbex 763096 763095 2026-04-06T21:00:47Z Mewtow 31375 /* Le cache de sommet */ 763096 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. C'est une unité d'accès mémoire un peu particulière, 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 de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===Le cache de sommet=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais il arrive que ce ne soit pas le cas, si des sommets sont dupliqués. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. L'usage d'un tampon d'indice présente aussi des opportunités d'optimisation, qui demandent cependant d'ajouter des mémoires caches à la carte graphique. Il y a précisément deux caches, qui sont collectivement appelés des '''caches de sommets'''. La carte graphique consulte ces caches en leur envoyant l'indice du sommet (et l'adresse du tampon d'indice utilisé, pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard ). La première optimisation mémorise le résultat du ''vertex shader'' dans un cache dédié : le '''''Post Transform Cache'''''. Le ''Post Transform Cache'' est une mémoire FIFO, qui mémorise les N derniers sommets calculés. Elle se situe entre l'unité de ''vertex shader''/T&L, et l'unité d'assemblage des primitives. Ce cache est utilisé avec la coopération de l'''input assembler'' et de l'assemblage de primitives. Une seconde optimisation mémorise les sommets chargés par l'''input assembler'' dans un cache séparé : le '''''pre-transform cache''''', situé avant l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire. En réalité, l'''input assembler'' charge les sommets par blocs, par paquets de 32 à 64 sommets. Il mémorise les derniers blocs dans le cache de sommets, et ceux-ci sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. Les sommets ont donc été préchargés en avance. [[File:Vertex cache.png|centre|vignette|upright=2.5|''Pre-transform'' et ''Post-transform cache''.]] Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} 3tq1fmvgsss8hb1twxi0sseffsainem 763097 763096 2026-04-06T21:01:16Z Mewtow 31375 /* Le cache de sommet */ 763097 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. C'est une unité d'accès mémoire un peu particulière, 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 de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===Le cache de sommet=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais il arrive que ce ne soit pas le cas, si des sommets sont dupliqués. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. L'usage d'un tampon d'indice présente aussi des opportunités d'optimisation, qui demandent cependant d'ajouter des mémoires caches à la carte graphique. Il y a précisément deux caches, qui sont collectivement appelés des '''caches de sommets'''. La carte graphique consulte ces caches en leur envoyant l'indice du sommet (et l'adresse du tampon d'indice utilisé, pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard ). La première optimisation mémorise le sommet transformé/éclairé dans un cache dédié : le '''''Post Transform Cache'''''. Le ''Post Transform Cache'' est une mémoire FIFO, qui mémorise les N derniers sommets calculés. Elle se situe entre l'unité de ''vertex shader''/T&L, et l'unité d'assemblage des primitives. Ce cache est utilisé avec la coopération de l'''input assembler'' et de l'assemblage de primitives. Une seconde optimisation mémorise les sommets chargés par le circuit ''input assembler'' dans un cache séparé : le '''''pre-transform cache''''', situé avant l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire. En réalité, l'''input assembler'' charge les sommets par blocs, par paquets de 32 à 64 sommets. Il mémorise les derniers blocs dans le cache de sommets, et ceux-ci sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. Les sommets ont donc été préchargés en avance. [[File:Vertex cache.png|centre|vignette|upright=2.5|''Pre-transform'' et ''Post-transform cache''.]] Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} 5f8xi21g3pkhmmi3d5puqur1d62e8vs 763098 763097 2026-04-06T21:14:14Z Mewtow 31375 /* L’implémentation matérielle du pipeline géométrique */ 763098 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. L'''input assembler'' charge les sommets de la mémoire vidéo dans les unités de traitement des sommets (circuit de T&L ou processeurs de''vertex shader''). Il procéde différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. Pour faire son travail, il a besoin de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, 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. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===Le cache de sommet=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais il arrive que ce ne soit pas le cas, si des sommets sont dupliqués. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. L'usage d'un tampon d'indice présente aussi des opportunités d'optimisation, qui demandent cependant d'ajouter des mémoires caches à la carte graphique. Il y a précisément deux caches, qui sont collectivement appelés des '''caches de sommets'''. La carte graphique consulte ces caches en leur envoyant l'indice du sommet (et l'adresse du tampon d'indice utilisé, pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard ). La première optimisation mémorise le sommet transformé/éclairé dans un cache dédié : le '''''Post Transform Cache'''''. Le ''Post Transform Cache'' est une mémoire FIFO, qui mémorise les N derniers sommets calculés. Elle se situe entre l'unité de ''vertex shader''/T&L, et l'unité d'assemblage des primitives. Ce cache est utilisé avec la coopération de l'''input assembler'' et de l'assemblage de primitives. Une seconde optimisation mémorise les sommets chargés par le circuit ''input assembler'' dans un cache séparé : le '''''pre-transform cache''''', situé avant l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire. En réalité, l'''input assembler'' charge les sommets par blocs, par paquets de 32 à 64 sommets. Il mémorise les derniers blocs dans le cache de sommets, et ceux-ci sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. Les sommets ont donc été préchargés en avance. [[File:Vertex cache.png|centre|vignette|upright=2.5|''Pre-transform'' et ''Post-transform cache''.]] Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} 5hb13v2zrnm0aktir00xhqs3srpmqf3 763099 763098 2026-04-06T21:17:30Z Mewtow 31375 /* L’implémentation matérielle du pipeline géométrique */ 763099 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu rajouter un circuit, situé juste avant l'unité de T&L : l'''input assembler''. Il charge les sommets, depuis la mémoire vidéo, dans les unités de T&L/''Vertex shader''. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. Pour faire son travail, il a besoin de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, 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. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] L'''input assembler'' est suivi par une étape de '''transformation-projection''', puis une étape d'éclairage. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. En sortie de l'étage précédent, on n'a que des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===Le cache de sommet=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais il arrive que ce ne soit pas le cas, si des sommets sont dupliqués. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. L'usage d'un tampon d'indice présente aussi des opportunités d'optimisation, qui demandent cependant d'ajouter des mémoires caches à la carte graphique. Il y a précisément deux caches, qui sont collectivement appelés des '''caches de sommets'''. La carte graphique consulte ces caches en leur envoyant l'indice du sommet (et l'adresse du tampon d'indice utilisé, pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard ). La première optimisation mémorise le sommet transformé/éclairé dans un cache dédié : le '''''Post Transform Cache'''''. Le ''Post Transform Cache'' est une mémoire FIFO, qui mémorise les N derniers sommets calculés. Elle se situe entre l'unité de ''vertex shader''/T&L, et l'unité d'assemblage des primitives. Ce cache est utilisé avec la coopération de l'''input assembler'' et de l'assemblage de primitives. Une seconde optimisation mémorise les sommets chargés par le circuit ''input assembler'' dans un cache séparé : le '''''pre-transform cache''''', situé avant l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire. En réalité, l'''input assembler'' charge les sommets par blocs, par paquets de 32 à 64 sommets. Il mémorise les derniers blocs dans le cache de sommets, et ceux-ci sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. Les sommets ont donc été préchargés en avance. [[File:Vertex cache.png|centre|vignette|upright=2.5|''Pre-transform'' et ''Post-transform cache''.]] Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} ls42vzllpgpkuh7x319rzeup4110xw2 763100 763099 2026-04-06T21:18:35Z Mewtow 31375 /* L’implémentation matérielle du pipeline géométrique */ 763100 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu rajouter un circuit, situé juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets, depuis la mémoire vidéo, dans les unités géométriques. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. Pour faire son travail, il a besoin de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, 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. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Pour le reste, les étapes de transformation-projection ont été implémentée soit avec des circuits fixes, soit avec des processeurs. ===Le cache de sommet=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais il arrive que ce ne soit pas le cas, si des sommets sont dupliqués. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. L'usage d'un tampon d'indice présente aussi des opportunités d'optimisation, qui demandent cependant d'ajouter des mémoires caches à la carte graphique. Il y a précisément deux caches, qui sont collectivement appelés des '''caches de sommets'''. La carte graphique consulte ces caches en leur envoyant l'indice du sommet (et l'adresse du tampon d'indice utilisé, pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard ). La première optimisation mémorise le sommet transformé/éclairé dans un cache dédié : le '''''Post Transform Cache'''''. Le ''Post Transform Cache'' est une mémoire FIFO, qui mémorise les N derniers sommets calculés. Elle se situe entre l'unité de ''vertex shader''/T&L, et l'unité d'assemblage des primitives. Ce cache est utilisé avec la coopération de l'''input assembler'' et de l'assemblage de primitives. Une seconde optimisation mémorise les sommets chargés par le circuit ''input assembler'' dans un cache séparé : le '''''pre-transform cache''''', situé avant l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire. En réalité, l'''input assembler'' charge les sommets par blocs, par paquets de 32 à 64 sommets. Il mémorise les derniers blocs dans le cache de sommets, et ceux-ci sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. Les sommets ont donc été préchargés en avance. [[File:Vertex cache.png|centre|vignette|upright=2.5|''Pre-transform'' et ''Post-transform cache''.]] Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} 71x11kom8fsunzavxaf79l5mpq76uit 763101 763100 2026-04-06T21:19:00Z Mewtow 31375 /* L’implémentation matérielle du pipeline géométrique */ 763101 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu rajouter un circuit, situé juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets, depuis la mémoire vidéo, dans les unités géométriques. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. Pour faire son travail, il a besoin de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, 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. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] ===Le cache de sommet=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais il arrive que ce ne soit pas le cas, si des sommets sont dupliqués. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. L'usage d'un tampon d'indice présente aussi des opportunités d'optimisation, qui demandent cependant d'ajouter des mémoires caches à la carte graphique. Il y a précisément deux caches, qui sont collectivement appelés des '''caches de sommets'''. La carte graphique consulte ces caches en leur envoyant l'indice du sommet (et l'adresse du tampon d'indice utilisé, pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard ). La première optimisation mémorise le sommet transformé/éclairé dans un cache dédié : le '''''Post Transform Cache'''''. Le ''Post Transform Cache'' est une mémoire FIFO, qui mémorise les N derniers sommets calculés. Elle se situe entre l'unité de ''vertex shader''/T&L, et l'unité d'assemblage des primitives. Ce cache est utilisé avec la coopération de l'''input assembler'' et de l'assemblage de primitives. Une seconde optimisation mémorise les sommets chargés par le circuit ''input assembler'' dans un cache séparé : le '''''pre-transform cache''''', situé avant l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire. En réalité, l'''input assembler'' charge les sommets par blocs, par paquets de 32 à 64 sommets. Il mémorise les derniers blocs dans le cache de sommets, et ceux-ci sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. Les sommets ont donc été préchargés en avance. [[File:Vertex cache.png|centre|vignette|upright=2.5|''Pre-transform'' et ''Post-transform cache''.]] Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} 9alrmii6r0iorza9qz0shtbjzhql680 763102 763101 2026-04-06T21:21:46Z Mewtow 31375 /* Le cache de sommet */ 763102 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. 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''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * 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. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Le pipeline géométrique de cette période était différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique n'était pas composé que de processeurs pour réaliser les étapes de transformation et de projection, il y avait deux autres circuits : un ''input assembler'' et un circuit d'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | Transformation, projection, éclairage | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du 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. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : 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. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L’implémentation matérielle du pipeline géométrique=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu rajouter un circuit, situé juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets, depuis la mémoire vidéo, dans les unités géométriques. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compréssée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Avec les ''triangle strip'', il doit mémoriser les deux derniers sommets chargés, pour les combiner avec le prochain sommet chargé. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. La FIFO est mise à jour à chaque fois qu'un sommet est chargé. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. Pour faire son travail, il a besoin de 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). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, 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. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Et les sommets ne sont pas dans l'ordre : deux sommets qui en sortent à la suite ne sont pas forcément dans le même triangle. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. L'étape d'assemblage de primitives est suivie par un '''tampon de primitives''', dans lequel les triangles sont accumulés avant d'entrer dans le rastériseur. Le contenu du tampon de primitive varie suivant la carte graphique, mais il y a deux possibilités principales. La première est simplement un paquet de sommets avec un petit tampon d'indices associé. L'autre est simplement un paquet de sommets, avec des sommets dupliqués s'ils sont partagés par plusieurs triangles. La première solution fait un meilleur usage de la mémoire du tampon de primitive, l'autre est plus simple et plus rapide, plus simple d'utilisation pour le rastériseur. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] ===Le cache de sommet=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais il arrive que ce ne soit pas le cas, si des sommets sont dupliqués. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. L'usage d'un tampon d'indice présente aussi des opportunités d'optimisation, qui demandent cependant d'ajouter des mémoires caches à la carte graphique. Il y a précisément deux caches, qui sont collectivement appelés des '''caches de sommets'''. La carte graphique consulte ces caches en leur envoyant l'indice du sommet (et l'adresse du tampon d'indice utilisé, pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard ). La premier cache mémorise le sommet transformé/éclairé dans un cache dédié. Il est appelé le '''''Post Transform Cache'''''. C'est une mémoire FIFO, qui mémorise les N derniers sommets calculés. Elle se situe entre l'unité de ''vertex shader''/T&L, et l'unité d'assemblage des primitives. Ce cache est utilisé avec la coopération de l'''input assembler'' et de l'assemblage de primitives. Le second cache mémorise les sommets chargés dans un cache séparé : le '''''pre-transform cache''''', situé avant l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire. En réalité, l'''input assembler'' charge les sommets par blocs, par paquets de 32 à 64 sommets. Il mémorise les derniers blocs dans le cache de sommets, et ceux-ci sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. Les sommets ont donc été préchargés en avance. [[File:Vertex cache.png|centre|vignette|upright=2.5|''Pre-transform'' et ''Post-transform cache''.]] Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'évolution du pipeline géométrique des cartes 3D=== Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée '''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'''input assembler'', ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. Les deux étapes du milieu étaient le fait d'un circuit de T&L (''Transform & Lightning'') unique, qui n'était pas programmable. Il était accompagné de deux circuits fixes pour les deux étapes restantes. L''''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} fdpcr7d6st0t3s8nj4ofjdxyvggmfqs Mathc initiation/Fichiers h : c44a4 0 76524 763103 762828 2026-04-07T11:22:53Z Xhungab 23827 763103 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] : Je vous propose comme cours de référence les trois livres de '''openstax''' en accès libre. Vous pouvez les lire en ligne ou télécharger les PDF. * [https://openstax.org/details/books/calculus-volume-1 Calculus 1], * [https://openstax.org/details/books/calculus-volume-2 Calculus 2], * [https://openstax.org/details/books/calculus-volume-3 Calculus 3]. : {{Partie{{{type|}}}|[[Mathc initiation/a08| Analyse I ; Les fonctions]]}} : {{Partie{{{type|}}}|[[Mathc initiation/Fichiers h : c60a3| Analyse I : Les courbes paramétriques]]}} : {{Partie{{{type|}}}|[[Mathc initiation/c58a3| Analyse I : Les fonctions vectorielles]]}} : {{Partie{{{type|}}}|[[Mathc initiation/a334| Analyse I : Les suites et les séries]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/a16| Analyse II : Dérivées partielles et applications]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/a09| Analyse III : Intégrale doubles et applications]]}} {{Partie{{{type|}}}|[[Mathc initiation/0044| Analyse III : Intégrale triples et applications]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/005j| Analyse III : Petite introduction sur les intégrales curvilignes]]}} {{Partie{{{type|}}}|[[Mathc initiation/0045| Analyse III : Intégrale curvilignes et applications.]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/005k| Analyse III : Petite introduction sur les champs de vecteurs]]}} {{Partie{{{type|}}}|[[Mathc initiation/005l| Analyse III : Intégrale curvilignes dans un champ de vecteurs.]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/005h| Analyse III : L'intégrale de surface]]}} {{Partie{{{type|}}}|[[Mathc initiation/005s| Analyse III : L'intégrale de flux de surface]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/004w| Analyse III : Théorème de Gauss (Théorème de la divergence)]]}} {{Partie{{{type|}}}|[[Mathc initiation/005i| Analyse III : Théorème de Green, de Stoke]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/a10| Analyse III : Les équations différentielles]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/a512| Analyse IV : Se familiariser avec la Transformée de Laplace]]}} : {{Partie{{{type|}}}|[[Mathc initiation/a584| Analyse IV : Se familiariser avec la transformée en Z]]}} : {{Partie{{{type|}}}|[[Mathc initiation/a592| Analyse IV : Se familiariser avec la transformée de Fourier discrète]]}} : {{Partie{{{type|}}}|[[Mathc initiation/a594| Analyse IV : Se familiariser avec les series de Fourier]]}} : . : {{Partie{{{type|}}}|fond={{{fond|}}}|prefixTable=I - |prefix1=Troisième Partie : | '''Bibliothèques'''}} : Pour éviter de devoir télécharger les bibliothèques à chaque sections, je vous propose ici de télécharger les fichiers le plus souvent utilisés. {{Partie{{{type|}}}|[[Mathc initiation/a406| La bibliothèque d'analyse I, II, III :]]}} : {{AutoCat}} tfcezcx9kqo0ncr7j0f8mjn39dxkf5i Les cartes graphiques/Le rendu d'une scène 3D : concepts de base 0 79234 763054 761582 2026-04-06T18:10:56Z Mewtow 31375 /* Les changements de coordonnées se font via des multiplications de matrices */ 763054 wikitext text/x-wiki Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D. ==Les bases du rendu 3D== Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme. ===Les objets 3D et leur géométrie=== [[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]] Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues. [[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]] : En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes. Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet. Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''. [[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]] Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL : [[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]] Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par : * une position ; * par la direction du regard (un vecteur). A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''. [[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]] [[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]] La majorité des jeux vidéos ajoutent deux plans : * un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches. * Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains. Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels. ===Les textures=== Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief. [[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]] Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres. Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique. Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas. ===La différence entre rastérisation et lancer de rayons=== [[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]] Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal. La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''. La rastérisation est structurée autour de trois étapes principales : * une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ; * une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ; * une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran. [[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]] L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée. Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit. ==Le calcul de la géométrie== Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective. ===Les trois étapes de transformation=== La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc 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, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets.. [[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]] Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. 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). [[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]] Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces. ===Les changements de coordonnées se font via des multiplications de matrices=== Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions de détail. Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble. Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc. Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps. ==L'élimination des surfaces cachées== Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. 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''. Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre. ===Les différentes formes de ''culling''/''clipping''=== La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable. [[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]] Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''. [[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]] Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger. L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z. ===L'algorithme du peintre=== Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''. [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ===Le tampon de profondeur=== Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle 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. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur. [[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]] Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives. Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective. Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne : [[File:Z-fighting.png|centre|vignette|Z-fighting]] ==La rastérisation== L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres. L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures. ===Le rendu en fil de fer=== [[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]] Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS. {| |[[File:Maze war.jpg|vignette|Maze war]] |[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]] |} Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet. L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer. Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres. ===Le rendu à primitives colorées=== [[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]] Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''. [[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]] La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique. Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés. Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet : * [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.] ===Le placage de textures direct=== Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D. L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''. L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''. [[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]] La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire. : Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran. Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse. L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes. Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''. {|class="wikitable" |- ! Géométrie | Processeurs dédiés programmé pour émuler le pipeline graphique |- ! Tri des quads du plus lointain au plus proche | Processeur principal (implémentation logicielle) |- ! Application des textures | ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''. |} L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque. Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D. [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn. ===Le placage de textures inverse=== Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. [[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]] Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. [[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]] Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés. [[File:01 3D-Rasterung-a.svg|vignette|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.]] Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé. Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités. L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. ==L'éclairage d'une scène 3D== L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation. Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé du '''''vertex lighting''''', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet/triangle d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu. L''''éclairage par pixel''' (''per-pixel lighting''), est calculé pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter. En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable. ===Les sources de lumière et les couleurs associées=== Dans ce qui suit, on part du principe que les modèles 3D ont une surface, sur laquelle on peut placer des points. Vous vous dites que ces points de surface sont tout bêtement les sommets et c'est vrai que c'est une possibilité. Mais sachez que ce n'est pas systématique. On peut aussi faire en sorte que les points de surface soient au centre des triangles du modèle 3D. Pour simplifier, vous pouvez considérer que le terme "point de la surface" correspond à un sommet, ce sera l'idéal et suffira largement pour les explications. L'éclairage attribue à chaque point de la surface une '''illumination''', à savoir sa luminosité, l'intensité de la lumière réfléchie par la surface. L'illumination d'un point de surface est définie par un niveau de gris. Plus un point de surface a une illumination importante, plus il a une couleur proche du blanc. Et inversement, plus son illumination est faible, plus il est proche du noir. L'attribution d'une illumination à chaque point de surface fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier la géométrie. Pour cela, il suffit que l'éclairage ne fasse pas que rendre la scène 3D en niveaux de gris, mais que les niveaux de luminosité sont calculés indépendamment pour chaque couleur RGB. L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, etc. Celles-ci sont souvent 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'''. Mais en plus de ces sources de lumière, il faut ajouter une '''lumière ambiante''', qui sert à simuler l’éclairage ambiant (d’où le terme lumière ambiante). Par exemple, elle correspond à la lumière du cycle jour/nuit pour les environnements extérieurs. On peut simuler un cycle jour-nuit simplement en modifiant la lumière ambiante : nulle en pleine nuit noire, élevée en plein jour. En rendu 3D, la lumière ambiante est définie comme une lumière égale en tout point de la scène 3D, et sert notamment à simuler l’éclairage ambiant (d’où le terme lumière ambiante). {| |[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]] |[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Lumière directionnelle.]] |} Le calcul exact de l'illumination de chaque point de surface demande de calculer trois illuminations indépendantes, qui ne proviennent pas des mêmes types de sources lumineuses. * L''''illumination ambiante''' correspond à la lumière ambiante réfléchie par la surface. * Les autres formes d'illumination 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. Il existe plusieurs sous-types d'illumination d'origine directionnelles, les deux principales étant les deux suivantes : ** L''''illumination spéculaire''' est la couleur de la lumière réfléchie via la réflexion de Snell-Descartes. ** L''''illumination 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). [[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]] Elles sont additionnées ensemble pour donner l'illumination finale du point de surface. Chaque composante rouge, bleu, ou verte de la couleur est traitée indépendamment des autres, ce qui donne une scène 3D coloriée. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Et ces algorithmes sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé. ===Les données nécessaires pour les algorithmes d'illumination=== L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage directionnel impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses. Le premier de ces paramètres est l''''intensité de la source de lumière''', à quel point elle émet de la lumière. Là encore, cette information est encodée par un simple nombre, un coefficient, spécifié par l'artiste lors de la création du niveau/scène 3D. La couleur de la source de lumière est une version améliorée de l'intensité de la source lumineuse, dans le sens où on a une intensité pour chaque composante RGB. Le second est un nombre attribué à chaque point de surface : le '''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 plusieurs coefficients de réflexion, pour chaque couleur : un pour la couleur ambiante, un pour la couleur diffuse, et un pour la couleur spéculaire. Les calculs de réflexion de la lumière demandent aussi de connaitre l'orientation de la surface. Pour gérer cette orientation, tout point de surface 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 point de surface (sommet ou triangle). [[File:Graphics lightmodel ptsource.png|centre|vignette|Normale de la surface.]] Ensuite, il faut aussi préciser l'orientation de la lumière, dans quel sens est orientée. La majorité des sources de lumière émettent de la lumière dans une direction bien précise. 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. Les sources de lumières habituelles, comme les lampes, émettent dans une direction bien précise, appelée la '''direction privilégiée'''. La direction privilégiée est un vecteur, notée v dans le schéma du dessous. Un autre vecteur important est celui de la ligne entre le point de surface considéré et la caméra (noté w dans le schéma ci-dessous). Il faut aussi tenir comme du vecteur qui trace la ligne entre la source de lumière et le point de surface (noté L dans le schéma ci-dessous). Enfin, il faut ajouter le vecteur qui correspond à la lumière réfléchie par la surface au niveau du point de surface. Ce vecteur, noté R et non-indiqué dans le schéma ci-dessous, se calcule à partir du vecteur L et de la normale. [[File:Graphics lightmodel spot.png|centre|vignette|Vecteurs utilisés dans le calcul de l'illumination, hors normale.]] La plupart de ces informations n'a pas à être calculée. C'est le cas de la normale de la surface ou du vecteur L qui sont connus une fois l'étape de transformation réalisée. Même chose pour le coefficient de réflexion ou de l'intensité de la lumière, qui sont connus dès la création du niveau ou de la scène 3D. Par contre, le reste doit être calculé à la volée par la carte graphique à partir de calculs vectoriels. ===Le calcul des couleurs par un algorithme d'illumination de Phong=== À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D.S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place. L'illumination ambiante d'un point de surface s'obtient à partir de la lumière ambiante, mais elle n'est pas strictement égale à celle-ci, c'est légèrement plus compliqué que ça. Deux objets de la même couleur, illuminés par la même lumière ambiante, ne donneront pas la même couleur. Un objet totalement rouge illuminé par une lumière ambiante donnera une couleur ambiante rouge, un objet vert donnera une lumière verte. Et sans même parler des couleurs, certains objets sont plus sombres à certains endroits et plus clairs à d'autres, ce qui fait que leurs différentes parties ne vont pas réagir de la même manière à la lumière ambiante. La couleur de chaque point de surface de l'objet lui-même est mémorisée dans le modèle 3D, ce qui fait que chaque point de surface se voit attribuer, en plus de ces trois coordonnées, une couleur ambiante qui indique comment celui-ci réagit à la lumière ambiante. La couleur ambiante finale d'un point de surface se calcule en multipliant la couleur ambiante de base par l'intensité de la lumière ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D. : <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. [[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]] L'illumination spéculaire et diffuse sont calculées autrement, à partir des vecteurs indiqués dans le schéma ci-contre. Tous les vecteurs sont définis à partir d'un point de la surface géométrique, d'un sommet, qui est éclairé. Les quatre vecteurs sont les suivants : * la normale N est un vecteur perpendiculaire à la surface, au sommet ; * le vecteur L qui connecte le sommet avec la source de lumière ; * le vecteur V qui connecte la caméra au sommet ; * le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes. Afin de simplifier les explications, il faut préciser deux choses. premièrement, le vecteur L a pour longueur l'intensité de la lumière émise par la source de lumière. Il s'agit d'un vecteur pointant vers la source lumineuse, c'est un choix arbitraire mais pas sans raison. Quant au vecteur pour la normale, sa longueur est le coefficient de réflexion diffuse <math>K_d</math>, qui indique à quel point la surface diffuse de la lumière, l'intensité de la source lumineuse. Il faut dire que ce coefficient est définit pour chaque point d'une surface, Le calcul implique une opération mathématique appelée le ''produit scalaire'', qui s'explique assez simplement. Elle prend deux vecteurs, que l'on notera A et B. Un produit scalaire prend la longueur des deux vecteurs et l'angle entre les deux vecteurs noté <math>\omega</math>. Le produit scalaire se calcule comme suit : : <math>\text{Produit scalaire} = A \times B \times \cos{(\omega)}</math> L'illumination diffuse est calculée en faisant le produit scalaire entre deux vecteurs : la normale de la surface et le vecteur L. L'angle entre N et L donne la manière dont la surface est penchée par rapport à la surface lumineuse : plus elle est penchée, plus la lumière est diffusée et moins l'illumination diffuse est intense. On rappelle que la longueur des deux vecteurs est repsectivement l'intensité de la lumière et le coefficient de réflexion. L'angle en question est noté <math>omega</math> dans l'équation suivante : : <math>\text{Illumination diffuse} = K_d \times I_a \times \cos{(\omega)} = K_d \times I_a \times (\vec{N} \cdot \vec{L})</math> 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, c'est à dire l'angle entre v et R, que nous noterons <math>\Omega</math>. Là encore, on doit utiliser le coefficient de réflexion spéculaire <math>K_s</math> de la surface et l'intensité de la lumière, ce qui donne : : <math>\text{Illumination spéculaire} = K_s \times I_a \times \cos{(\Omega)} = K_s \times I_a \times (\vec{R} \cdot \vec{v})</math> En additionnant ces trois sources d'illumination, on trouve : : <math>\text{Illumination ambiante} = K_a \times I_a + I_a \times \left[ K_d \times \cos{(\omega)} + K_s \times \cos{(\Omega)} \right]</math> ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment. [[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]] L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle. L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''. L''''éclairage de Phong''' est différent des précédents dans le sens où il calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée. [[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]] Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. {| |- |[[File:Flatshading01.png|vignette|upright=1|Flat shading]] |[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]] |[[File:Phongshading01.png|vignette|upright=1|Phong Shading]] |- |[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]] |- |[[File:Per face lighting.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]] |- |[[File:Per face lighting example.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]] |} L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres. ===Le bump-mapping et autres approximations de l'éclairage par pixel=== Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. [[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]] [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] ==Les ''shaders''== Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D. ===L'utilisation des ''shaders'' pour les algorithmes d'éclairage=== Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables. ===Les autres utilisations des ''shaders''=== Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables. Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Le rendu d'une scène 3D : l'API graphique | nextText=Le rendu d'une scène 3D : l'API graphique }}{{autocat}} 69bwrzjux8eeowapzuvqq8c1gpy5e7b 763055 763054 2026-04-06T18:11:08Z Mewtow 31375 /* Les changements de coordonnées se font via des multiplications de matrices */ 763055 wikitext text/x-wiki Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D. ==Les bases du rendu 3D== Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme. ===Les objets 3D et leur géométrie=== [[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]] Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues. [[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]] : En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes. Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet. Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''. [[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]] Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL : [[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]] Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par : * une position ; * par la direction du regard (un vecteur). A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''. [[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]] [[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]] La majorité des jeux vidéos ajoutent deux plans : * un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches. * Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains. Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels. ===Les textures=== Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief. [[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]] Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres. Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique. Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas. ===La différence entre rastérisation et lancer de rayons=== [[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]] Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal. La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''. La rastérisation est structurée autour de trois étapes principales : * une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ; * une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ; * une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran. [[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]] L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée. Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit. ==Le calcul de la géométrie== Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective. ===Les trois étapes de transformation=== La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc 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, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets.. [[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]] Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. 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). [[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]] Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces. ===Les changements de coordonnées se font via des multiplications de matrices=== Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail. Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble. Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc. Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps. ==L'élimination des surfaces cachées== Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. 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''. Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre. ===Les différentes formes de ''culling''/''clipping''=== La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable. [[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]] Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''. [[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]] Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger. L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z. ===L'algorithme du peintre=== Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''. [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ===Le tampon de profondeur=== Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle 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. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur. [[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]] Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives. Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective. Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne : [[File:Z-fighting.png|centre|vignette|Z-fighting]] ==La rastérisation== L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres. L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures. ===Le rendu en fil de fer=== [[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]] Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS. {| |[[File:Maze war.jpg|vignette|Maze war]] |[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]] |} Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet. L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer. Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres. ===Le rendu à primitives colorées=== [[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]] Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''. [[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]] La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique. Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés. Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet : * [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.] ===Le placage de textures direct=== Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D. L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''. L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''. [[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]] La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire. : Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran. Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse. L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes. Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''. {|class="wikitable" |- ! Géométrie | Processeurs dédiés programmé pour émuler le pipeline graphique |- ! Tri des quads du plus lointain au plus proche | Processeur principal (implémentation logicielle) |- ! Application des textures | ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''. |} L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque. Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D. [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn. ===Le placage de textures inverse=== Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. [[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]] Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. [[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]] Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés. [[File:01 3D-Rasterung-a.svg|vignette|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.]] Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé. Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités. L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. ==L'éclairage d'une scène 3D== L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation. Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé du '''''vertex lighting''''', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet/triangle d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu. L''''éclairage par pixel''' (''per-pixel lighting''), est calculé pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter. En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable. ===Les sources de lumière et les couleurs associées=== Dans ce qui suit, on part du principe que les modèles 3D ont une surface, sur laquelle on peut placer des points. Vous vous dites que ces points de surface sont tout bêtement les sommets et c'est vrai que c'est une possibilité. Mais sachez que ce n'est pas systématique. On peut aussi faire en sorte que les points de surface soient au centre des triangles du modèle 3D. Pour simplifier, vous pouvez considérer que le terme "point de la surface" correspond à un sommet, ce sera l'idéal et suffira largement pour les explications. L'éclairage attribue à chaque point de la surface une '''illumination''', à savoir sa luminosité, l'intensité de la lumière réfléchie par la surface. L'illumination d'un point de surface est définie par un niveau de gris. Plus un point de surface a une illumination importante, plus il a une couleur proche du blanc. Et inversement, plus son illumination est faible, plus il est proche du noir. L'attribution d'une illumination à chaque point de surface fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier la géométrie. Pour cela, il suffit que l'éclairage ne fasse pas que rendre la scène 3D en niveaux de gris, mais que les niveaux de luminosité sont calculés indépendamment pour chaque couleur RGB. L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, etc. Celles-ci sont souvent 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'''. Mais en plus de ces sources de lumière, il faut ajouter une '''lumière ambiante''', qui sert à simuler l’éclairage ambiant (d’où le terme lumière ambiante). Par exemple, elle correspond à la lumière du cycle jour/nuit pour les environnements extérieurs. On peut simuler un cycle jour-nuit simplement en modifiant la lumière ambiante : nulle en pleine nuit noire, élevée en plein jour. En rendu 3D, la lumière ambiante est définie comme une lumière égale en tout point de la scène 3D, et sert notamment à simuler l’éclairage ambiant (d’où le terme lumière ambiante). {| |[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]] |[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Lumière directionnelle.]] |} Le calcul exact de l'illumination de chaque point de surface demande de calculer trois illuminations indépendantes, qui ne proviennent pas des mêmes types de sources lumineuses. * L''''illumination ambiante''' correspond à la lumière ambiante réfléchie par la surface. * Les autres formes d'illumination 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. Il existe plusieurs sous-types d'illumination d'origine directionnelles, les deux principales étant les deux suivantes : ** L''''illumination spéculaire''' est la couleur de la lumière réfléchie via la réflexion de Snell-Descartes. ** L''''illumination 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). [[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]] Elles sont additionnées ensemble pour donner l'illumination finale du point de surface. Chaque composante rouge, bleu, ou verte de la couleur est traitée indépendamment des autres, ce qui donne une scène 3D coloriée. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Et ces algorithmes sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé. ===Les données nécessaires pour les algorithmes d'illumination=== L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage directionnel impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses. Le premier de ces paramètres est l''''intensité de la source de lumière''', à quel point elle émet de la lumière. Là encore, cette information est encodée par un simple nombre, un coefficient, spécifié par l'artiste lors de la création du niveau/scène 3D. La couleur de la source de lumière est une version améliorée de l'intensité de la source lumineuse, dans le sens où on a une intensité pour chaque composante RGB. Le second est un nombre attribué à chaque point de surface : le '''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 plusieurs coefficients de réflexion, pour chaque couleur : un pour la couleur ambiante, un pour la couleur diffuse, et un pour la couleur spéculaire. Les calculs de réflexion de la lumière demandent aussi de connaitre l'orientation de la surface. Pour gérer cette orientation, tout point de surface 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 point de surface (sommet ou triangle). [[File:Graphics lightmodel ptsource.png|centre|vignette|Normale de la surface.]] Ensuite, il faut aussi préciser l'orientation de la lumière, dans quel sens est orientée. La majorité des sources de lumière émettent de la lumière dans une direction bien précise. 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. Les sources de lumières habituelles, comme les lampes, émettent dans une direction bien précise, appelée la '''direction privilégiée'''. La direction privilégiée est un vecteur, notée v dans le schéma du dessous. Un autre vecteur important est celui de la ligne entre le point de surface considéré et la caméra (noté w dans le schéma ci-dessous). Il faut aussi tenir comme du vecteur qui trace la ligne entre la source de lumière et le point de surface (noté L dans le schéma ci-dessous). Enfin, il faut ajouter le vecteur qui correspond à la lumière réfléchie par la surface au niveau du point de surface. Ce vecteur, noté R et non-indiqué dans le schéma ci-dessous, se calcule à partir du vecteur L et de la normale. [[File:Graphics lightmodel spot.png|centre|vignette|Vecteurs utilisés dans le calcul de l'illumination, hors normale.]] La plupart de ces informations n'a pas à être calculée. C'est le cas de la normale de la surface ou du vecteur L qui sont connus une fois l'étape de transformation réalisée. Même chose pour le coefficient de réflexion ou de l'intensité de la lumière, qui sont connus dès la création du niveau ou de la scène 3D. Par contre, le reste doit être calculé à la volée par la carte graphique à partir de calculs vectoriels. ===Le calcul des couleurs par un algorithme d'illumination de Phong=== À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D.S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place. L'illumination ambiante d'un point de surface s'obtient à partir de la lumière ambiante, mais elle n'est pas strictement égale à celle-ci, c'est légèrement plus compliqué que ça. Deux objets de la même couleur, illuminés par la même lumière ambiante, ne donneront pas la même couleur. Un objet totalement rouge illuminé par une lumière ambiante donnera une couleur ambiante rouge, un objet vert donnera une lumière verte. Et sans même parler des couleurs, certains objets sont plus sombres à certains endroits et plus clairs à d'autres, ce qui fait que leurs différentes parties ne vont pas réagir de la même manière à la lumière ambiante. La couleur de chaque point de surface de l'objet lui-même est mémorisée dans le modèle 3D, ce qui fait que chaque point de surface se voit attribuer, en plus de ces trois coordonnées, une couleur ambiante qui indique comment celui-ci réagit à la lumière ambiante. La couleur ambiante finale d'un point de surface se calcule en multipliant la couleur ambiante de base par l'intensité de la lumière ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D. : <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. [[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]] L'illumination spéculaire et diffuse sont calculées autrement, à partir des vecteurs indiqués dans le schéma ci-contre. Tous les vecteurs sont définis à partir d'un point de la surface géométrique, d'un sommet, qui est éclairé. Les quatre vecteurs sont les suivants : * la normale N est un vecteur perpendiculaire à la surface, au sommet ; * le vecteur L qui connecte le sommet avec la source de lumière ; * le vecteur V qui connecte la caméra au sommet ; * le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes. Afin de simplifier les explications, il faut préciser deux choses. premièrement, le vecteur L a pour longueur l'intensité de la lumière émise par la source de lumière. Il s'agit d'un vecteur pointant vers la source lumineuse, c'est un choix arbitraire mais pas sans raison. Quant au vecteur pour la normale, sa longueur est le coefficient de réflexion diffuse <math>K_d</math>, qui indique à quel point la surface diffuse de la lumière, l'intensité de la source lumineuse. Il faut dire que ce coefficient est définit pour chaque point d'une surface, Le calcul implique une opération mathématique appelée le ''produit scalaire'', qui s'explique assez simplement. Elle prend deux vecteurs, que l'on notera A et B. Un produit scalaire prend la longueur des deux vecteurs et l'angle entre les deux vecteurs noté <math>\omega</math>. Le produit scalaire se calcule comme suit : : <math>\text{Produit scalaire} = A \times B \times \cos{(\omega)}</math> L'illumination diffuse est calculée en faisant le produit scalaire entre deux vecteurs : la normale de la surface et le vecteur L. L'angle entre N et L donne la manière dont la surface est penchée par rapport à la surface lumineuse : plus elle est penchée, plus la lumière est diffusée et moins l'illumination diffuse est intense. On rappelle que la longueur des deux vecteurs est repsectivement l'intensité de la lumière et le coefficient de réflexion. L'angle en question est noté <math>omega</math> dans l'équation suivante : : <math>\text{Illumination diffuse} = K_d \times I_a \times \cos{(\omega)} = K_d \times I_a \times (\vec{N} \cdot \vec{L})</math> 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, c'est à dire l'angle entre v et R, que nous noterons <math>\Omega</math>. Là encore, on doit utiliser le coefficient de réflexion spéculaire <math>K_s</math> de la surface et l'intensité de la lumière, ce qui donne : : <math>\text{Illumination spéculaire} = K_s \times I_a \times \cos{(\Omega)} = K_s \times I_a \times (\vec{R} \cdot \vec{v})</math> En additionnant ces trois sources d'illumination, on trouve : : <math>\text{Illumination ambiante} = K_a \times I_a + I_a \times \left[ K_d \times \cos{(\omega)} + K_s \times \cos{(\Omega)} \right]</math> ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment. [[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]] L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle. L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''. L''''éclairage de Phong''' est différent des précédents dans le sens où il calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée. [[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]] Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. {| |- |[[File:Flatshading01.png|vignette|upright=1|Flat shading]] |[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]] |[[File:Phongshading01.png|vignette|upright=1|Phong Shading]] |- |[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]] |- |[[File:Per face lighting.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]] |- |[[File:Per face lighting example.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]] |} L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres. ===Le bump-mapping et autres approximations de l'éclairage par pixel=== Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. [[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]] [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] ==Les ''shaders''== Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D. ===L'utilisation des ''shaders'' pour les algorithmes d'éclairage=== Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables. ===Les autres utilisations des ''shaders''=== Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables. Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Le rendu d'une scène 3D : l'API graphique | nextText=Le rendu d'une scène 3D : l'API graphique }}{{autocat}} pcs4zv97pa5zuky0kjl6ch3psjsrixi Discussion Wikilivres:Le Bistro/2026 5 83406 763044 762841 2026-04-06T16:19:11Z MediaWiki message delivery 36013 /* Actualités techniques n° 2026-15 */ nouvelle section 763044 wikitext text/x-wiki == Actualités techniques n° 2026-03 == <section begin="technews-2026-W03"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/03|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * La Fondation Wikimedia a publié des questions directrices pour son plan annuel de juillet 2026 à juin 2027 sur les plateformes [[m:Special:MyLanguage/Wikimedia Foundation Annual Plan/2026-2027/Product & Technology OKRs|Meta]] et ''[[diffblog:2025/12/10/shaping-wikimedia-foundations-2026-2027-annual-goals-key-questions-for-the-wikimedia-movement/|Diff]]''. Celles-ci portent sur les tendances mondiales, une expérimentation plus rapide et plus constructive, un meilleur accompagnement des nouveaux contributeurs, le renforcement du rôle des éditeurs et des utilisateurs avancés, l'amélioration de la collaboration entre les projets, ainsi que le développement et la fidélisation du lectorat. Des commentaires et suggestions sont les bienvenus sur la [[m:Talk:Wikimedia Foundation Annual Plan/2026-2027|page de discussion]]. '''Actualités pour la contribution''' * Dans le cadre des travaux en cours de l'équipe technique communautaire sur le projet [[m:Special:MyLanguage/Community Wishlist/W372|Listes de surveillance multiples]], l'affichage de [[Special:EditWatchlist|Modifier la liste de surveillance]] sera mis à jour entant que qu'une première étape vers la prise en charge de plusieurs listes de surveillance. De plus, la pagination de [[Special:Search|Recherche]] sera également mise à jour, dans le cadre du travail sur le souhait [[m:Special:MyLanguage/Community Wishlist/W186|Refonte de la pagination / navigation des pages]]. [https://phabricator.wikimedia.org/T411596] * [[m:Special:GlobalWatchlist|La Liste de Surveillance Globale]] est une [[mw:Special:MyLanguage/Extension:GlobalWatchlist|extension]] de MediaWiki qui vous permet de voir vos listes de surveillance provenant de différents wikis sur la même page. Il a récemment été mis à jour pour ressembler davantage à la [[Special:Watchlist|Liste de surveillance]] régulière, par exemple en le préparant pour les comptes temporaires dans le masquage IP (y compris le réacheminement des liens des utilisateurs vers les pages de contributions), en mettant les titres de page en gras et en ouvrant les liens dans les résumés d'édition et les balises dans de nouveaux onglets du navigateur. [https://phabricator.wikimedia.org/T398361][https://phabricator.wikimedia.org/T298919][https://phabricator.wikimedia.org/T273526][https://phabricator.wikimedia.org/T286309] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:28|la tâche soumise|les {{formatnum:28}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:28||s}} la semaine dernière]]. Par exemple, le problème selon lequel les blocs globaux ne disposaient pas de l'option permettant de désactiver l'envoi d'e-mails a maintenant été résolu et sera disponible à l'utilisation à partir de la semaine du 13 janvier. [https://phabricator.wikimedia.org/T401293] '''Actualités pour la contribution technique''' * L'[[mw:Special:MyLanguage/VisualEditor/Citation tool|outil de citation VisualEditor]] et les [[mw:Special:MyLanguage/Help:Reference Previews|Aperçus de référence]] prennent désormais en charge "carte" comme type de référence. [https://phabricator.wikimedia.org/T411083] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.10|MediaWiki]]/[[mw:MediaWiki 1.46/wmf.11|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/03|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W03"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 12 janvier 2026 à 20:33 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=29907192 --> == Thank You for Last Year – Join Wiki Loves Ramadan 2026 == Dear Wikimedia communities, We hope you are doing well, and we wish you a happy New Year. ''Last year, we captured light. This year, we’ll capture legacy.'' In 2025, communities around the world shared the glow of Ramadan nights and the warmth of collective iftars. In 2026, ''Wiki Loves Ramadan'' is expanding, bringing more stories, more cultures, and deeper global connections across Wikimedia projects. We invite you to explore the ''Wiki Loves Ramadan 2026'' [[m:Special:MyLanguage/Wiki Loves Ramadan 2026|Meta page]] to learn how you can participate and [[m:Special:MyLanguage/Wiki Loves Ramadan 2026/Participating communities|sign up]] your community. 📷 ''Photo campaign on '' [[c:Special:MyLanguage/Commons:Wiki Loves Ramadan 2026|Wikimedia Commons]] If you have questions about the project, please refer to the FAQs: * [[m:Special:MyLanguage/Wiki Loves Ramadan/FAQ/|Meta-Wiki]] * [[c:Special:MyLanguage/Commons:Wiki Loves Ramadan/FAQ|Wikimedia Commons]] ''Early registration for updates is now open via the '''[[m:Special:RegisterForEvent/2710|Event page]]''''' ''Stay connected and receive updates:'' * [https://t.me/WikiLovesRamadan Telegram channel] * [https://lists.wikimedia.org/postorius/lists/wikilovesramadan.lists.wikimedia.org/ Mailing list] We look forward to collaborating with you and your community. '''The Wiki Loves Ramadan 2026 Organizing Team''' 16 janvier 2026 à 20:44 (CET) <!-- Message envoyé par User:ZI Jony@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Non-Technical_Village_Pumps_distribution_list&oldid=29879549 --> == <span lang="en" dir="ltr">Tech News: 2026-04</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W04"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/04|Translations]] are available. '''Updates for editors''' * The tray shown on [[Special:Diff|Special:Diff]] in mobile view has been redesigned. It is now collapsed by default, and incorporates a link to undo the edit being viewed, making it easier for mobile editors and reviewers to take action while keeping the interface uncluttered. [https://phabricator.wikimedia.org/T402297] * [[m:Special:GlobalWatchlist|The Global Watchlist]] lets you view your watchlists from multiple wikis on one page. The [[mw:Special:MyLanguage/Extension:GlobalWatchlist|extension]] continues to improve — it now automatically determines the text direction (ensuring correct display of sites with unusual domain names) and shows detailed descriptions for log actions. Later this week, a new permanent link for page creations and CSS classes for each entry element will be added. [https://phabricator.wikimedia.org/T412505][https://phabricator.wikimedia.org/T287929][https://phabricator.wikimedia.org/T262768][https://phabricator.wikimedia.org/T414135] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:32}} community-submitted {{PLURAL:32|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, the previously observed issue in Vector 2022, where anchor link targets were obscured by the sticky header, has now been addressed. [https://phabricator.wikimedia.org/T406114] '''Updates for technical contributors''' * As mentioned in the [[m:Special:MyLanguage/Tech/News/2025/44|October 2025 deprecation announcement]], MediaWiki Interfaces team will begin sunsetting all transform endpoints containing a trailing slash from the MediaWiki REST API the week of January 26. Changes are expected to roll out to all wikis on or before January 30th. All API users currently calling them are encouraged to transition to the non-trailing slash versions. Both endpoint variations can be found, compared, and tested using the [https://test.wikipedia.org/wiki/Special:RestSandbox REST Sandbox]. If you have questions or encounter any problems, please file a ticket in Phabricator to the [https://phabricator.wikimedia.org/project/view/6931/ #MW-Interfaces-Team board]. * Interactive reference documentation for the [[mw:Special:MyLanguage/Wikimedia REST API|Wikimedia REST API]] has moved. Requests to API docs previously hosted through [[mw:Special:MyLanguage/RESTBase|RESTBase]] (e.g.: <code dir=ltr>https://en.wikipedia.org/api/rest_v1/</code>) are now redirected to the [[w:en:Special:RestSandbox|REST Sandbox]]. * The [[mw:Special:MyLanguage/Wikidata Platform|WMF Wikidata Platform team]] (WDP) has published its [[d:Special:MyLanguage/Wikidata:Wikidata Platform team/Newsletter|January 2026 newsletter]]. It includes updates on the legacy full-graph endpoint decommissioning, the User-Agent policy change, the monthly Blazegraph migration office hours, and efforts to reduce regressions caused by the legacy endpoint shutdown. As a reminder, you can [[m:Special:MyLanguage/Global message delivery/Targets/WDP team updates|subscribe to the WDP newsletter]]! * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.12|MediaWiki]] '''Meetings and events''' * The [[mw:Wikimedia Hackathon Northwestern Europe 2026|Wikimedia Hackathon Northwestern Europe 2026]] will take place on 13-14 March 2026 in Arnhem, the Netherlands. Applications opened mid-December and will close soon or when capacity is reached. It's a two-day, technically oriented hackathon bringing together Wikimedians from the region. Hope to see you there! '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/04|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W04"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 19 janvier 2026 à 21:29 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=29943403 --> == Révision annuelle du code universel de conduite et des lignes directrices de l'application == <section begin="announcement-content" /> Nous vous informons que la période de relecture annuelle du Code de conduite universel et des règles d'applications est actuellement ouverte. Vous pouvez faire vos commentaires sur les modifications que vous souhaitez apporter jusqu'au 9 février 2026. C'est la première d'une série d'étapes nécessaires pour la révision annuelle. Vous trouverez [[m:Special:MyLanguage/Universal Code of Conduct/Annual review/2026|d'autres informations et les discussions auxquelles participer sur la page UCoC de Meta]]. Le [[m:Special:MyLanguage/Universal Code of Conduct/Coordinating Committee|Comité de coordination du code universel de conduite]] (U4C &mdash; Universal Code of Conduct Coordinating Committee) est un groupe global dont le rôle est de fournir une implémentation équitable et cohérente de l'UCoC. Cette relecture annuelle a été envisagée et mise en place par l'U4C. Pour plus d'informations et les responsabilités de l'U4C, veuillez lire la [[m:Special:MyLanguage/Universal Code of Conduct/Coordinating Committee/Charter|Charte de l'U4C]]. Veuillez partager ces informations avec les autres membres concernés de votre communauté. -- En coopération avec l'U4C, [[m:User:Keegan (WMF)|Keegan (WMF)]] ([[m:User talk:Keegan (WMF)|discussion]])<section end="announcement-content" /> 19 janvier 2026 à 22:01 (CET) <!-- Message envoyé par User:Keegan (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Global_message_delivery&oldid=29905753 --> == Actualités techniques n° 2026-05 == <section begin="technews-2026-W05"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/05|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * La Fondation Wikimedia invite à donner des commentaires sur [[m:Special:MyLanguage/Product and Technology Advisory Council/Year1 Reflections and Proposed Way Forward 2026 Update|l’avenir proposé]] du [[:m:Special:MyLanguage/Product and Technology Advisory Council|Conseil consultatif des produits et technologies]] jusqu’au 28 février. * Tous les utilisateurs disposant d'un compte enregistré peuvent désormais utiliser des clés d'accès pour la [[m:Special:MyLanguage/Help:Two-factor authentication|double authentification]] (2FA). Les clés d'accès sont un moyen simple de se connecter sans utiliser un second appareil. Elles vérifient l'identité de l'utilisateur à l'aide d'une empreinte digitale, d'une reconnaissance faciale ou d'un code PIN. Pour configurer une clé d'accès, configurez d'abord une méthode 2FA classique. Actuellement, pour se connecter avec une clé d'accès, les utilisateurs doivent également utiliser un mot de passe. Plus tard ce trimestre, la connexion sans mot de passe permettra aux utilisateurs de se connecter d'un simple clic avec une clé d'accès. Les utilisateurs disposant de droits avancés devront également avoir la 2FA activée. Cela fait partie du projet [[mw:Special:MyLanguage/Product Safety and Integrity/Account Security|Sécurité du compte]]. * Les contributeurs non enregistrés sur des IP bloquées ou des plages d'IP bloquées peuvent désormais interagir sur le wiki pour faire appel d'un blocage en créant un compte temporaire afin de contester un blocage sur la page de discussion de l'utilisateur, sauf si l'option « empêcher cet utilisateur de modifier sa propre page de discussion » est activée. Cela résout le problème des utilisateurs déconnectés incapables d'utiliser le processus de déblocage par défaut via la page de discussion de l'utilisateur. [https://phabricator.wikimedia.org/T398673] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:20|la tâche soumise|les {{formatnum:20}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:20||s}} la semaine dernière]]. Par exemple, la description des méthodes d'authentification à deux facteurs (2FA) sur la page de gestion a été mise à jour. Il est désormais plus clair et plus facile pour les utilisateurs à comprendre et à utiliser. [https://phabricator.wikimedia.org/T332385] '''Actualités pour la contribution technique''' * Une nouvelle variable AbuseFilter, <code>account_type</code>, a été ajoutée pour fournir un moyen fiable de déterminer le type de compte créé dans les actions <code>createaccount</code> et <code>autocreateaccount</code>. Dans le cadre de ce changement, la variable <code>accountname</code> a été renommée en <code>account_name</code>, et <code>accountname</code> est désormais obsolète. Les gestionnaires de filtres doivent mettre à jour tous les filtres qui utilisent des vérifications de type de compte codées en dur ou la variable obsolète. [https://phabricator.wikimedia.org/T414049] * Les vignettes d'images demandées dans des tailles non standard, et en utilisant des méthodes non standard telles que les requêtes directes à <code dir=ltr><nowiki>upload.wikimedia.org/…</nowiki></code>, cesseront de fonctionner dans un proche avenir. Ce changement vise à prévenir les abus externes continus par des robots et des aspirateurs web. Certains utilisateurs ayant des CSS/JS personnalisés, les administrateurs d'interface qui peuvent corriger les gadgets et les thèmes locaux, ainsi que les auteurs d'outils, devront mettre à jour leur code pour utiliser des tailles de vignettes standard. [[phab:T414805|Des détails, des liens de recherche et des exemples de correction sont disponibles dans la tâche]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.13|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/05|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W05"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 26 janvier 2026 à 22:17 (CET) <!-- Message envoyé par User:UOzurumba (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=29969530 --> == <span lang="en" dir="ltr">Tech News: 2026-06</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W06"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/06|Translations]] are available. '''Updates for editors''' * The "{{int:pageinfo-toolboxlink}}" feature, which gives validating information about a page ([{{fullurl:{{FULLPAGENAME}}|action=info}} example]), now automatically includes a table of contents. If there is a local [[{{ns:8}}:Pageinfo-header]] page created by individual users, it can now be removed. [https://phabricator.wikimedia.org/T363726] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:21}} community-submitted {{PLURAL:21|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, VisualEditor previously added bold or italic formatting inside link descriptions, making the wikicode complex. This has now been fixed. [https://phabricator.wikimedia.org/T409669] '''Updates for technical contributors''' * There was no XML dump on 20 January. Additionally, from now on, dumps will be generated once per month only. [https://phabricator.wikimedia.org/T414389] * The MediaWiki Interfaces team removed support for all transform endpoints containing a trailing slash from the [https://www.mediawiki.org/wiki/Special:MyLanguage/API:REST%20API MediaWiki REST API]. All API users currently calling those endpoints are encouraged to transition to the non-trailing slash versions. If you have questions or encounter any problems, please file a ticket in phabricator to the [https://phabricator.wikimedia.org/project/view/6931/ #MW-Interfaces-Team board]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.14|MediaWiki]] '''Weekly highlight''' * Users are reminded that the Wikimedia Foundation has shared some guiding questions for the July 2026–June 2027 Annual Plan on [[m:Special:MyLanguage/Wikimedia Foundation Annual Plan/2026-2027/Product & Technology OKRs|Meta]] and ''[[diffblog:2025/12/10/shaping-wikimedia-foundations-2026-2027-annual-goals-key-questions-for-the-wikimedia-movement/|Diff]]''. These focus on global trends, faster and healthier experimentation, better support for newcomers, strengthening editors and advanced users, improving collaboration across projects, and growing and retaining readership. Feedback and ideas are welcome on the [[m:Talk:Wikimedia Foundation Annual Plan/2026-2027|talk page]]. '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/06|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W06"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 2 février 2026 à 18:43 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30000986 --> == Actualités techniques n° 2026-07 == <section begin="technews-2026-W07"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/07|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * [[File:Maki-gift-15.svg|12px|link=|class=skin-invert|Concerne un souhait]] Les contributeurs connectés qui gèrent de grandes ou complexes listes de suivi peuvent désormais organiser et filtrer les pages surveillées de manière à améliorer leurs flux de travail grâce à la nouvelle fonctionnalité [[mw:Special:MyLanguage/Help:Watchlist labels|Étiquettes de liste de suivi]]. En ajoutant des étiquettes personnalisées (par exemple : pages que vous avez créées, pages surveillées pour vandalisme, ou pages de discussion), les utilisateurs peuvent identifier plus rapidement ce qui nécessite une attention, réduire la charge cognitive et répondre plus efficacement. Cela améliore l'utilisabilité de la liste de suivi, en particulier pour les éditeurs très actifs. * Une nouvelle fonctionnalité disponible sur [[Special:Contributions|Special:Contributions]] montre [[mw:Special:MyLanguage/Trust and Safety Product/Temporary Accounts|des comptes temporaires]] qui sont probablement utilisés par la même personne, et rend ainsi le patrouillage moins chronophage. En vérifiant les contributions d'un compte temporaire, les utilisateurs ayant accès aux adresses IP des comptes temporaires peuvent désormais avoir une vue des contributions des comptes temporaires associés. La fonctionnalité recherche toutes les adresses IP associées à un compte temporaire donné pendant la période de conservation des données et affiche toutes les contributions de tous les comptes temporaires ayant utilisé ces adresses IP. [[mw:Special:MyLanguage/Trust and Safety Product/Temporary Accounts#February 2026: Improvements to the patroller tooling|Plus...]] [https://phabricator.wikimedia.org/T415674] * Lorsque les éditeurs prévisualisent une modification de wikitexte, la boîte de rappel indiquant qu'ils ne voient qu'une prévisualisation (qui est affichée en haut) a désormais un fond gris/neutre au lieu d'un fond jaune/d'avertissement. Cela facilite la distinction entre les notes de prévisualisation et les avertissements réels (par exemple, les conflits de modification ou les cibles de redirection problématiques), qui seront désormais affichés dans des boîtes d'avertissement ou d'erreur séparées. [https://phabricator.wikimedia.org/T414742] * La [[m:Special:GlobalWatchlist|Liste de suivi globale]] vous permet de consulter vos listes de suivi provenant de plusieurs wikis sur une seule page. L' [[mw:Special:MyLanguage/Extension:GlobalWatchlist|extension]] continue de s'améliorer — elle prend désormais en charge correctement plus d'un site Wikibase, par exemple à la fois [[d:|Wikidata]] et [[testwikidata:|testwikidata]]. De plus, des problèmes concernant la direction du texte ont été résolus pour les utilisateurs qui préfèrent Wikidata ou d'autres sites Wikibase dans des langues de droite à gauche (RTL). [https://phabricator.wikimedia.org/T415440][https://phabricator.wikimedia.org/T415458] * <span lang="en" dir="ltr" class="mw-content-ltr">The automatic "magic links" for ISBN, RFC, and PMID numbers have been [[mw:Special:MyLanguage/Help:Magic links|deprecated in wikitext since 2021]] due to inflexibility and difficulties with localization. Several wikis have successfully replaced RFC and PMID magic links with equivalent external links, but a template was often required to replace the functionality of the ISBN magic link. There is now a new [[mw:Special:MyLanguage/Help:Magic words#isbn|built-in parser function]] <code dir=ltr><nowiki>{{#isbn}}</nowiki></code> available to replace the basic functionality of the ISBN magic link. This makes it easier for wikis who wish to migrate off of the deprecated magic link functionality to do so.</span> [https://phabricator.wikimedia.org/T145604] * Deux nouveaux wikis ont été créés : ** un {{int:project-localized-name-group-wikipedia}} dans [[d:Q35401|Jju]] ([[w:kaj:|<code>w:kaj:</code>]]) [https://phabricator.wikimedia.org/T413283] ** un {{int:project-localized-name-group-wikipedia}} dans [[d:Q1186896|Nawat]] ([[w:ppl:|<code>w:ppl:</code>]]) [https://phabricator.wikimedia.org/T413273] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:23|la tâche soumise|les {{formatnum:23}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:23||s}} la semaine dernière]]. '''Actualités pour la contribution technique''' * Un nouveau groupe d'utilisateurs global a été créé : [[{{int:grouppage-local-bot}}|{{int:group-local-bot}}]]. Il sera utilisé en interne par le logiciel pour permettre aux robots communautaires de contourner les limites de débit appliquées aux [[w:en:Web_scraping|web scrapers]] abusifs. Les comptes approuvés en tant que robots sur au moins un wiki Wikimedia seront automatiquement ajoutés à ce groupe. Cela ne changera pas les autorisations dont dispose le robot. [https://phabricator.wikimedia.org/T415588] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.15|MediaWiki]] '''Rencontres et évènements''' * La [[mw:Special:MyLanguage/MediaWiki Users and Developers Conference Spring 2026|Conférence des utilisateurs et des développeurs de MediaWiki, Printemps 2026]] se tiendra du 25 au 27 mars à Salt Lake City, États-Unis. Cet événement est organisé par et pour la communauté MediaWiki de tiers. Vous pouvez proposer des sessions et vous inscrire pour y assister. [https://lists.wikimedia.org/hyperkitty/list/wikitech-l@lists.wikimedia.org/thread/AZBWVI46SDEB65PGR5J6E4TYOQQEZXM7/] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/07|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W07"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 10 février 2026 à 00:30 (CET) <!-- Message envoyé par User:Quiddity (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30026671 --> == Actualités techniques n° 2026-08 == <section begin="technews-2026-W08"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/08|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * <span class="mw-translate-fuzzy">L'[[mw:Special:MyLanguage/Wikimedia Site Reliability Engineering|équipe SRE]] va procéder au nettoyage d'[[m:Special:MyLanguage/Etherpad|Etherpad]], l'éditeur web open source de documents collaboratifs en temps réel. Tous les blocs-notes seront définitivement supprimés après le 30 avril 2026 – si des projets de migration sont encore en cours à cette date, l'équipe pourra réexaminer la date au cas par cas. Veuillez effectuer des sauvegardes locales de tout contenu que vous souhaitez conserver, car les données supprimées ne pourront pas être récupérées. Ce nettoyage permet de réduire la taille de la base de données et l'empreinte de l'infrastructure. Etherpad continuera de prendre en charge la collaboration en temps réel, mais le stockage à long terme n'est plus assuré. D'autres nettoyages pourront avoir lieu ultérieurement sans préavis.</span> [https://phabricator.wikimedia.org/T415237] '''Actualités pour la contribution''' * L'équipe de Recherche d'Informations lancera une [[mw:Special:MyLanguage/Readers/Information Retrieval/Phase 1|expérimentation sur l'application mobile Android]], afin de tester des fonctionnalités de recherche hybrides capables de gérer à la fois les requêtes sémantiques et par mots-clés. L'amélioration de la recherche sur la plateforme permettra aux lecteurs de trouver plus facilement ce qu'ils cherchent, directement sur Wikipédia. L'expérimentation sera d'abord lancée sur Wikipédia en grec fin février, puis sur les versions anglaise, française et portugaise en mars. [https://diff.wikimedia.org/2026/01/08/semantic-search-making-it-easier-to-find-the-information-readers-want/ En savoir plus] sur le blog ''Diff''. [https://www.mediawiki.org/wiki/Readers/Information_Retrieval] * L'équipe « Croissance des lecteurs » mènera [[mw:Special:MyLanguage/Readers/Reader Growth/WE3.10.2 Mobile Table of Contents|une expérience]] auprès des utilisateurs de la version mobile du site web qui ajoute une table des matières et développe automatiquement toutes les sections des articles, afin de mieux comprendre les problèmes de navigation qu'ils rencontrent. Le test sera disponible sur les versions arabe, chinoise, anglaise, française, indonésienne et vietnamienne de Wikipedia. * Auparavant, les notifications ([[{{ns:8}}:Sitenotice]] et [[{{ns:8}}:Anonnotice]]) du site ne s'affichaient que sur la version ordinateur. Maintenant, elles s'afficheront désormais sur toutes les plateformes. Les utilisateurs mobiles verront ces notifications. Les administrateurs du site doivent être prêts à tester et à corriger les notifications sur les appareils mobiles afin d'éviter toute interférence avec les articles. Pour désactiver ces notifications, les administrateurs d'interface peuvent ajouter <code dir="ltr">#siteNotice { display: none; }</code> à [[{{ns:8}}:Minerva.css]]. [https://phabricator.wikimedia.org/T138572][https://phabricator.wikimedia.org/T416644] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:19|la tâche soumise|les {{formatnum:19}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:19||s}} la semaine dernière]]. Par exemple, un problème concernant la section ''[[Special:RecentChanges|Spécial:Modifications récentes]]'' a été résolu. Auparavant, cliquer sur « Masquer » dans les filtres actifs entraînait la disparition du bouton « Afficher les nouvelles modifications depuis… », alors qu'il aurait dû rester visible. Ce bouton fonctionne désormais correctement. [https://phabricator.wikimedia.org/T406339] '''Actualités pour la contribution technique''' * Une nouvelle documentation est désormais disponible pour aider les rédacteurs à déboguer les fonctionnalités de recherche interne. Elle facilite le dépannage lorsque des pages n'apparaissent pas dans les résultats, lorsque le classement semble inattendu et lorsqu'il est nécessaire d'inspecter le contenu indexé, ce qui permet de mieux comprendre et d'analyser le comportement de la recherche. [[mw:Help:CirrusSearch/Debug|En savoir plus]]. [https://phabricator.wikimedia.org/T411169] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.16|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/08|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W08"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 16 février 2026 à 20:17 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30086330 --> == <span lang="en" dir="ltr">Tech News: 2026-09</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W09"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/09|Translations]] are available. '''Weekly highlight''' * [[mw:Special:MyLanguage/Edit check/Reference Check|Reference Check]] has been deployed to English Wikipedia, completing its rollout across all Wikipedias. The feature prompts newcomers to add a citation before publishing new content, helping reduce common citation-related reverts and improve verifiability. In A/B testing, the impact was substantial: newcomers shown Reference Check were approximately 2.2 times more likely to include a reference on desktop and about 17.5 times more likely on mobile web. [https://analytics.wikimedia.org/published/reports/editing/reference_check_ab_test_report_final_2025.html] '''Updates for editors''' * The [[mw:Special:MyLanguage/Extension:InterwikiSorting|InterwikiSorting extension]], which allowed for the [[m:Special:MyLanguage/Interwiki sorting order|sorting of interwiki links]], has been undeployed from Wikipedia. As a result, editors who had enabled interwiki link sorting in non-compact mode (full list format) will now see links reordered. The links moving forward will be listed in the alphabetical order of language code. [https://phabricator.wikimedia.org/T253764] * Later this week, people who are editing a page-section using the mobile visual editor, will notice a new "Edit full page" button. When tapped, you will be able to edit the entire article. This helps when the change you want to make is outside the section you initially opened. [https://phabricator.wikimedia.org/T387175][https://phabricator.wikimedia.org/T409112] * [[mw:Special:MyLanguage/Readers/Reader Experience|The Reader Experience team]] is inviting editors to assess whether dark mode should still be considered "beta" on their wiki, based on their experience of how well it functions on desktop and mobile. If the feature is deemed mature, editors can update the interface messages in <code dir=ltr>MediaWiki:skin-theme-description</code> and <code dir=ltr>MediaWiki:Vector-night-mode-beta-tag</code> to indicate that dark mode is ready and no longer considered beta. * The improved [[mw:Wikimedia_Apps/Team/iOS/Activity_Tab|Activity tab]] which displays user-insights is now available to all users of the Wikipedia iOS app (version 7.9.0 and later). Following earlier A/B testing that showed higher account creation among users with access to the feature, it has been rolled out to 100% of users along with some updates. The Activity tab now shows your edited articles in the timeline, offers editing impact insights like contribution counts and article view trends, and customization options to improve in-app experience for users. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:21}} community-submitted {{PLURAL:21|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, a bug that prevented [[mw:Special:MyLanguage/Extension:DiscussionTools|DiscussionTools]] from working on mobile has now been fixed, restoring full functionality. [https://phabricator.wikimedia.org/T415303] '''Updates for technical contributors''' * The [[m:Special:GlobalWatchlist|Global Watchlist]] lets you view your watchlists from multiple wikis on one page. The [[mw:Special:MyLanguage/Extension:GlobalWatchlist|extension]] that makes this possible continues to improve. The latest upgrade is the inclusion of a [[mw:Extension:GlobalWatchlist#hook|new hook]], <code dir=ltr>ext.globalwatchlist.rebuild</code>, which fires after each watchlist rebuild. This allows you to run gadgets and user scripts for the Special page. [https://phabricator.wikimedia.org/T275159] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.17|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/09|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W09"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 23 février 2026 à 20:03 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30119102 --> == Actualités techniques n° 2026-10 == <section begin="technews-2026-W10"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/10|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * Le [[m:Special:MyLanguage/Wikipedia 25/Easter egg experiments|mode Anniversaire]] Wikipedia 25 est maintenant disponible sur Wikipédia en français, anglais, betawi, breton, chinois, espagnol, gorontalo, indonésien, italien, luxembourgeois, madurais, néerlandais, sicilien, tchèque, thaï et vietnamien ! Cette campagne à temps limitée célèbre 25 ans de Wikipédia avec une mascotte : « Baby Globe », disponible sous la forme d'un réglage. Lorsque ce réglage est activé, Baby Globe est montrée sur [[m:Special:MyLanguage/Wikipedia 25/Easter egg experiments/article configuration|environ 2 500 articles]], attendant d'être découverte par des lecteurs. Chaque communauté peut choisir d'activer le mode Anniversaire par consensus et en demandant à un administrateur de le rendre disponible et de le personaliser via une [[m:Special:MyLanguage/Wikipedia 25/Easter egg experiments#Community Configuration Demo|configuration]] sur le wiki local. '''Actualités pour la contribution''' * Le [[:m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing|sous-référencement]], une nouvelle fonctionalité pour réutiliser des références avec des détails différents est maintenant disponible sur Wikipédia en suédois, polonais et [[:phab:T418209|quelques autres]]. Vous pouvez [[:m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing#test|essayer la fonctionalité]] sur ces projets ou sur testwiki et [https://en.wikipedia.beta.wmcloud.org/wiki/Sub-referencing betawiki]. Les retours des premiers essais sur Wikipédia en allemand ont été [[:m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing/Learnings|publiés dans un rapport]]. Contactez l'équipe de Wikimédia Allemagne si vous êtes [[:m:Talk:WMDE Technical Wishes/Sub-referencing#Pilot wikis|intéressés pour devenir un wiki pilote]]. * La [[mw:Special:MyLanguage/Help:Edit check#Paste check|vérification du collage clavier]] sera disponible sur tous les Wikipédias cette semaine. Cette fonctionalité avertit les nouveaux contributeurs qui collent du texte qu'ils n'ont probablement pas écrit de vérifier si laisser celui-ci risque de causer une violation du droit d'auteur. La vérification du collage clavier [[mw:Special:MyLanguage/Edit check/Tags|marque]] toutes les modifications où l'avertissement a été montré pour permettre leur vérification. Les administrateurs locaux peuvent configurer les différents aspects de cette fonctionalité à travers [[{{#special:EditChecks}}]]. Des [[mw:Special:MyLanguage/Edit check/Paste Check#A/B Experiment|études]] sur 22 wikis ont montré que cette vérification permet une réduction de 18% des annulations comparé au groupe de contrôle. Les traducteurs peuvent [https://translatewiki.net/w/i.php?title=Special%3ATranslate&group=ext-visualeditor-ve-mw-editcheck&filter=&optional=1&action=translate aider à traduire] cette fonctionalité. * <span lang="en" dir="ltr" class="mw-content-ltr">The [[mw:Special:MyLanguage/Readers/Reader Experience|Reader Experience team]] will be standardizing the user menu in the top right for all mobile users so that it is closer to the desktop experience. Currently this user menu is only visible to users with Advanced Mobile Controls (AMC) turned on. The only change is that a couple buttons previously in the left-side menu will move to the top right for users who do not have AMC turned on. This change is expected to go out March 9 and seeks to improve the user interface.</span> [https://phabricator.wikimedia.org/T413912] * À partir de la semaine du 2 mars, les emails envoyés lorsqu'une adresse email a été ajoutée, supprimée ou changée pour un compte changera pour adopter un formattage HTML beaucoup plus agréable et plus clair que le texte brut précédent. [https://phabricator.wikimedia.org/T410807] * Les notifications sont actuellement limitées à 2 000 entrées historiques par utilisateur et remontent à 2013 lorsque la fonctionnalité a été publiée. Le système va être modifié pour ne stocker que les notifications des 5 dernières années, mais jusqu'à 10 000 d'entre elles. Cela contribuera à la santé à long terme des infrastructures et à empêcher que les notifications plus récentes disparaissent trop tôt. [https://phabricator.wikimedia.org/T383948] * <span lang="en" dir="ltr" class="mw-content-ltr">The [[m:Special:GlobalWatchlist|Global Watchlist]] which lets you view your watchlists from multiple wikis on a single page continues to see improvements. The latest update improves label usage experience. The [[mw:Special:MyLanguage/Extension:GlobalWatchlist|extension]] now allows activating the [[mw:Special:MyLanguage/Manual:Language#Fallback languages|language fallback system]] for Wikidata items without labels in the viewed language, and showing those labels in the user’s preferred Wikidata language if no <code dir=ltr>uselang=</code> URL parameter is provided.</span> [https://phabricator.wikimedia.org/T373686][https://phabricator.wikimedia.org/T416111] * L'équipe Wikipédia Android a commencé un test beta de la [[mw:Special:MyLanguage/Readers/Information Retrieval/Phase 1|recherche hybride]] sur Wikipédia en grec. Cette recherche hybride supporte les requêtes sémantique et par mot clés, permettant aux utilisateurs de trouver ce qu'ils cherchent plus facilement. * Pour des raisons de sécurité, les membres de certains groupes sont [[m:Special:MyLanguage/Mandatory two-factor authentication for users with some extended rights|forcés d'avoir la double authentification]] (A2F) d'activée. Actuellement, l'A2F n'est nécessaire que pour utiliser les droits du groupe, et non pour en faire partie. Vu que ce système admet certaines failles, il sera [[phab:T418580|changé graduellement en mars]]. Les membres de ces groupes ne pourront plus désactiver la dernière méthose d'A2F sur leur compte, et il sera impossible d'ajouter des utilisateurs sans A2F à ces groupes. Il sera toujours possible de rajouter d'autres méthodes d'authentification et d'en enlever, tant qu'une est toujours activée. Dans la seconde moitié de mars, les utilisateurs sans A2F seront retirés de ces groupes. Cela s'applique aux administrateurs CentralNotice, aux vérificateurs d'utilisateurs, aux administrateurs d'interface, aux masqueurs, aux staff de Wikidata et Wikifonctions ainsi qu'aux bureaux IT et Confiance et sécurité de la WMF. Rien ne changera pour les autres utilisateurs. Voir la tâche liée pour le calendrier de déploiement. [https://phabricator.wikimedia.org/T418580] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:27|la tâche soumise|les {{formatnum:27}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:27||s}} la semaine dernière]]. Par exemple, le problème empêchant les utilisateurs de créer une instance dans [https://www.wikibase.cloud/ Wikibase.cloud] a maintenant été résolu. [https://phabricator.wikimedia.org/T416807] '''Actualités pour la contribution technique''' * <span lang="en" dir="ltr" class="mw-content-ltr">To help ensure [[mw:Special:MyLanguage/MediaWiki Product Insights/Responsible Reuse|fair use of infrastructure]], over the next month the Wikimedia Foundation will implement global API rate limits across our APIs. In early March, stricter limits will be applied to unidentified requests from outside Toolforge/WMCS and API requests that are made from web browsers. In April, higher limits will be applied to identified traffic. These limits are intentionally set as high as possible to minimise impact on the community. Bots running in Toolforge/WMCS or with the bot user right on any wiki should not be affected for now. However, all developers are advised to follow updated best practices. For more information, see [[mw:Special:MyLanguage/Wikimedia APIs/Rate limits|Wikimedia APIs/Rate limits]].</span> * <span lang="en" dir="ltr" class="mw-content-ltr">The Wikidata Query Service Linked Data Fragment (LDF) endpoint will be decommissioned in February. This endpoint served limited traffic, which was successfully migrated to other data access methods that were better suited to support existing use cases. The hardware used to support the LDF endpoint will be reallocated to support the ongoing backend migration efforts.</span> [https://phabricator.wikimedia.org/T415696] * Le nouvel analyseur syntaxique Parsoid [[mw:Special:MyLanguage/Parsoid/Parser Unification/Updates|continue d'être déployés sur plus de wikis]], améliorant la pérennité de la platforme et rendant plus facile l'ajout de nouvelles fonctionalités de lecture et de modification. Parsoid est maintenant l'analyseur par défaut sur 488 wikis de la WMF (268 Wikipédias), couvrant plus de 10% de toutes les lectures de pages Wikipédia. * Le processus et les critères pour [[Special:MyLanguage/Wikimedia Enterprise#Access|demander un accès exceptionnel]] au flux à fort volume de l'API ''Wikimédia Entreprise'' (sans coût pour des utilisations en rapport à notre mission) [[m:Talk:Wikimedia Enterprise#Exceptional access criteria|ont maintenant été publiés]]. Notre but est de donner une documentation plus claire et plus complète aux utilisateurs. * [https://techblog.wikimedia.org/ Le blog Tech], dédié à la communité technique de Wikimédia [https://techblog.wikimedia.org/2026/02/24/a-tech-blog-diff/ va migrer] vers [[diffblog:|Diff]], le blog pour les nouvelles et événements de la communauté. La migration devrait être terminée en Avril 2026, après quoi les nouveaux posts seront acceptés pour être publiés. Les lecteurs pourront lire les posts - anciens ou nouveaux - sur https://diff.wikimedia.org/. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.18|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/10|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W10"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 2 mars 2026 à 18:51 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30137798 --> == <span lang="en" dir="ltr">Tech News: 2026-11</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W11"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/11|Translations]] are available. '''Weekly highlight''' * [[m:Special:MyLanguage/Tech/Server switch|All wikis will be read-only]] for a few minutes on Wednesday, 25 March 2026 at [https://zonestamp.toolforge.org/1774450800 15:00 UTC]. This is for the datacenter server switchover backup tests, [[wikitech:Deployments/Yearly calendar|which happen twice a year]]. During the switchover, all Wikimedia website traffic is shifted from one primary data center to the backup data center to test availability and prevent service disruption even in emergencies. * Last week, all wikis had 2 hours of read-only time, and extended unavailability for user-scripts and gadgets. This was due to a security incident which has since been resolved. Work is ongoing to prevent re-occurrences. For current information please see the [[m:Steward's noticeboard#Statement on Meta about today's user script security incident|post on the Stewards' noticeboard]] ([[m:Special:MyLanguage/Wikimedia Foundation/Product and Technology/Product Safety and Integrity/March 2026 User Script Incident|translations]]). '''Updates for editors''' * Users facing multiple blocks on mobile will now see the reasons for each block separately, instead of a generic message. This helps them understand why they are blocked and what steps they can take to resolve the issue. For example, users affected for using common VPNs (such as [[Special:MyLanguage/Apple iCloud Private Relay|iCloud Private Relay]]) will receive clearer guidance on what they need to do to start editing again. [https://phabricator.wikimedia.org/T357118] * Later this week, [[mw:Special:MyLanguage/VisualEditor/Suggestion Mode|Suggestion Mode]] will become available as a beta feature within the visual editor at all Wikipedias. This feature proactively suggests various types of actions that people can consider taking to improve Wikipedia articles, and learn about related guidelines. The feature is locally configurable, and can also be locally expanded with custom Suggestions. Current settings can be seen at [[Special:EditChecks]] and there are [[mw:Special:MyLanguage/Help:Suggestion mode#For administrators %E2%80%93 local customization|instructions for how administrators can customize]] the links to point to local guidelines. The feature is connected to [[mw:Special:MyLanguage/Help:Edit check|Edit check]] which suggests improvements while someone is writing new content. In the future, the Editing team plans to evaluate the feature's impact with newcomers through a controlled experiment. [https://phabricator.wikimedia.org/T404600] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:23}} community-submitted {{PLURAL:23|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, the issue where the cursor became misaligned during the use of CodeMirror’s syntax highlighting, which makes wikitext and code easier to read, has now been fixed. This problem specifically affected users who defined a font rule in a custom stylesheet while creating a new topic with DiscussionTools. [https://phabricator.wikimedia.org/T418793] '''Updates for technical contributors''' * API rate limiting update: To help ensure [[mw:Special:MyLanguage/MediaWiki Product Insights/Responsible Reuse|fair use of infrastructure]], global API rate limits will be applied this week to requests without a compliant User-Agent that originate from outside Toolforge/WMCS and to unauthenticated requests made from web browsers. Higher limits will be applied to identified traffic in April. Bots running in Toolforge/WMCS or with the bot user right on any wiki should not be affected for now. However, all developers are advised to follow updated best practices. For more information, see [[mw:Special:MyLanguage/Wikimedia APIs/Rate limits|Wikimedia APIs/Rate limits]]. * The new GraphQL API has been released. The API was developed as a flexible alternative to select features of the Wikidata Query Service (WDQS), to improve developer experience and foster adaptability, and efficient data access. Try it out and [[d:Wikidata:Wikibase GraphQL#Feedback and development|give feedback]]. You can also [https://greatquestion.co/wikimediadeutschland/GraphQLAPI/apply sign up for usability tests]. * The [[m:Special:MyLanguage/Product and Technology Advisory Council/Unsupported Tools Working Group|PTAC Unsupported Tools Working Group]] continued improvements to [[commons:Special:MyLanguage/Commons:Video2commons#|Video2Commons]] in February, with fixes addressing authentication errors, large-file handling, task queue visibility, and clearer upload behavior. Work is still ongoing in some areas, including changes related to deprecated server-side uploads. Read [[m:Special:MyLanguage/Product and Technology Advisory Council/Unsupported Tools Working Group#February 2026|this update]] to learn more. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.19|MediaWiki]] '''In depth''' * The Article Guidance team invites experienced Wikipedia editors from selected [[mw:Special:MyLanguage/Article guidance/Pilot wikis and collaborators#Collaborators|pilot wikis]] and interested contributors from other Wikipedias to fill out this questionnaire which is available in [https://docs.google.com/forms/d/e/1FAIpQLSfmLeVWnxmsCbPoI_UF2jyRcn73WRGWCVPHzerXb4Cz97X_Ag/viewform English], [https://docs.google.com/forms/d/e/1FAIpQLSd6rzr4XXQw8r4024fE3geTPFe13M_6w7Mitj-YJi0sOlWTAw/viewform?usp=header Arabic], [https://docs.google.com/forms/d/e/1FAIpQLSdok3-RfB18lcugYTUMGkpwmqG_8p760Wv4dCXitOXOszjUDw/viewform?usp=header Bengali], [https://docs.google.com/forms/d/e/1FAIpQLSfjTfYp4jEo0akA4B1e-Nfg3QZPCudUjhJzHzzDi6AHyAaMGA/viewform?usp=header Japanese], [https://docs.google.com/forms/d/e/1FAIpQLScteVoI29Aue4xc72dekk-6RYtvmMgQxzMI900UOawrFrSTWg/viewform?usp=header Portuguese], [https://docs.google.com/forms/d/e/1FAIpQLSetdxnYwL3ub2vqA7awCg5hJZPMIYcDPaiTe12rY9h0GYnVlw/viewform?usp=header Persian], and [https://docs.google.com/forms/d/e/1FAIpQLScNvfJF-Ot-4pzA4qAN771_0QDJ4Li19YcUsaTgSKW8Nc7U_Q/viewform?usp=header Turkish]. Your answers will help the team customize guidance for less experienced editors and help them learn community policies and practices while creating an article. Learn more [[mw:Special:MyLanguage/Article guidance|on the project page]]. '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/11|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W11"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 9 mars 2026 à 19:52 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30213008 --> == <span lang="en" dir="ltr">Tech News: 2026-12</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W12"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/12|Translations]] are available. '''Updates for editors''' * The [[mw:Special:MyLanguage/Help:Extension:CodeMirror|{{int:codemirror-beta-feature-title}}]] beta feature, also known as [[mw:Special:MyLanguage/Extension:CodeMirror|CodeMirror 6]], has been used for wikitext syntax highlighting since November 2024. It will be promoted out of beta by May 2026 in order to bring improvements and new [[mw:Special:MyLanguage/Help:Extension:CodeMirror#Features|features]] to all editors who use the standard syntax highlighter. If you have any questions or concerns about promoting the feature out of beta, [[mw:Special:MyLanguage/Help talk:Extension:CodeMirror|please share]]. [https://phabricator.wikimedia.org/T259059] * Some changes to local user groups are performed by stewards on Meta-Wiki and logged there only. Now, interwiki rights changes will be logged both on Meta-Wiki and the wiki of the target user to make it easier to access a full record of user's rights changes on a local wiki. Past log entries for such changes will be backfilled in the coming weeks. [https://phabricator.wikimedia.org/T6055] * On wikis using [[m:Special:MyLanguage/Flagged Revisions|Flagged Revisions]], the number of pending changes shown on [[{{#Special:PendingChanges}}]] previously counted pages which were no longer pending review, because they have been removed from the system without being reviewed, e.g. due to being deleted, moved to a different namespace, or due to wiki configuration changes. The count will be correct now. On some wikis the number shown will be much smaller than before. There should be no change to the list of pages itself. [https://phabricator.wikimedia.org/T413016] * Wikifunctions composition language has been rewritten, resulting in a new version of the language. This change aims to increase service stability by reducing the orchestrator's memory consumption. This rewrite also enables substantial latency reduction, code simplification, and better abstractions, which will open the door to later feature additions. Read more about [[f:Special:MyLanguage/Wikifunctions:Status updates/2026-03-11|the changes]]. * Users can now sort search results alphabetically by page title. The update gives an additional option to finding pages more easily and quickly. Previously, results could be sorted by Edit date, Creation date, or Relevance. To use the new option, open 'Advanced Search' on the search results page and select 'Alphabetically' under 'Sorting Order'. [https://phabricator.wikimedia.org/T403775] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:28}} community-submitted {{PLURAL:28|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, the bug that prevented UploadWizard on Wikimedia Commons from importing files from Flickr has now been fixed. [https://phabricator.wikimedia.org/T419263] '''Updates for technical contributors''' * A new special page, [[{{#special:LintTemplateErrors}}]], has been created to list transcluded pages that are flagged as containing lint errors to help users discover them easily. The list is sorted by the number of transclusions with errors. For example: [[{{#special:LintTemplateErrors}}/night-mode-unaware-background-color]]. [https://phabricator.wikimedia.org/T170874] * Users of the [[mw:Special:MyLanguage/Help:Extension:CodeMirror|{{int:codemirror-beta-feature-title}}]] beta feature have been using [[mw:Special:MyLanguage/Extension:CodeMirror|CodeMirror]] instead of [[mw:Special:MyLanguage/Extension:CodeEditor|CodeEditor]] for syntax highlighting when editing JavaScript, CSS, JSON, Vue and Lua content pages, for some time now. Along with promoting CodeMirror 6 out of beta, the plan is to replace CodeEditor as the standard editor for these content models by May 2026. [[mw:Special:MyLanguage/Help talk:Extension:CodeMirror|Feedback or concerns are welcome]]. [https://phabricator.wikimedia.org/T419332] * The [[mw:Special:MyLanguage/Extension:CodeMirror|CodeMirror]] JavaScript modules will soon be upgraded to CodeMirror 6. Leading up to the upgrade, loading the <code dir=ltr>ext.CodeMirror</code> or <code dir=ltr>ext.CodeMirror.lib</code> modules from gadgets and user scripts was deprecated in July 2025. The use of the <code dir=ltr>ext.CodeMirror.switch</code> hook was also deprecated in March 2025. Contributors can now make their scripts or gadgets compatible with CodeMirror 6. See the [[mw:Special:MyLanguage/Extension:CodeMirror#Gadgets and user scripts|migration guide]] for more information. [https://phabricator.wikimedia.org/T373720] * The MediaWiki Interfaces team is expanding coverage of REST API module definitions to include [[mw:Special:MyLanguage/API:REST API/Extensions|extension APIs]]. REST API modules are groups of related endpoints that can be independently managed and versioned. Modules now exist for [https://phabricator.wikimedia.org/T414470 GrowthExperiments] and [https://phabricator.wikimedia.org/T419053 Wikifunctions] APIs. As we migrate extension APIs to this structure, documentation will move out of the main MediaWiki OpenAPI spec and REST Sandbox view, and will instead be accessible via module-specific options in the dropdown on the [https://test.wikipedia.org/wiki/Special:RestSandbox REST Sandbox] (i.e., [[{{#Special:RestSandbox}}]], available on all wiki projects). * The [[mw:Special:MyLanguage/Extension:Scribunto|Scribunto]] extension provides different pieces of information about the wiki where the module is being used via the [[mw:Special:MyLanguage/Extension:Scribunto/Lua reference manual|mw.site]] library. Starting last week, the library also provides a [[mw:Special:MyLanguage/Extension:Scribunto/Lua reference manual#mw.site.wikiId|way]] of accessing the [[mw:Special:MyLanguage/Manual:Wiki ID|wiki ID]] that can be used to facilitate cross-wiki module maintenance. [https://phabricator.wikimedia.org/T146616] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.20|MediaWiki]] '''In depth''' * The [[m:Special:MyLanguage/Coolest Tool Award|2026 Coolest Tool Award]] celebrating outstanding community tools, is now open for nominations! Nominate your favorite tool using the [https://wikimediafoundation.limesurvey.net/435684?lang=en nomination survey] form by 23 March 2026. For more information on privacy and data handling, please see the [[foundation:Special:MyLanguage/Legal:Coolest_Tool_Award_2026_Survey_Privacy_Statement|survey privacy statement]]. '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/12|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W12"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 16 mars 2026 à 20:35 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30260505 --> == <span lang="en" dir="ltr">Upcoming deployment of CampaignEvents extension to Wikibooks</span> == <div lang="en" dir="ltr"> <section begin="message"/> Hello everyone, We are writing to inform you that the [[mw:Help:Extension:CampaignEvents|CampaignEvents extension]] will be deployed to all Wikibooks projects during the week of '''23 March 2026'''. This follows last year’s broader rollout across Wikimedia projects. We realized that Wikibooks was not included at the time, and we’re now addressing that to ensure consistency across all communities. The CampaignEvents extension provides tools to support event and campaign organization on-wiki, including features like on-wiki event registration and collaboration lists(global event list). We welcome any questions, feedback, or concerns you may have. We are also happy to support anyone interested in trying out the tools. ''Apologies if this message is not in your preferred language. If you’re able to help translate it for your community, please feel free to do so.'' <section end="message"/> </div> <bdi lang="en" dir="ltr">[[User:Udehb-WMF|Udehb-WMF]] ([[User talk:Udehb-WMF|discussion]]) 19 mars 2026 à 19:22 (CET)</bdi> <!-- Message envoyé par User:Udehb-WMF@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=User:Udehb-WMF/sandbox/MM_target&oldid=30284073 --> == <span lang="en" dir="ltr">Tech News: 2026-13</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W13"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/13|Translations]] are available. '''Weekly highlight''' * Wikimedia site users can now log in without a password using passkeys. This is a secure method supported by fingerprint, facial recognition, or PIN. With this change, all users who opt for passwordless login will find it easier, faster, and more secure to log in to their accounts using any device. The new passkey login option currently appears as an autofill suggestion in the username field. An additional [[phab:T417120|"Log in with passkey" button]] will soon be available for users who have already registered a passkey. This update will improve security and user experience. The [[c:File:Passwordless_login_screencast.webm|screen recording]] demonstrates the passwordless login process step by step. * [[m:Special:MyLanguage/Tech/Server switch|All wikis will be read-only]] for a few minutes on Wednesday, 25 March 2026 at [https://zonestamp.toolforge.org/1774450800 15:00 UTC]. This is for the datacenter server switchover backup tests, [[wikitech:Deployments/Yearly calendar|which happen twice a year]]. During the switchover, all Wikimedia website traffic is shifted from one primary data center to the backup data center to test availability and prevent service disruption even in emergencies. '''Updates for editors''' * Wikimedia site users can now export their notifications older than 5 years using a [[toolforge:echo-chamber|new Toolforge tool]]. This will ensure that users retain their important notifications and avoid them being lost based on the planned change to delete notifications older than 5 years, as previously announced. [https://phabricator.wikimedia.org/T383948] * Wikipedia editors in Indonesian, Thai, Turkish, and Simple English now have access to Special:PersonalDashboard. This is an [[mw:Special:MyLanguage/Moderator Tools/Dashboard|early version of an experience]] that introduces newer editors to patrolling workflows, making it easier for them to move from making edits to participating in more advanced moderation work on their project. [https://phabricator.wikimedia.org/T402647] * The [[Special:Block]] now has two minor interface changes. Administrators can now easily perform indefinite blocks through a dedicated radio button in the expiry section. Also, choosing an indefinite expiry provides a different set of common reasons to select from, which can be changed at: [[MediaWiki:Ipbreason-indef-dropdown]]. [https://phabricator.wikimedia.org/T401823] * Mobile editors [[mw:Special:MyLanguage/Contributors/Account Creation Experiments#Logged-out|at several wikis]] can now see an improved logged-out edit warning, thanks to the recent updates from the Growth team. These changes released last week are part of ongoing efforts and tests to enhance [[mw:Special:MyLanguage/Contributors/Account Creation Experiments|account creation experience on mobile]] and then increase participation. [https://phabricator.wikimedia.org/T408484] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:36}} community-submitted {{PLURAL:36|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, the bug that prevented mobile web users from seeing the block information when affected by multiple blocks has been fixed. They can now see messages of all the blocks currently affecting them when they access Wikipedia. '''Updates for technical contributors''' * Images built using Toolforge will soon get the upgraded buildpacks version, bringing support for newer language versions and other upstream improvements and fixes. If you use Toolforge Build Service, review the recent [https://lists.wikimedia.org/hyperkitty/list/cloud-announce@lists.wikimedia.org/thread/EMYTA32EV2V5SQ2JIEOD2CL66YFIZEKV/ cloud-announce email] and update your build configuration as necessary to ensure your tools are compatible. [https://wikitech.wikimedia.org/w/index.php?title=Help:Toolforge/Building_container_images&oldid=2392097#Buildpack_environment_upgrade_process][https://phabricator.wikimedia.org/T380127] * The [https://api.wikimedia.org/wiki/Main_Page API Portal] documentation wiki will shut down in June 2026. API keys created on the API Portal will continue to work normally. api.wikimedia.org endpoints will be deprecated gradually starting in July 2026. Documentation on the API Portal is moving to [[mw:Wikimedia APIs|mediawiki.org]]. Learn more on the [[wikitech:API Portal/Deprecation|project page]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.21|MediaWiki]] '''In depth''' * [[m:Special:MyLanguage/WMDE Technical Wishes|WMDE Technical Wishes]] is considering improvements to [[m:WMDE Technical Wishes/References/VisualEditor automatic reference names|automatically generated reference names in VisualEditor]]. Please check out the [[m:WMDE Technical Wishes/References/VisualEditor automatic reference names#Proposed solutions|proposed solutions]] and participate in the [[m:Talk:WMDE Technical Wishes/References/VisualEditor automatic reference names#Request for comment|request for comment]]. '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/13|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W13"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 23 mars 2026 à 17:51 (CET) <!-- Message envoyé par User:UOzurumba (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30268305 --> == Actualités techniques n° 2026-14 == <section begin="technews-2026-W14"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/14|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * Le version Beta de [[abstract:|Abstract Wikipedia]], un nouveau projet Wikimédia indépendant du langage, a été lancée la semaine dernière. Ce projet permet aux communautés de construire des articles Wikipédia dans leur langue natale, qui peuvent directement être lus par les autres utilisateurs et utilisatrices dans leur propre langage. Le wiki fonctionne grâce à des instructions de Wikifunctions et au contenu structuré issu de Wikidata. [[:f:Special:MyLanguage/Wikifunctions:Status updates/2026-03-26|En savoir plus]]. '''Actualités pour la contribution''' * L'équipe Croissance mène un test A/B afin d'évaluer l'effet d'un message plus clair et plus convivial encourageant à la création de comptes sur les wikis. Actuellement, lorsqu'un utilisateur mobile non connecté lance la modification, un message d'avertissement s'affiche, pouvant paraître abrupt et décourageant. Il présente également la modification par compte temporaire comme option par défaut, au lieu d'inciter à la création d'un compte. Le test est mené sur dix Wikipédia, dont les versions en arabe, français, espagnol et allemand. [[mw:Special:MyLanguage/Contributors/Account Creation Experiments#2. Improve logged-out warning message (T415160)|En savoir plus]]. * L'équipe des applications Wikimédia sollicite vos commentaires sur [[mw:Special:MyLanguage/Wikimedia Apps/Team/Future of Editing on the Mobile Apps|comment devrait fonctionner l'édition dans les applications mobiles Wikipédia]]. La discussion porte sur l'amélioration de l'accès aux outils d'édition lorsque les utilisateurs appuient sur « Modifier ». Cette initiative s'inscrit dans un effort plus large visant à offrir aux lecteurs intéressés par la contribution une expérience utilisateur plus intuitive. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:45|la tâche soumise|les {{formatnum:45}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:45||s}} la semaine dernière]]. Par exemple, un problème avec la récupération de citations à partir du site d'archive de journaux [https://www.newspapers.com Newspapers.com], qui ne fonctionnait plus en raison d'un blocage des requêtes [[mw:Special:MyLanguage/Citoid|Citoid]], a maintenant été résolu. [https://phabricator.wikimedia.org/T419903] '''Actualités pour la contribution technique''' * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.22|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/14|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W14"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 30 mars 2026 à 21:25 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30329462 --> == Action Required: Update templates/modules for electoral maps (Migrating from P1846 to P14226) == Hello everyone, This is a notice regarding an ongoing data migration on Wikidata that may affect your election-related templates and Lua modules (such as <code>Module:Itemgroup/list</code>). '''The Change:'''<br /> Currently, many templates pull electoral maps from Wikidata using the property [[:d:Property:P1846|P1846]], combined with the qualifier [[:d:Property:P180|P180]]: [[:d:Q19571328|Q19571328]]. We are migrating this data (across roughly 4,000 items) to a newly created, dedicated property: '''[[:d:Property:P14226|P14226]]'''. '''What You Need To Do:'''<br /> To ensure your templates and infoboxes do not break or lose their maps, please update your local code to fetch data from [[:d:Property:P14226|P14226]] instead of the old [[:d:Property:P1846|P1846]] + [[:d:Property:P180|P180]] structure. A [[m:Wikidata/Property Migration: P1846 to P14226/List|list of pages]] was generated using Wikimedia Global Search. '''Deadline:'''<br /> We are temporarily retaining the old data on [[:d:Property:P1846|P1846]] to allow for a smooth transition. However, to complete the data cleanup on Wikidata, the old [[:d:Property:P1846|P1846]] statements will be removed after '''May 1, 2026'''. Please update your modules and templates before this date to prevent any disruption to your wiki's election articles. Let us know if you have any questions or need assistance with the query logic. Thank you for your help! [[User:ZI Jony|ZI Jony]] using [[Utilisateur:MediaWiki message delivery|MediaWiki message delivery]] ([[Discussion utilisateur:MediaWiki message delivery|discussion]]) 3 avril 2026 à 19:11 (CEST) <!-- Message envoyé par User:ZI Jony@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Non-Technical_Village_Pumps_distribution_list&oldid=29941252 --> == Actualités techniques n° 2026-15 == <section begin="technews-2026-W15"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/15|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * L’[[mw:Special:MyLanguage/Help:Extension:CampaignEvents|extension CampaignEvents]] comprend désormais une nouvelle fonctionnalité de définition d’objectifs de groupe, permettant aux organisateurs de définir et de suivre les objectifs de l’événement, tels que le nombre d’articles créés et de contributeurs participants en temps réel. De même, les participants peuvent travailler vers des cibles communes et voir leur impact collectif au fur et à mesure que l’événement se déroule. Cette fonctionnalité est désormais disponible sur tous les wikis Wikimedia. Pour en savoir plus, consultez [[mw:Special:MyLanguage/Help:Extension:CampaignEvents/Registration/Collaborative contributions#Goal setting|la documentation]]. * [[File:Maki-gift-15.svg|12px|link=|class=skin-invert|Concerne un souhait]] La nouvelle fonctionnalité d'[[mw:Special:MyLanguage/Help:Watchlist labels|étiquettes de liste de suivi]] (annoncée dans les [[m:Special:MyLanguage/Tech/News/2026/07|Actualités techniques 2026-07 ]]) est désormais disponible via l'ÉditeurVisuel, l'éditeur de code et l'«étoile de suivi»(ou le lien de suivi, pour les habillages qui n'ont pas d'icône d'étoile). Auparavant, il n'était possible d'attribuer des étiquettes que via [[Special:EditWatchlist|Modifier la liste de suivi]]. Dans ces trois emplacements, il s'agit d'un nouveau champ situé après le champ d'expiration. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:23|la tâche soumise|les {{formatnum:23}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:23||s}} la semaine dernière]]. Par exemple, le problème où les pages de discussion sur mobile avec Parsoid sont inutilisables après les en-têtes de section vides, a maintenant été résolu. [https://phabricator.wikimedia.org/T419171] '''Actualités pour la contribution technique''' * La [[m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing|fonctionnalité de sous-référencement]], qui permet aux contributeurs d'ajouter des détails à une référence existante sans la dupliquer, sera progressivement déployée sur [[phab:T414094|davantage de wikis]] plus tard cette année. Les wikis utilisant le gadget [[mw:Special:MyLanguage/Reference Tooltips|Reference Tooltips]] sont encouragés à mettre à jour leur version (généralement sur [[m:MediaWiki:Gadget-ReferenceTooltips.js|MediaWiki:Gadget-ReferenceTooltips.js]] comme indiqué [https://en.wikipedia.org/w/index.php?diff=1344408362 ici]) pour assurer la compatibilité. D'autres gadgets liés aux références pourraient également être affectés. [https://phabricator.wikimedia.org/T416304] * Toutes les éditions de Wikinews seront fermées et passeront en mode lecture seule le 4 mai 2026. Le contenu restera accessible, mais aucune nouvelle modification ni aucun nouvel article ne pourra être ajouté. Cette fermeture a été approuvée par le Conseil d'administration de la Fondation Wikimedia à la suite de discussions prolongées. [[m:Wikimedia Foundation Board noticeboard#Board of Trustees Approves Closure of Wikinews|En savoir plus]]. * L'[[:mw:Special:MyLanguage/API:Action API|API d'action]] a proposé plusieurs formats pour les résultats demandés. L'un d'entre eux, <bdi lang="zxx" dir="ltr"><code><nowiki>format=php</nowiki></code></bdi>, sera bientôt supprimé. Veuillez vous assurer que vos scripts ou robots utilisent le [[mw:Special:MyLanguage/API:Data formats#Output|format JSON]]. Cette suppression devrait affecter très peu de scripts et de robots. [https://phabricator.wikimedia.org/T118538] * La page [[Special:NamespaceInfo|Special:NamespaceInfo]] inclut désormais les alias d'espace de noms. Par exemple «WP» pour l'espace de noms ''Projet'' (''Wikipédia'') sur la Wikipédia en allemand. [https://phabricator.wikimedia.org/T381455] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.23|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/15|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W15"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 6 avril 2026 à 18:19 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30362761 --> ib85o7gh5yqqx4ifq1vjpfhskuaot2h Les cartes graphiques/Le rendu d'une scène 3D : l'API graphique 0 83408 763040 758184 2026-04-06T15:05:40Z Mewtow 31375 /* Les draw calls */ 763040 wikitext text/x-wiki De nos jours, le développement de jeux vidéo, ou tout simplement de tout rendu 3D, utilise des API 3D. Les API 3D les plus connues sont DirectX, OpenGL, et Vulkan. L'enjeu des API est de ne pas avoir à recoder un moteur de jeu différent pour chaque carte graphique ou ordinateur existant. Elles fournissent des fonctions qui effectuent des calculs bien spécifiques de rendu 3D, mais pas que. L'application de rendu 3D utilise des fonctionnalités de ces API 3D, qui elles-mêmes utilisent les autres intermédiaires, les autres maillons de la chaîne. Typiquement, ces API communiquent avec le pilote de la carte graphique et le système d'exploitation. ==La description des API 3D les plus communes== Dans ce chapitre, nous n'allons pas faire de cours du DirextX, ulkan ou toute API précise. Toutes le API graphiques fonctionnent globalement sur les mêmes principes, que nous allons expliquer dans les grandes lignes. Les explications seront conçues pour que les personnes sans bagage de la programmation graphique puissent comprendre, seuls desbases très mineures en programmation seront nécessaires dans le pire des cas. ===Les ''draw calls''=== Une API 3D fournit un certain nombre de fonctions qu'un programmeur peut exécuter à loisir. La principale est la fonction qui dessine quelque chose dans le ''framebuffer''. Elle est appelée ''draw()'' dans la terminologie DirectX, gldraw pour OpenGL, vkcmddraw pour Vulkan. Une exécution de cette fonction est appelée un '''''draw call'''''. Un ''draw call''envoie des informations à la carte graphique, afin qu'elle affiche ce qui est demandé. Instinctivement, on pourrait croire que la fonction ''draw'' calcule tout l'image à afficher d'un seul coup, mais ce n'est pas le cas. En réalité, le moteur graphique d'un jeu effectue le rendu objet par objet, avec un ''draw call'' par objet. Plus il y a d'objets, plus le processeur exécutera de ''draw calls''. Diverses optimisations permettent d'économiser des ''draw calls'', mais cela ne change pas le fait que dessiner l'image finale demande plusieurs ''draw calls'', entre une centaine et plusieurs centaines de milliers suivant la complexité de la scène à rendre. Le fait de rendre une image objet par objet permet de nombreuses optimisations. Par exemple, il peut utiliser une première passe pour dessiner les objets opaques, puis une seconde pour les objets transparents. Tous les moteurs 3D font ainsi, car gérer la transparence est toujours compliqué, surtout avec un tampon de profondeur. Un autre avantage est que le moteur de jeu peut faciliter le travail de l'élimination des surfaces cachées. Par exemple, le moteur de jeu peut trier les objets selon leur profondeur, afin de les rendre du plus proche au plus lointain. Pour les objets opaques, cela permet d'éliminer les surfaces cachées à la perfection : aucun triangle/pixel caché par un autre ne sera rendu. Pour la transparence, cela permet un rendu idéal. Mais trier les objets selon leur profondeur prend alors du temps CPU, qu'il faut comparer à ce qui est gagné sur le GPU. Avant les années 2010 environ, le processeur faisait une bonne partie de l'élimination des surfaces cachées, dans le sens où il déterminait quels objets étaient cachés par d'autres. Il n'émettait pas de ''draw calls'' pour les objets complétement cachés par un autre objet opaque. Par contre, il travaillait au niveau des objets, alors que le GPU travaillait au niveau des triangles. Les objets partiellement cachés étaient gérés par le GPU, avec une élimination des surface cachées triangle par triangle. De nos jours, l'élimination des surfaces cachées est réalisée sur le GPU, dans sa totalité. L'idée est d'utiliser un ''shader'' séparé, un ''compute shader'', qui s'exécute avant toute autre opération de rendu. La scène 3D et tous les modèles sont dans la mémoire vidéo, et non en mémoire RAM. Le ''compute shader'' lit l'ensemble de la géométrie et élimine les surface cachées. On parle de '''''GPU driven rendering''''' pour désigner cette élimination des surfaces cachées réalisée sur le GPU (il faudrait aussi rajouter le choix du ''Level Of Detail'', mais passons. Lors d'un ''draw call'', certains paramètres vont rester constants, alors que d'autres vont varier d'un sommet à l'autre. Les premiers paramètres, constants pour tout un objet, sont appelées des '''variables uniformes'''. Les seconds sont des '''attributs de sommet'''. Par exemple, prenons un sommet : sa position, sa couleur et ses coordonnées de texture sont des attributs du sommet. Les deux sont stockés différemment : les variables uniformes sont simplement intégrées dans les shaders, alors que les attributs sont placés dans le tampon de sommets. : Il y a la même chose avec les pixels, avec des attributs de pixels et des ''pixel uniforms'', la différence étant que les attributs de pixels sont calculés par la rastérisation. ===Les ''render target''=== Plus haut, j'ai dit qu'un ''draw call'' dessine une image dans le ''framebuffer''. Et il s'agit là du cas le plus important, mais certaines techniques de rendu demandent de dessiner des images intermédiaires, qui sont utilisées pour calculer l'image finale. Les images intermédiaires doivent alors être enregistrées ailleurs, par exemple dans une texture. L'idée générale d'enregistrer des images intermédiaires dans une texture, qui sont alors lues par un ''pixel shader'' pour des calculs d'éclairage, des filtres de post-traitement, ou autre. Autoriser d'enregistrer l'image finale dans une texture s'appelle du '''''render-to-texture'''''. Les techniques d'éclairage basées sur des ''shadowmap'' sont dans ce cas. Elles demandent de rendre la scène 3D deux fois : une fois du point de vue de la source de lumière, puis une seconde fois pour obtenir l'image finale. L'idée est que les pixels invisibles depuis la source de lumière, mais visibles depuis la caméra, sont dans l'ombre. La scène rendue depuis la caméra doit donc être mémorisée quelque part, de préférence dans une texture appelée une ''shadowmap''. Une autre utilisation est l'application de filtres de post-traitement, comme du bloom, de la profondeur de champ, etc. L'idée est de mémoriser l'image initiale, sans post-traitement, dans une texture. Puis, un ''shader'' lit cette texture, applique un filtre dessus, et mémorise le résultat dans une autre texture ou dans le ''framebuffer'' s'il calcule l'image finale. Pour cela, les API 3D modernes permettent de préciser où enregistrer l'image finale : dans le ''framebuffer'', dans une texture, dans une simple portion de mémoire, etc. Les endroits où l'image finale peut être rendue s'appellent des '''''render target'''''. Les API modernes supportent de nombreux ''render target'', avec au minimum un ''framebuffer''. Initialement, les API anciennes ne supportaient que le ''framebuffer''. Puis le ''render-to-texture'' est apparu, puis d'autres formes de ''render target''. Il faut noter que les API modernes permettent à un ''shader'' d'écrire dans plusieurs ''render-target''. On parle alors de '''''Multiple Render Targets''''', abrévié en MRT. Le MRT accélère fortement les techniques de rendu différé, qui enregistrent plusieurs images séparées, qui sont combinées par un pixel shader pour obtenir l'image finale. L'intérêt initial était d'accélérer le calcul de l'éclairage par pixel. Sans rendu différé, avec les anciennes API graphiques, il fallait utiliser un ''draw call'' par objet et par source de lumière. Un objet éclairé par N sources de lumière demandait N ''draw call'' pour être éclairé. Avec le rendu différé, pas besoin. De plus, on garantit que le calcul de l'éclairage n'est pas réalisé sur des pixels invisibles, à savoir des calculer l'éclairage pour des triangles cachés par un objet opaque. Le désavantage est que la transparence n'est pas prise en charge, de même que l’antialiasing de type MSAA. Le rendu différé demande deux passes de rendu. La première passe calcule tout, sauf le ''pixel shader'', il n'y a pas de calculs d'éclairage par pixel. Elle enregistre son résultat dans plusieurs textures :un avec la couleur non-éclairée de chaque pixel, un autre pour la profondeur de chaque pixel (le tampon de profondeur), une texture contenant les normales de la surface pour chaque pixel, et éventuellement d'autres informations (couleur spéculaire, autres). Les textures sont ensuite utilisées par un pixel shader pour calculer l'image finale avec éclairage. Il faut alors supporter des pseudo-''framebuffer'' pour chaque "texture", appelés des '''''G-buffer''''', pour gérer de telles techniques. De plus, le MRT optimise le rendu. Pas besoin de faire un ''draw call'' par ''G-buffer'', chacun recalculant la géométrie. Avec le MRT, les différents ''G-buffer'' sont calculés en une seule passe, la géométrie n'est calculée qu'une seule fois. {| |[[File:Deferred rendering pass col.jpg|thumb|''G-buffer'' pour la couleur.]] |[[File:Deferred rendering pass dep.jpg|thumb|''G-buffer'' pour la profondeur.]] |[[File:Deferred rendering pass nor.jpg|thumb|''G-buffer'' pour les normales.]] |[[File:Deferred rendering pass res.jpg|thumb|Image finale]] |} ===Les ''render states'' et les ''Pipeline State Object''=== Pour rendre un objet avec un ''draw call'', il faut préciser toutes informations nécessaires pour son rendu : la géométrie de l'objet représentée par une liste de triangles, les textures de l'objet, les ''shaders'' à exécuter (vertex ou pixel shaders), etc. Pour simplifier, nous allons regrouper ces informations en deux : un ''mesh'' qui représente la géométrie de l'objet, et le reste. La géométrie de l'objet est juste une liste de triangles. Le reste est regroupé dans un '''''render state''''', qui liste les textures, les shaders, quel ''render starget'' utiliser, et surtout : diverses options de configuration. Il n'y a qu'un seul ''render state'' actif, qui est mémorisé dans une portion de la RAM qui est toujours fixe. Pour les programmeurs, le ''render state'' est dans une variable globale, qui est lue directement par la fonction ''draw''. Si on veut rendre un objet, on doit mettre à jour le ''render state'' avant de lancer un ''draw call''. Un moteur graphique fait donc le travail suivant : * Pour chaque image : ** Mettre à jour la position de la caméra et autres ** Pour chaque objet, scène 3D inclue : *** 1 - Mettre à jour le ''render state'' *** 2 - Exécuter le ''draw call'' L'API 3D fournit des fonctions pour modifier le ''render state'', en plus de la fonction''draw''. A ce niveau, les anciennes API fonctionne différemment des API plus récentes comme DirectX 12, Vulkan et consort. Les anciennes API fournissaient plusieurs fonctions très spécialisées : certaines pour modifier les textures, d'autres pour changer les shaders, et un paquet d’autres pour modifier telle ou telle option de configuration. Par exemple, il y a probablement une fonction pour changer l'antialiasing. Les API modernes, comme DirectX 12 et Vulkan, permettent de mettre à jour le ''render state'' assez simplement. L'idée est de pré-calculer un ''render state'', qui est alors appelé un ''Pipeline State Object'' (PSO). Mettre à jour le ''render state'' demande alors juste de copier un PSO dans le render state, au lieu d'exécuter une dizaine ou centaine de fonctions pour obtenir le ''render state'' voulu. ===Les commandes graphiques=== L'API 3D traduit chaque ''draw call'' en une ou plusieurs '''commandes graphiques''', qui sont envoyées au ''driver'' du GPU. Les commandes en question sont assez diverses, mais elles sont spécifiques à chaque API graphique. Intuitivement, un ''draw call'' correspond à une commande graphique. Mais il peut y avoir d'autres types de commandes. Par exemple, copier une texture dans la mémoire vidéo demande d'exécuter une commande decopie, idem pour ce qui est de copier un objet/''mesh''. Pour comprendre en quoi un ''draw call'' peut se traduire en plusieurs commandes, prenons l'exemple suivant. On souhaite rendre un objet avec une texture bien précise, mais celle-ci n'a pas encore été chargée en mémoire vidéo. Dans ce cas, le ''draw call'' utilisera une commande pour copier la texture en mémoire vidéo, puis une seconde commande pour rendre l'objet dans le ''framebuffer''. Par contre, si la texture est déjà en mémoire vidéo, le ''draw call'' se traduira en une unique commande de rendu 3D. Il en est de même si le ''mesh'' n'est pas encore en mémoire vidéo : il faut exécuter une commande pour copier le ''mesh'' dans la mémoire vidéo. Il faut préciser que c'est la même chose si le ''draw call'' exécute un shader pour la première fois . Le ''driver'' doit compiler le shader pour la première fois, puis utiliser une commande pour mettre le résultat en mémoire vidéo, puis enfin effectuer le rendu. Cela explique le ''shader stuttering'' présent dans certains jeux récents, à savoir le petit ralentissement très énervant qui survient quand un ''shader'' est compilé en plein milieu d'une partie de jeu. Il est possible de limiter ce problème en compilant des shaders à l'avance, histoire de préparer le terrain pour les futurs ''draw calls'', dans une certaine mesure, mais cela demande du travail, qui n'est possible que le nombre de shaders à compiler reste faible. Les commandes graphiques sont envoyées au ''driver'' de la carte graphique. Il transforme alors ces commandes graphiques en '''commandes matérielles''', compréhensibles par le matériel, en quelque chose que le GPU peut exécuter. Le format des commandes matérielles est spécifique à chaque marquer de GPU, les GPU NVIDIA, Intel et AMD n'utilisent pas le même format de commande. Il est même possible que chaque GPU ait son propre format pour les commandes matérielles. Aussi, nous allons nous arrêter là pour le moment et laissons cela au chapitre sur le processeur de commande. ===Les optimisations liées aux ''draw calls''=== Il faut noter qu'un ''draw call'' demande d'utiliser un peu de puissance CPU : il faut traduire le ''draw call'' en commandes, les envoyer au ''driver'', qui fait du travail dessus, avant de les envoyer au GPU. Dans les premières versions d'OpenGL et DirectX, chaque ''draw call'' effectuait une commutation de contexte pour passer en espace noyau, afin de communiquer avec le ''driver''. Mais cette contrainte a depuis été relâchée, bien qu'elle marche dans les grandes lignes. Faire plein de ''draw calls'' aura donc un cout en CPU conséquent. Une première optimisation regroupe les objets avec le même ''render state'' ensemble. Sans cette optimisation, le moteur graphique met à jour le ''render state'' à chaque fois qu'il rend un objet. Avec cette optimisation, il met à jour le ''render state'' plus rarement. Par contre, le moteur graphique dépense du temps et de la puissance de calcul pour faire le tri. Il y a donc un compromis pas évident, qui ne vaudrait pas souvent le coup. Cependant, cette optimisation débloque d'autres optimisations très importantes, qui permettent de réduire le nombre de ''draw calls''. Plus haut, j'ai dit que le rendu se fait objet par objet, ''mesh'' par ''mesh''. Mais il s'agit là d'une simplification. En réalité, tout moteur graphique digne de ce nom incorpore des optimisations qui cassent cette règle. L'idée est d'éviter de faire plein de petits ''draw call'' : le GPU sera alors peu utilisé alors que le CPU fera beaucoup de travail. A l'inverse, faire peu de gros ''draw call'' entrainera une forte occupation du GPU au prix d'un cout CPU mineur. La première optimisation, appelée le '''''batching''''', regroupe plusieurs objets/''meshs'' en un seul ''draw call''. Par contre, cette optimisation ne marche que pour des objets ayant le même ''render state'', à l'exception de la géométrie. Les deux objets rendus ensemble doivent utiliser les mêmes shaders, les mêmes textures, etc. De plus, la fusion de deux objets doit se faire en mémoire RAM et est le fait du CPU, le GPU et la mémoire vidéo ne sont pas concernés. L'optimisation marche bien pour des objets statiques, ce qui permet de faire la fusion une fois pour toute, là où les objets dynamiques demandent de faire la fusion à chaque image. Diverses optimisations permettent de faciliter le ''batching''. L'idée est de rendre les différents ''render state'' plus similaires que la normale. Une optimisation de ce type est l'usage d''''atlas de textures'''. Un atlas de texture regroupe plusieurs textures en une seule texture. Deux objets avec les mêmes shaders et les mêmes options de configuration, peuvent ainsi partager le même ''render state'' quand ils adressent le même atlas de texture et non exactement les mêmes textures. Une seconde optimisation,appelée l''''''instancing''''', marche dans le cas où un objet dynamique est présent en plusieurs exemplaires à l'écran. L'idée est qu'au lieu d'utiliser un ''draw call'' par exemplaire, on utilise un seul ''draw call'' pour tous les exemplaires. L'avantage est que la carte n'a besoin de mémoriser qu'un seul exemplaire en mémoire vidéo, au lieu de mémoriser plusieurs copies du même ''mesh''. Il faut préciser que les différents exemplaires peuvent être placés à des endroits éloignés, être tournés différemment par rapport à la caméra, être dans des états d'animation différents, etc. Pour cela, le ''draw call'' précise, pour chaque exemplaire, comment l'orienter, le tourner et l'animer. Le ''render state'' contient pour cela une '''liste d'instances''' pour mémoriser ces informations pur chaque exemplaire. Le GPU peut consulter cette liste et la copier en mémoire vidéo. Une seule commande permet ainsi de rendre plusieurs exemplaires : le GPU lit la liste d'instance, le ''mesh'' et dessine automatiquement chaque exemplaire voulu de l'objet. Réduire le nombre de ''draw calls'' peut aussi se faire en évitant les objets peu détaillés, qui utilisent peu de polygones. Pour des objets trop peu détaillés, le GPU exécutera le ''draw call'' très vite et devra attendre que le CPU envoie le suivant. Le cout du ''draw call'' dominera le temps de calcul sur le GPU. Du temps de DirectX 9, l'idéal était d'avoir des objets d'au moins une centaine de triangles. De nos jours, les GPU les CPU sont plus puissant,ce qui fait que ce chiffre est à revoir, mais je n'en connais pas la valeur, même approximative. ==Le pipeline graphique== En plus de fournir des fonctions que les programmeurs peuvent utiliser, les API graphiques décrivent comment s'effectue le rendu d'une image. Elles spécifient comment doit être traité la géométrie, comment doit se faire la rastérisation, le filtrage de texture et bien d'autres choses. Pour le dire autrement, elles décrivent le pipeline graphique à utiliser. Pour rappel, le pipeline graphique comprend plusieurs étapes : plusieurs étapes de traitement de la géométrie, une phase de rastérisation, puis plusieurs étapes de traitement des pixels. Une API 3D comme DirectX ou OpenGl décrète quelles sont les étapes à faire, ce qu'elles font, et l'ordre dans lesquelles il faut les exécuter. Il n'existe pas un pipeline graphique unique et chaque API 3D fait à sa sauce, mais la plupart des API modernes ont des pipelines graphiques très similaires. Les seules différences majeures concernent la présence d'étapes facultatives, comme l'étape de tesselation, qui sont absentes des API anciennes. Pour donner un exemple, je vais prendre l'exemple d'OpenGL 1.0, une des premières version d'OpenGL, aujourd'hui totalement obsolète. Le pipeline d'OpenGL 1.0 est illustré ci-dessous. Il implémente le pipeline graphique de base, avec une phase de traitement de la géométrie (''per vertex operations'' et ''primitive assembly''), la rastérisation, et les traitements sur les pixels (''per fragment operations''). On y voit la présence du ''framebuffer'' et de la mémoire dédiée aux textures, les deux étant soit séparées, soit placée dans la même mémoire vidéo. La ''display list'' est une liste de commandes, de ''draw calls'', que la carte graphique doit traiter d'un seul bloc, chaque ''display list'' correspond au rendu d'une image, pour simplifier. Les étapes ''evaluator'' et ''pixel operations'' sont des étapes facultatives, qui ne sont pas dans le pipeline graphique de base, mais qui sont utiles pour implémenter certains effets graphiques. [[File:Pipeline OpenGL.svg|centre|vignette|upright=2|Pipeline d'OpenGL 1.0]] Le pipeline d'OpenGL 1.0 vu plus haut est très simple, comparé aux pipelines des API modernes. Pour comparaison, voici des schémas qui décrivent le pipeline de DirextX 10 et 11. Vous voyez que le nombre d'étapes n'est pas le même, que les étapes elles-mêmes sont légèrement différentes, etc. Toutes les API 3D modernes sont organisées plus ou moins de la même manière, ce qui fait que le pipeline des schémas ci-dessous colle assez bien avec les logiciels 3D anciens et modernes, ainsi qu'avec l'organisation des cartes graphiques (anciennes ou modernes). {| | style="vertical-align:top;" | [[File:D3D Pipeline.svg|vignette|D3D Pipeline]] |[[File:D3D11 Pipeline.svg|vignette|Pipeline de D3D 11]] |} ===L'implémentation peut être logicielle ou matérielle=== Une API graphique est avant tout quelque chose qui aide le programmeur. Il est d'ailleurs possible de les utiliser sans GPU, avec une simple carte d'affichage. Le rendu 3D se fait alors sur le processeur, et la carte d'affichage ne fait que recevoir l'image calculée et l'afficher. Et c'était le cas dans les années 90, avant l'invention des premières cartes accélératrices 3D. Le rôle des API 3D était de fournir des morceaux de code et un pipeline graphique, afin de simplifier le travail des développeurs, pas de déporter des calculs sur une carte accélératrice 3D. D'ailleurs, OpenGl et Direct X sont apparues avant que les premières cartes graphiques grand public soient inventées. Les premiers accélérateurs 3D sont arrivés sur le marché quelques mois après la toute première version de Direct X et Microsoft n'avait pas prévu le coup. OpenGL était lui encore plus ancien et ne servait pas initialement pour les jeux vidéos, mais pour la production d'images de synthèses et dans des applications industrielles (conception assistée par ordinateur, imagerie médicale, autres). OpenGL était l'API plébiscitée à l'époque, car elle était déjà bien implantée dans le domaine industriel, la compatibilité avec les différents OS de l'époque était très bonne, mais aussi car elle était assez simple à programmer. De nos jours, la grosse majorité du rendu 3D se fait sur le GPU. Les ''draw calls'' sont intégralement traités par le GPU, à quelques détails près. Mais les premières cartes accélératrices 3D ne le gérait que partiellement. Concrétement, les premières cartes de 3Dfx déléguaient le traitement de la géométrie au processeur, et ne s'occupaient que des étapes de rastérisation, de placage de texture et les étapes suivantes. Autant prévenir maintenant, nous verrons de nombreuses cartes graphiques de de genre dans le chapitre sur l'historique de l'accélération 3D. ===Les API imposent des contraintes sur le matériel=== Les API graphiques décrivent un pipeline, mais fournissent aussi d'autres contraintes. Par exemple, elles fournissent des régles sur la manière dont doit être faite la rastérisation. Elle disent plus ou moins quel doit être le résultat attendu par le programmeur. Et les GPU doivent respecter ces règles, ils doivent effectuer le rendu de manière à avoir un résultat identique à celui spécifié par l'API. Notez ma formulation quelque peu alambiquée, qui cache un point important : les GPU font comme si ! Je dis faire comme si, car il se peut que le matériel fasse autrement, mais pour un résultat identique. Tant que l'image finale est celle attendue par l'API 3D, le GPU a le droit de prendre des raccourcis, d'éliminer des calculs inutiles, d'utiliser un algorithme de rastérisation différent, etc. Par exemple, il arrive que la carte graphique fasse certaines opérations en avance, comparé au pipeline imposé par l'API, pour des raisons de performance. Typiquement, effectuer du ''culling'' ou les tests de profondeur plus tôt permet d'annuler de nombreux pixels invisibles à l'écran, et donc d'éliminer beaucoup de calculs inutiles. Mais la carte graphique doit cependant corriger le tout de manière à ce que pour le programmeur, tout se passe comme l'API 3D l'ordonne. De manière générale, sans même se limiter à l'ordonnancement des étapes du pipeline graphique, les règles imposées par les API 3D sont des contraintes fortes, qui contraignent les cartes graphiques dans ce qu'elles peuvent faire. De nombreuses optimisations sont rendues impossibles à cause des contraintes des API 3D. ==Le pilote de carte graphique== Le pilote de la carte graphique est un logiciel qui s'occupe de faire l'interface entre les API 3D et la carte graphique. 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é. Le pilote d'un périphérique sert justement à ajouter ce qui manque : ils ajoutent au système d'exploitation de quoi reconnaître le périphérique, de quoi l'utiliser au mieux. 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 très basique, juste assez pour afficher l’interface de base du système d'exploitation. Par exemple, certaines résolutions ne sont pas disponibles et 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. Le pilote de la carte graphique gère beaucoup de choses. Comme tout pilote de périphérique, il gère la communication entre procersseur et GPU, via des techniques communes comme les interruptions, le ''pooling'' ou le ''DMA''. Plus évident, il s'occupe de la gestion de la mémoire vidéo, à savoir que c'est lui qui place les textures ou les modèles 3D dedans, il place le ''framebuffer'', les ''render target'' et tout ce qui réside en mémoire vidéo. Il s'occupe aussi des fonctionnalités liées à l'affichage : initialiser la carte graphique, fixer la résolution, le taux de rafraichissement, gérer le curseur de souris matériel, etc. Mais surtout, le pilote de périphérique s'occupe de l'exécution des ''draw call'' et des changements de ''render state''. Dans ce qui suit, nous allons nous intéresser aux fonctionnalités spécifiques au rendu 3D. ===Les commandes matérielles, compréhensibles par le GPU=== Pour rappel, les API 3Denvoient des '''commandes graphiques''' au pilote de périphérique. Les commandes graphiques sont standardisées, spécifiques à chaque API, et surtout : indépendantes du matériel. Le matériel ne comprend pas ces commandes graphiques ! A la place, le GPU comprend des '''commandes matérielles''', spécifiques à chaque marque de GPU, si ce n'est à chaque GPU. Lors du passage à une nouvelle génération de GPU, des commandes matérielles peuvent apparaître, d'autres disparaître, d'autre voient leur fonctionnement légèrement altéré, etc. Le pilote de la carte graphique doit convertir les commandes graphiques de l'API 3D, en commandes matérielles que le GPU peut comprendre. : La traduction des commandes se fait dans le pilote en espace utilisateur, alors que leur envoi au GPU est le fait du pilote en espace noyau. L'envoi des commandes à la carte graphique ne se fait pas directement. La carte graphique n'est pas toujours libre pour accepter une nouvelle commande, soit parce qu'elle est occupée par une commande précédente, soit parce qu'elle fait autre chose. Il faut alors faire patienter les données dans une '''file de commandes''', où les commandes matérielles attendent leur tour, dans l'ordre d'arrivée. Elle est placée soit dans une portion de la mémoire vidéo, soit est dans la mémoire RAM. Si la file de commandes est plein, le driver n'accepte plus de demandes en provenance des applications. Une file de commandes pleine 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 la file 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. ===La compilation des ''shaders''=== Le pilote de carte graphique traduit les ''shaders'' en code machine que le GPU peut exécuter. 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, avant d'être pré-compilés vers un langage dit intermédiaire. 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. En clair, il y a deux passes de compilation : une passe de traduction du code source en langage intermédiaire, puis une passe de compilation du code intermédiaire vers le code machine. Notons que la première passe est réalisée par le programmeur des ''shaders'', alors que la seconde est le fait du pilote du GPU. L'avantage est que la compilation prend moins de temps, comparé à compiler directement du code HLSL/GLSL. Le gros du travail à été fait lors de la première passe de compilation et le pilote graphique ne fait que finir le travail. 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 plus vendus et/ou les plus populaires. 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. Les anciennes cartes graphiques avaient des circuits de T&L pour traiter la géométrie, mais elles ont disparues sur les machines récentes. Par souci de compatibilité, les circuits de T&L doivent être émulés sur les GPU récents. Sans cette émulation, les vieux jeux vidéo 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'' fournit par le pilote de carte graphique. {{NavChapitre | book=Les cartes graphiques | prev=La mémoire unifiée et la mémoire vidéo dédiée | prevText=La mémoire unifiée et la mémoire vidéo dédiée | next=Le processeur de commandes | nextText=Le processeur de commandes }}{{autocat}} ad07q5zfhgu0yrestz6audiijwz9y85 763041 763040 2026-04-06T15:07:57Z Mewtow 31375 /* Les draw calls */ 763041 wikitext text/x-wiki De nos jours, le développement de jeux vidéo, ou tout simplement de tout rendu 3D, utilise des API 3D. Les API 3D les plus connues sont DirectX, OpenGL, et Vulkan. L'enjeu des API est de ne pas avoir à recoder un moteur de jeu différent pour chaque carte graphique ou ordinateur existant. Elles fournissent des fonctions qui effectuent des calculs bien spécifiques de rendu 3D, mais pas que. L'application de rendu 3D utilise des fonctionnalités de ces API 3D, qui elles-mêmes utilisent les autres intermédiaires, les autres maillons de la chaîne. Typiquement, ces API communiquent avec le pilote de la carte graphique et le système d'exploitation. ==La description des API 3D les plus communes== Dans ce chapitre, nous n'allons pas faire de cours du DirextX, ulkan ou toute API précise. Toutes le API graphiques fonctionnent globalement sur les mêmes principes, que nous allons expliquer dans les grandes lignes. Les explications seront conçues pour que les personnes sans bagage de la programmation graphique puissent comprendre, seuls desbases très mineures en programmation seront nécessaires dans le pire des cas. ===Les ''draw calls''=== Une API 3D fournit un certain nombre de fonctions qu'un programmeur peut exécuter à loisir. La principale est la fonction qui dessine quelque chose dans le ''framebuffer''. Elle est appelée ''draw()'' dans la terminologie DirectX, gldraw pour OpenGL, vkcmddraw pour Vulkan. Une exécution de cette fonction est appelée un '''''draw call'''''. Un ''draw call''envoie des informations à la carte graphique, afin qu'elle affiche ce qui est demandé. Instinctivement, on pourrait croire que la fonction ''draw'' calcule tout l'image à afficher d'un seul coup, mais ce n'est pas le cas. En réalité, le moteur graphique d'un jeu effectue le rendu objet par objet, avec un ''draw call'' par objet. Plus il y a d'objets, plus le processeur exécutera de ''draw calls''. Diverses optimisations permettent d'économiser des ''draw calls'', mais cela ne change pas le fait que dessiner l'image finale demande plusieurs ''draw calls'', entre une centaine et plusieurs centaines de milliers suivant la complexité de la scène à rendre. Le fait de rendre une image objet par objet permet de nombreuses optimisations. Par exemple, il peut utiliser une première passe pour dessiner les objets opaques, puis une seconde pour les objets transparents. Tous les moteurs 3D font ainsi, car gérer la transparence est toujours compliqué, surtout avec un tampon de profondeur. Un autre avantage est que le moteur de jeu peut faciliter le travail de l'élimination des surfaces cachées. Par exemple, le moteur de jeu peut trier les objets selon leur profondeur, afin de les rendre du plus proche au plus lointain. Pour les objets opaques, cela permet d'éliminer les surfaces cachées à la perfection : aucun triangle/pixel caché par un autre ne sera rendu. Pour la transparence, cela permet un rendu idéal. Mais trier les objets selon leur profondeur prend alors du temps CPU, qu'il faut comparer à ce qui est gagné sur le GPU. Avant les années 2010 environ, le processeur faisait une bonne partie de l'élimination des surfaces cachées, dans le sens où il déterminait quels objets étaient cachés par d'autres. Il n'émettait pas de ''draw calls'' pour les objets complétement cachés par un autre objet opaque. Par contre, il travaillait au niveau des objets, alors que le GPU travaillait au niveau des triangles. Les objets partiellement cachés étaient gérés par le GPU, avec une élimination des surface cachées triangle par triangle. De nos jours, l'élimination des surfaces cachées est réalisée sur le GPU, dans sa totalité. L'idée est d'utiliser un ''shader'' séparé, un ''compute shader'', qui s'exécute avant toute autre opération de rendu. La scène 3D et tous les modèles sont dans la mémoire vidéo, et non en mémoire RAM. Le ''compute shader'' lit l'ensemble de la géométrie et élimine les surface cachées. On parle de '''''GPU driven rendering''''' pour désigner cette élimination des surfaces cachées réalisée sur le GPU (il faudrait aussi rajouter le choix du ''Level Of Detail'', mais passons. Lors d'un ''draw call'', certains paramètres vont rester constants, alors que d'autres vont varier d'un sommet à l'autre. Les premiers paramètres sont appelées des '''variables uniformes''', ou encore des ''uniforms''. Elles restent les mêmes pour un objet, mais varient d'un objet à l'autre. Un exemple est les matrices utilisées par les étapes de transformation et de projection. Les paramètres qui varient d'un sommet à l'autre sont des '''attributs de sommet'''. Par exemple, prenons un sommet : sa position, sa couleur et ses coordonnées de texture sont des attributs du sommet. Les deux sont stockés différemment : les variables uniformes sont simplement intégrées dans les shaders, alors que les attributs sont placés dans le tampon de sommets. : Il y a la même chose avec les pixels, avec des attributs de pixels et des ''pixel uniforms'', la différence étant que les attributs de pixels sont calculés par la rastérisation. Il faut noter que les processeurs de shaders avaient autrefois des registres séparés pour les deux, et c'est toujours un peu le cas à l'heure actuelle. ===Les ''render target''=== Plus haut, j'ai dit qu'un ''draw call'' dessine une image dans le ''framebuffer''. Et il s'agit là du cas le plus important, mais certaines techniques de rendu demandent de dessiner des images intermédiaires, qui sont utilisées pour calculer l'image finale. Les images intermédiaires doivent alors être enregistrées ailleurs, par exemple dans une texture. L'idée générale d'enregistrer des images intermédiaires dans une texture, qui sont alors lues par un ''pixel shader'' pour des calculs d'éclairage, des filtres de post-traitement, ou autre. Autoriser d'enregistrer l'image finale dans une texture s'appelle du '''''render-to-texture'''''. Les techniques d'éclairage basées sur des ''shadowmap'' sont dans ce cas. Elles demandent de rendre la scène 3D deux fois : une fois du point de vue de la source de lumière, puis une seconde fois pour obtenir l'image finale. L'idée est que les pixels invisibles depuis la source de lumière, mais visibles depuis la caméra, sont dans l'ombre. La scène rendue depuis la caméra doit donc être mémorisée quelque part, de préférence dans une texture appelée une ''shadowmap''. Une autre utilisation est l'application de filtres de post-traitement, comme du bloom, de la profondeur de champ, etc. L'idée est de mémoriser l'image initiale, sans post-traitement, dans une texture. Puis, un ''shader'' lit cette texture, applique un filtre dessus, et mémorise le résultat dans une autre texture ou dans le ''framebuffer'' s'il calcule l'image finale. Pour cela, les API 3D modernes permettent de préciser où enregistrer l'image finale : dans le ''framebuffer'', dans une texture, dans une simple portion de mémoire, etc. Les endroits où l'image finale peut être rendue s'appellent des '''''render target'''''. Les API modernes supportent de nombreux ''render target'', avec au minimum un ''framebuffer''. Initialement, les API anciennes ne supportaient que le ''framebuffer''. Puis le ''render-to-texture'' est apparu, puis d'autres formes de ''render target''. Il faut noter que les API modernes permettent à un ''shader'' d'écrire dans plusieurs ''render-target''. On parle alors de '''''Multiple Render Targets''''', abrévié en MRT. Le MRT accélère fortement les techniques de rendu différé, qui enregistrent plusieurs images séparées, qui sont combinées par un pixel shader pour obtenir l'image finale. L'intérêt initial était d'accélérer le calcul de l'éclairage par pixel. Sans rendu différé, avec les anciennes API graphiques, il fallait utiliser un ''draw call'' par objet et par source de lumière. Un objet éclairé par N sources de lumière demandait N ''draw call'' pour être éclairé. Avec le rendu différé, pas besoin. De plus, on garantit que le calcul de l'éclairage n'est pas réalisé sur des pixels invisibles, à savoir des calculer l'éclairage pour des triangles cachés par un objet opaque. Le désavantage est que la transparence n'est pas prise en charge, de même que l’antialiasing de type MSAA. Le rendu différé demande deux passes de rendu. La première passe calcule tout, sauf le ''pixel shader'', il n'y a pas de calculs d'éclairage par pixel. Elle enregistre son résultat dans plusieurs textures :un avec la couleur non-éclairée de chaque pixel, un autre pour la profondeur de chaque pixel (le tampon de profondeur), une texture contenant les normales de la surface pour chaque pixel, et éventuellement d'autres informations (couleur spéculaire, autres). Les textures sont ensuite utilisées par un pixel shader pour calculer l'image finale avec éclairage. Il faut alors supporter des pseudo-''framebuffer'' pour chaque "texture", appelés des '''''G-buffer''''', pour gérer de telles techniques. De plus, le MRT optimise le rendu. Pas besoin de faire un ''draw call'' par ''G-buffer'', chacun recalculant la géométrie. Avec le MRT, les différents ''G-buffer'' sont calculés en une seule passe, la géométrie n'est calculée qu'une seule fois. {| |[[File:Deferred rendering pass col.jpg|thumb|''G-buffer'' pour la couleur.]] |[[File:Deferred rendering pass dep.jpg|thumb|''G-buffer'' pour la profondeur.]] |[[File:Deferred rendering pass nor.jpg|thumb|''G-buffer'' pour les normales.]] |[[File:Deferred rendering pass res.jpg|thumb|Image finale]] |} ===Les ''render states'' et les ''Pipeline State Object''=== Pour rendre un objet avec un ''draw call'', il faut préciser toutes informations nécessaires pour son rendu : la géométrie de l'objet représentée par une liste de triangles, les textures de l'objet, les ''shaders'' à exécuter (vertex ou pixel shaders), etc. Pour simplifier, nous allons regrouper ces informations en deux : un ''mesh'' qui représente la géométrie de l'objet, et le reste. La géométrie de l'objet est juste une liste de triangles. Le reste est regroupé dans un '''''render state''''', qui liste les textures, les shaders, quel ''render starget'' utiliser, et surtout : diverses options de configuration. Il n'y a qu'un seul ''render state'' actif, qui est mémorisé dans une portion de la RAM qui est toujours fixe. Pour les programmeurs, le ''render state'' est dans une variable globale, qui est lue directement par la fonction ''draw''. Si on veut rendre un objet, on doit mettre à jour le ''render state'' avant de lancer un ''draw call''. Un moteur graphique fait donc le travail suivant : * Pour chaque image : ** Mettre à jour la position de la caméra et autres ** Pour chaque objet, scène 3D inclue : *** 1 - Mettre à jour le ''render state'' *** 2 - Exécuter le ''draw call'' L'API 3D fournit des fonctions pour modifier le ''render state'', en plus de la fonction''draw''. A ce niveau, les anciennes API fonctionne différemment des API plus récentes comme DirectX 12, Vulkan et consort. Les anciennes API fournissaient plusieurs fonctions très spécialisées : certaines pour modifier les textures, d'autres pour changer les shaders, et un paquet d’autres pour modifier telle ou telle option de configuration. Par exemple, il y a probablement une fonction pour changer l'antialiasing. Les API modernes, comme DirectX 12 et Vulkan, permettent de mettre à jour le ''render state'' assez simplement. L'idée est de pré-calculer un ''render state'', qui est alors appelé un ''Pipeline State Object'' (PSO). Mettre à jour le ''render state'' demande alors juste de copier un PSO dans le render state, au lieu d'exécuter une dizaine ou centaine de fonctions pour obtenir le ''render state'' voulu. ===Les commandes graphiques=== L'API 3D traduit chaque ''draw call'' en une ou plusieurs '''commandes graphiques''', qui sont envoyées au ''driver'' du GPU. Les commandes en question sont assez diverses, mais elles sont spécifiques à chaque API graphique. Intuitivement, un ''draw call'' correspond à une commande graphique. Mais il peut y avoir d'autres types de commandes. Par exemple, copier une texture dans la mémoire vidéo demande d'exécuter une commande decopie, idem pour ce qui est de copier un objet/''mesh''. Pour comprendre en quoi un ''draw call'' peut se traduire en plusieurs commandes, prenons l'exemple suivant. On souhaite rendre un objet avec une texture bien précise, mais celle-ci n'a pas encore été chargée en mémoire vidéo. Dans ce cas, le ''draw call'' utilisera une commande pour copier la texture en mémoire vidéo, puis une seconde commande pour rendre l'objet dans le ''framebuffer''. Par contre, si la texture est déjà en mémoire vidéo, le ''draw call'' se traduira en une unique commande de rendu 3D. Il en est de même si le ''mesh'' n'est pas encore en mémoire vidéo : il faut exécuter une commande pour copier le ''mesh'' dans la mémoire vidéo. Il faut préciser que c'est la même chose si le ''draw call'' exécute un shader pour la première fois . Le ''driver'' doit compiler le shader pour la première fois, puis utiliser une commande pour mettre le résultat en mémoire vidéo, puis enfin effectuer le rendu. Cela explique le ''shader stuttering'' présent dans certains jeux récents, à savoir le petit ralentissement très énervant qui survient quand un ''shader'' est compilé en plein milieu d'une partie de jeu. Il est possible de limiter ce problème en compilant des shaders à l'avance, histoire de préparer le terrain pour les futurs ''draw calls'', dans une certaine mesure, mais cela demande du travail, qui n'est possible que le nombre de shaders à compiler reste faible. Les commandes graphiques sont envoyées au ''driver'' de la carte graphique. Il transforme alors ces commandes graphiques en '''commandes matérielles''', compréhensibles par le matériel, en quelque chose que le GPU peut exécuter. Le format des commandes matérielles est spécifique à chaque marquer de GPU, les GPU NVIDIA, Intel et AMD n'utilisent pas le même format de commande. Il est même possible que chaque GPU ait son propre format pour les commandes matérielles. Aussi, nous allons nous arrêter là pour le moment et laissons cela au chapitre sur le processeur de commande. ===Les optimisations liées aux ''draw calls''=== Il faut noter qu'un ''draw call'' demande d'utiliser un peu de puissance CPU : il faut traduire le ''draw call'' en commandes, les envoyer au ''driver'', qui fait du travail dessus, avant de les envoyer au GPU. Dans les premières versions d'OpenGL et DirectX, chaque ''draw call'' effectuait une commutation de contexte pour passer en espace noyau, afin de communiquer avec le ''driver''. Mais cette contrainte a depuis été relâchée, bien qu'elle marche dans les grandes lignes. Faire plein de ''draw calls'' aura donc un cout en CPU conséquent. Une première optimisation regroupe les objets avec le même ''render state'' ensemble. Sans cette optimisation, le moteur graphique met à jour le ''render state'' à chaque fois qu'il rend un objet. Avec cette optimisation, il met à jour le ''render state'' plus rarement. Par contre, le moteur graphique dépense du temps et de la puissance de calcul pour faire le tri. Il y a donc un compromis pas évident, qui ne vaudrait pas souvent le coup. Cependant, cette optimisation débloque d'autres optimisations très importantes, qui permettent de réduire le nombre de ''draw calls''. Plus haut, j'ai dit que le rendu se fait objet par objet, ''mesh'' par ''mesh''. Mais il s'agit là d'une simplification. En réalité, tout moteur graphique digne de ce nom incorpore des optimisations qui cassent cette règle. L'idée est d'éviter de faire plein de petits ''draw call'' : le GPU sera alors peu utilisé alors que le CPU fera beaucoup de travail. A l'inverse, faire peu de gros ''draw call'' entrainera une forte occupation du GPU au prix d'un cout CPU mineur. La première optimisation, appelée le '''''batching''''', regroupe plusieurs objets/''meshs'' en un seul ''draw call''. Par contre, cette optimisation ne marche que pour des objets ayant le même ''render state'', à l'exception de la géométrie. Les deux objets rendus ensemble doivent utiliser les mêmes shaders, les mêmes textures, etc. De plus, la fusion de deux objets doit se faire en mémoire RAM et est le fait du CPU, le GPU et la mémoire vidéo ne sont pas concernés. L'optimisation marche bien pour des objets statiques, ce qui permet de faire la fusion une fois pour toute, là où les objets dynamiques demandent de faire la fusion à chaque image. Diverses optimisations permettent de faciliter le ''batching''. L'idée est de rendre les différents ''render state'' plus similaires que la normale. Une optimisation de ce type est l'usage d''''atlas de textures'''. Un atlas de texture regroupe plusieurs textures en une seule texture. Deux objets avec les mêmes shaders et les mêmes options de configuration, peuvent ainsi partager le même ''render state'' quand ils adressent le même atlas de texture et non exactement les mêmes textures. Une seconde optimisation,appelée l''''''instancing''''', marche dans le cas où un objet dynamique est présent en plusieurs exemplaires à l'écran. L'idée est qu'au lieu d'utiliser un ''draw call'' par exemplaire, on utilise un seul ''draw call'' pour tous les exemplaires. L'avantage est que la carte n'a besoin de mémoriser qu'un seul exemplaire en mémoire vidéo, au lieu de mémoriser plusieurs copies du même ''mesh''. Il faut préciser que les différents exemplaires peuvent être placés à des endroits éloignés, être tournés différemment par rapport à la caméra, être dans des états d'animation différents, etc. Pour cela, le ''draw call'' précise, pour chaque exemplaire, comment l'orienter, le tourner et l'animer. Le ''render state'' contient pour cela une '''liste d'instances''' pour mémoriser ces informations pur chaque exemplaire. Le GPU peut consulter cette liste et la copier en mémoire vidéo. Une seule commande permet ainsi de rendre plusieurs exemplaires : le GPU lit la liste d'instance, le ''mesh'' et dessine automatiquement chaque exemplaire voulu de l'objet. Réduire le nombre de ''draw calls'' peut aussi se faire en évitant les objets peu détaillés, qui utilisent peu de polygones. Pour des objets trop peu détaillés, le GPU exécutera le ''draw call'' très vite et devra attendre que le CPU envoie le suivant. Le cout du ''draw call'' dominera le temps de calcul sur le GPU. Du temps de DirectX 9, l'idéal était d'avoir des objets d'au moins une centaine de triangles. De nos jours, les GPU les CPU sont plus puissant,ce qui fait que ce chiffre est à revoir, mais je n'en connais pas la valeur, même approximative. ==Le pipeline graphique== En plus de fournir des fonctions que les programmeurs peuvent utiliser, les API graphiques décrivent comment s'effectue le rendu d'une image. Elles spécifient comment doit être traité la géométrie, comment doit se faire la rastérisation, le filtrage de texture et bien d'autres choses. Pour le dire autrement, elles décrivent le pipeline graphique à utiliser. Pour rappel, le pipeline graphique comprend plusieurs étapes : plusieurs étapes de traitement de la géométrie, une phase de rastérisation, puis plusieurs étapes de traitement des pixels. Une API 3D comme DirectX ou OpenGl décrète quelles sont les étapes à faire, ce qu'elles font, et l'ordre dans lesquelles il faut les exécuter. Il n'existe pas un pipeline graphique unique et chaque API 3D fait à sa sauce, mais la plupart des API modernes ont des pipelines graphiques très similaires. Les seules différences majeures concernent la présence d'étapes facultatives, comme l'étape de tesselation, qui sont absentes des API anciennes. Pour donner un exemple, je vais prendre l'exemple d'OpenGL 1.0, une des premières version d'OpenGL, aujourd'hui totalement obsolète. Le pipeline d'OpenGL 1.0 est illustré ci-dessous. Il implémente le pipeline graphique de base, avec une phase de traitement de la géométrie (''per vertex operations'' et ''primitive assembly''), la rastérisation, et les traitements sur les pixels (''per fragment operations''). On y voit la présence du ''framebuffer'' et de la mémoire dédiée aux textures, les deux étant soit séparées, soit placée dans la même mémoire vidéo. La ''display list'' est une liste de commandes, de ''draw calls'', que la carte graphique doit traiter d'un seul bloc, chaque ''display list'' correspond au rendu d'une image, pour simplifier. Les étapes ''evaluator'' et ''pixel operations'' sont des étapes facultatives, qui ne sont pas dans le pipeline graphique de base, mais qui sont utiles pour implémenter certains effets graphiques. [[File:Pipeline OpenGL.svg|centre|vignette|upright=2|Pipeline d'OpenGL 1.0]] Le pipeline d'OpenGL 1.0 vu plus haut est très simple, comparé aux pipelines des API modernes. Pour comparaison, voici des schémas qui décrivent le pipeline de DirextX 10 et 11. Vous voyez que le nombre d'étapes n'est pas le même, que les étapes elles-mêmes sont légèrement différentes, etc. Toutes les API 3D modernes sont organisées plus ou moins de la même manière, ce qui fait que le pipeline des schémas ci-dessous colle assez bien avec les logiciels 3D anciens et modernes, ainsi qu'avec l'organisation des cartes graphiques (anciennes ou modernes). {| | style="vertical-align:top;" | [[File:D3D Pipeline.svg|vignette|D3D Pipeline]] |[[File:D3D11 Pipeline.svg|vignette|Pipeline de D3D 11]] |} ===L'implémentation peut être logicielle ou matérielle=== Une API graphique est avant tout quelque chose qui aide le programmeur. Il est d'ailleurs possible de les utiliser sans GPU, avec une simple carte d'affichage. Le rendu 3D se fait alors sur le processeur, et la carte d'affichage ne fait que recevoir l'image calculée et l'afficher. Et c'était le cas dans les années 90, avant l'invention des premières cartes accélératrices 3D. Le rôle des API 3D était de fournir des morceaux de code et un pipeline graphique, afin de simplifier le travail des développeurs, pas de déporter des calculs sur une carte accélératrice 3D. D'ailleurs, OpenGl et Direct X sont apparues avant que les premières cartes graphiques grand public soient inventées. Les premiers accélérateurs 3D sont arrivés sur le marché quelques mois après la toute première version de Direct X et Microsoft n'avait pas prévu le coup. OpenGL était lui encore plus ancien et ne servait pas initialement pour les jeux vidéos, mais pour la production d'images de synthèses et dans des applications industrielles (conception assistée par ordinateur, imagerie médicale, autres). OpenGL était l'API plébiscitée à l'époque, car elle était déjà bien implantée dans le domaine industriel, la compatibilité avec les différents OS de l'époque était très bonne, mais aussi car elle était assez simple à programmer. De nos jours, la grosse majorité du rendu 3D se fait sur le GPU. Les ''draw calls'' sont intégralement traités par le GPU, à quelques détails près. Mais les premières cartes accélératrices 3D ne le gérait que partiellement. Concrétement, les premières cartes de 3Dfx déléguaient le traitement de la géométrie au processeur, et ne s'occupaient que des étapes de rastérisation, de placage de texture et les étapes suivantes. Autant prévenir maintenant, nous verrons de nombreuses cartes graphiques de de genre dans le chapitre sur l'historique de l'accélération 3D. ===Les API imposent des contraintes sur le matériel=== Les API graphiques décrivent un pipeline, mais fournissent aussi d'autres contraintes. Par exemple, elles fournissent des régles sur la manière dont doit être faite la rastérisation. Elle disent plus ou moins quel doit être le résultat attendu par le programmeur. Et les GPU doivent respecter ces règles, ils doivent effectuer le rendu de manière à avoir un résultat identique à celui spécifié par l'API. Notez ma formulation quelque peu alambiquée, qui cache un point important : les GPU font comme si ! Je dis faire comme si, car il se peut que le matériel fasse autrement, mais pour un résultat identique. Tant que l'image finale est celle attendue par l'API 3D, le GPU a le droit de prendre des raccourcis, d'éliminer des calculs inutiles, d'utiliser un algorithme de rastérisation différent, etc. Par exemple, il arrive que la carte graphique fasse certaines opérations en avance, comparé au pipeline imposé par l'API, pour des raisons de performance. Typiquement, effectuer du ''culling'' ou les tests de profondeur plus tôt permet d'annuler de nombreux pixels invisibles à l'écran, et donc d'éliminer beaucoup de calculs inutiles. Mais la carte graphique doit cependant corriger le tout de manière à ce que pour le programmeur, tout se passe comme l'API 3D l'ordonne. De manière générale, sans même se limiter à l'ordonnancement des étapes du pipeline graphique, les règles imposées par les API 3D sont des contraintes fortes, qui contraignent les cartes graphiques dans ce qu'elles peuvent faire. De nombreuses optimisations sont rendues impossibles à cause des contraintes des API 3D. ==Le pilote de carte graphique== Le pilote de la carte graphique est un logiciel qui s'occupe de faire l'interface entre les API 3D et la carte graphique. 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é. Le pilote d'un périphérique sert justement à ajouter ce qui manque : ils ajoutent au système d'exploitation de quoi reconnaître le périphérique, de quoi l'utiliser au mieux. 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 très basique, juste assez pour afficher l’interface de base du système d'exploitation. Par exemple, certaines résolutions ne sont pas disponibles et 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. Le pilote de la carte graphique gère beaucoup de choses. Comme tout pilote de périphérique, il gère la communication entre procersseur et GPU, via des techniques communes comme les interruptions, le ''pooling'' ou le ''DMA''. Plus évident, il s'occupe de la gestion de la mémoire vidéo, à savoir que c'est lui qui place les textures ou les modèles 3D dedans, il place le ''framebuffer'', les ''render target'' et tout ce qui réside en mémoire vidéo. Il s'occupe aussi des fonctionnalités liées à l'affichage : initialiser la carte graphique, fixer la résolution, le taux de rafraichissement, gérer le curseur de souris matériel, etc. Mais surtout, le pilote de périphérique s'occupe de l'exécution des ''draw call'' et des changements de ''render state''. Dans ce qui suit, nous allons nous intéresser aux fonctionnalités spécifiques au rendu 3D. ===Les commandes matérielles, compréhensibles par le GPU=== Pour rappel, les API 3Denvoient des '''commandes graphiques''' au pilote de périphérique. Les commandes graphiques sont standardisées, spécifiques à chaque API, et surtout : indépendantes du matériel. Le matériel ne comprend pas ces commandes graphiques ! A la place, le GPU comprend des '''commandes matérielles''', spécifiques à chaque marque de GPU, si ce n'est à chaque GPU. Lors du passage à une nouvelle génération de GPU, des commandes matérielles peuvent apparaître, d'autres disparaître, d'autre voient leur fonctionnement légèrement altéré, etc. Le pilote de la carte graphique doit convertir les commandes graphiques de l'API 3D, en commandes matérielles que le GPU peut comprendre. : La traduction des commandes se fait dans le pilote en espace utilisateur, alors que leur envoi au GPU est le fait du pilote en espace noyau. L'envoi des commandes à la carte graphique ne se fait pas directement. La carte graphique n'est pas toujours libre pour accepter une nouvelle commande, soit parce qu'elle est occupée par une commande précédente, soit parce qu'elle fait autre chose. Il faut alors faire patienter les données dans une '''file de commandes''', où les commandes matérielles attendent leur tour, dans l'ordre d'arrivée. Elle est placée soit dans une portion de la mémoire vidéo, soit est dans la mémoire RAM. Si la file de commandes est plein, le driver n'accepte plus de demandes en provenance des applications. Une file de commandes pleine 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 la file 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. ===La compilation des ''shaders''=== Le pilote de carte graphique traduit les ''shaders'' en code machine que le GPU peut exécuter. 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, avant d'être pré-compilés vers un langage dit intermédiaire. 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. En clair, il y a deux passes de compilation : une passe de traduction du code source en langage intermédiaire, puis une passe de compilation du code intermédiaire vers le code machine. Notons que la première passe est réalisée par le programmeur des ''shaders'', alors que la seconde est le fait du pilote du GPU. L'avantage est que la compilation prend moins de temps, comparé à compiler directement du code HLSL/GLSL. Le gros du travail à été fait lors de la première passe de compilation et le pilote graphique ne fait que finir le travail. 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 plus vendus et/ou les plus populaires. 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. Les anciennes cartes graphiques avaient des circuits de T&L pour traiter la géométrie, mais elles ont disparues sur les machines récentes. Par souci de compatibilité, les circuits de T&L doivent être émulés sur les GPU récents. Sans cette émulation, les vieux jeux vidéo 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'' fournit par le pilote de carte graphique. {{NavChapitre | book=Les cartes graphiques | prev=La mémoire unifiée et la mémoire vidéo dédiée | prevText=La mémoire unifiée et la mémoire vidéo dédiée | next=Le processeur de commandes | nextText=Le processeur de commandes }}{{autocat}} o7gm3lp81ckwc0z9y96x1da3bfle624 763042 763041 2026-04-06T15:09:12Z Mewtow 31375 /* Les draw calls */ 763042 wikitext text/x-wiki De nos jours, le développement de jeux vidéo, ou tout simplement de tout rendu 3D, utilise des API 3D. Les API 3D les plus connues sont DirectX, OpenGL, et Vulkan. L'enjeu des API est de ne pas avoir à recoder un moteur de jeu différent pour chaque carte graphique ou ordinateur existant. Elles fournissent des fonctions qui effectuent des calculs bien spécifiques de rendu 3D, mais pas que. L'application de rendu 3D utilise des fonctionnalités de ces API 3D, qui elles-mêmes utilisent les autres intermédiaires, les autres maillons de la chaîne. Typiquement, ces API communiquent avec le pilote de la carte graphique et le système d'exploitation. ==La description des API 3D les plus communes== Dans ce chapitre, nous n'allons pas faire de cours du DirextX, ulkan ou toute API précise. Toutes le API graphiques fonctionnent globalement sur les mêmes principes, que nous allons expliquer dans les grandes lignes. Les explications seront conçues pour que les personnes sans bagage de la programmation graphique puissent comprendre, seuls desbases très mineures en programmation seront nécessaires dans le pire des cas. ===Les ''draw calls''=== Une API 3D fournit un certain nombre de fonctions qu'un programmeur peut exécuter à loisir. La principale est la fonction qui dessine quelque chose dans le ''framebuffer''. Elle est appelée ''draw()'' dans la terminologie DirectX, gldraw pour OpenGL, vkcmddraw pour Vulkan. Une exécution de cette fonction est appelée un '''''draw call'''''. Un ''draw call''envoie des informations à la carte graphique, afin qu'elle affiche ce qui est demandé. Instinctivement, on pourrait croire que la fonction ''draw'' calcule tout l'image à afficher d'un seul coup, mais ce n'est pas le cas. En réalité, le moteur graphique d'un jeu effectue le rendu objet par objet, avec un ''draw call'' par objet. Plus il y a d'objets, plus le processeur exécutera de ''draw calls''. Diverses optimisations permettent d'économiser des ''draw calls'', mais cela ne change pas le fait que dessiner l'image finale demande plusieurs ''draw calls'', entre une centaine et plusieurs centaines de milliers suivant la complexité de la scène à rendre. Le fait de rendre une image objet par objet permet de nombreuses optimisations. Par exemple, il peut utiliser une première passe pour dessiner les objets opaques, puis une seconde pour les objets transparents. Tous les moteurs 3D font ainsi, car gérer la transparence est toujours compliqué, surtout avec un tampon de profondeur. Un autre avantage est que le moteur de jeu peut faciliter le travail de l'élimination des surfaces cachées. Par exemple, le moteur de jeu peut trier les objets selon leur profondeur, afin de les rendre du plus proche au plus lointain. Pour les objets opaques, cela permet d'éliminer les surfaces cachées à la perfection : aucun triangle/pixel caché par un autre ne sera rendu. Pour la transparence, cela permet un rendu idéal. Mais trier les objets selon leur profondeur prend alors du temps CPU, qu'il faut comparer à ce qui est gagné sur le GPU. Avant les années 2010 environ, le processeur faisait une bonne partie de l'élimination des surfaces cachées, dans le sens où il déterminait quels objets étaient cachés par d'autres. Il n'émettait pas de ''draw calls'' pour les objets complétement cachés par un autre objet opaque. Par contre, il travaillait au niveau des objets, alors que le GPU travaillait au niveau des triangles. Les objets partiellement cachés étaient gérés par le GPU, avec une élimination des surface cachées triangle par triangle. De nos jours, l'élimination des surfaces cachées est réalisée sur le GPU, dans sa totalité. L'idée est d'utiliser un ''shader'' séparé, un ''compute shader'', qui s'exécute avant toute autre opération de rendu. La scène 3D et tous les modèles sont dans la mémoire vidéo, et non en mémoire RAM. Le ''compute shader'' lit l'ensemble de la géométrie et élimine les surface cachées. On parle de '''''GPU driven rendering''''' pour désigner cette élimination des surfaces cachées réalisée sur le GPU (il faudrait aussi rajouter le choix du ''Level Of Detail'', mais passons. ===Les attributs de sommets et variables uniformes=== Lors d'un ''draw call'', certains paramètres vont rester constants, alors que d'autres vont varier d'un sommet à l'autre. Les paramètres qui varient d'un sommet à l'autre sont des '''attributs de sommet'''. Par exemple, prenons un sommet : sa position, sa couleur et ses coordonnées de texture sont des attributs du sommet. Les paramètres constants sont appelées des '''variables uniformes''', ou encore des ''uniforms''. Elles restent les mêmes pour un objet, mais varient d'un objet à l'autre. Un exemple est les matrices utilisées par les étapes de transformation et de projection. : Il y a la même chose avec les pixels, avec des attributs de pixels et des ''pixel uniforms'', la différence étant que les attributs de pixels sont calculés par la rastérisation. Les deux sont stockés différemment : les variables uniformes sont simplement intégrées dans les shaders, alors que les attributs sont placés dans le tampon de sommets. Il faut noter que les processeurs de shaders avaient autrefois des registres séparés pour les deux, et c'est toujours un peu le cas à l'heure actuelle. ===Les ''render target''=== Plus haut, j'ai dit qu'un ''draw call'' dessine une image dans le ''framebuffer''. Et il s'agit là du cas le plus important, mais certaines techniques de rendu demandent de dessiner des images intermédiaires, qui sont utilisées pour calculer l'image finale. Les images intermédiaires doivent alors être enregistrées ailleurs, par exemple dans une texture. L'idée générale d'enregistrer des images intermédiaires dans une texture, qui sont alors lues par un ''pixel shader'' pour des calculs d'éclairage, des filtres de post-traitement, ou autre. Autoriser d'enregistrer l'image finale dans une texture s'appelle du '''''render-to-texture'''''. Les techniques d'éclairage basées sur des ''shadowmap'' sont dans ce cas. Elles demandent de rendre la scène 3D deux fois : une fois du point de vue de la source de lumière, puis une seconde fois pour obtenir l'image finale. L'idée est que les pixels invisibles depuis la source de lumière, mais visibles depuis la caméra, sont dans l'ombre. La scène rendue depuis la caméra doit donc être mémorisée quelque part, de préférence dans une texture appelée une ''shadowmap''. Une autre utilisation est l'application de filtres de post-traitement, comme du bloom, de la profondeur de champ, etc. L'idée est de mémoriser l'image initiale, sans post-traitement, dans une texture. Puis, un ''shader'' lit cette texture, applique un filtre dessus, et mémorise le résultat dans une autre texture ou dans le ''framebuffer'' s'il calcule l'image finale. Pour cela, les API 3D modernes permettent de préciser où enregistrer l'image finale : dans le ''framebuffer'', dans une texture, dans une simple portion de mémoire, etc. Les endroits où l'image finale peut être rendue s'appellent des '''''render target'''''. Les API modernes supportent de nombreux ''render target'', avec au minimum un ''framebuffer''. Initialement, les API anciennes ne supportaient que le ''framebuffer''. Puis le ''render-to-texture'' est apparu, puis d'autres formes de ''render target''. Il faut noter que les API modernes permettent à un ''shader'' d'écrire dans plusieurs ''render-target''. On parle alors de '''''Multiple Render Targets''''', abrévié en MRT. Le MRT accélère fortement les techniques de rendu différé, qui enregistrent plusieurs images séparées, qui sont combinées par un pixel shader pour obtenir l'image finale. L'intérêt initial était d'accélérer le calcul de l'éclairage par pixel. Sans rendu différé, avec les anciennes API graphiques, il fallait utiliser un ''draw call'' par objet et par source de lumière. Un objet éclairé par N sources de lumière demandait N ''draw call'' pour être éclairé. Avec le rendu différé, pas besoin. De plus, on garantit que le calcul de l'éclairage n'est pas réalisé sur des pixels invisibles, à savoir des calculer l'éclairage pour des triangles cachés par un objet opaque. Le désavantage est que la transparence n'est pas prise en charge, de même que l’antialiasing de type MSAA. Le rendu différé demande deux passes de rendu. La première passe calcule tout, sauf le ''pixel shader'', il n'y a pas de calculs d'éclairage par pixel. Elle enregistre son résultat dans plusieurs textures :un avec la couleur non-éclairée de chaque pixel, un autre pour la profondeur de chaque pixel (le tampon de profondeur), une texture contenant les normales de la surface pour chaque pixel, et éventuellement d'autres informations (couleur spéculaire, autres). Les textures sont ensuite utilisées par un pixel shader pour calculer l'image finale avec éclairage. Il faut alors supporter des pseudo-''framebuffer'' pour chaque "texture", appelés des '''''G-buffer''''', pour gérer de telles techniques. De plus, le MRT optimise le rendu. Pas besoin de faire un ''draw call'' par ''G-buffer'', chacun recalculant la géométrie. Avec le MRT, les différents ''G-buffer'' sont calculés en une seule passe, la géométrie n'est calculée qu'une seule fois. {| |[[File:Deferred rendering pass col.jpg|thumb|''G-buffer'' pour la couleur.]] |[[File:Deferred rendering pass dep.jpg|thumb|''G-buffer'' pour la profondeur.]] |[[File:Deferred rendering pass nor.jpg|thumb|''G-buffer'' pour les normales.]] |[[File:Deferred rendering pass res.jpg|thumb|Image finale]] |} ===Les ''render states'' et les ''Pipeline State Object''=== Pour rendre un objet avec un ''draw call'', il faut préciser toutes informations nécessaires pour son rendu : la géométrie de l'objet représentée par une liste de triangles, les textures de l'objet, les ''shaders'' à exécuter (vertex ou pixel shaders), etc. Pour simplifier, nous allons regrouper ces informations en deux : un ''mesh'' qui représente la géométrie de l'objet, et le reste. La géométrie de l'objet est juste une liste de triangles. Le reste est regroupé dans un '''''render state''''', qui liste les textures, les shaders, quel ''render starget'' utiliser, et surtout : diverses options de configuration. Il n'y a qu'un seul ''render state'' actif, qui est mémorisé dans une portion de la RAM qui est toujours fixe. Pour les programmeurs, le ''render state'' est dans une variable globale, qui est lue directement par la fonction ''draw''. Si on veut rendre un objet, on doit mettre à jour le ''render state'' avant de lancer un ''draw call''. Un moteur graphique fait donc le travail suivant : * Pour chaque image : ** Mettre à jour la position de la caméra et autres ** Pour chaque objet, scène 3D inclue : *** 1 - Mettre à jour le ''render state'' *** 2 - Exécuter le ''draw call'' L'API 3D fournit des fonctions pour modifier le ''render state'', en plus de la fonction''draw''. A ce niveau, les anciennes API fonctionne différemment des API plus récentes comme DirectX 12, Vulkan et consort. Les anciennes API fournissaient plusieurs fonctions très spécialisées : certaines pour modifier les textures, d'autres pour changer les shaders, et un paquet d’autres pour modifier telle ou telle option de configuration. Par exemple, il y a probablement une fonction pour changer l'antialiasing. Les API modernes, comme DirectX 12 et Vulkan, permettent de mettre à jour le ''render state'' assez simplement. L'idée est de pré-calculer un ''render state'', qui est alors appelé un ''Pipeline State Object'' (PSO). Mettre à jour le ''render state'' demande alors juste de copier un PSO dans le render state, au lieu d'exécuter une dizaine ou centaine de fonctions pour obtenir le ''render state'' voulu. ===Les commandes graphiques=== L'API 3D traduit chaque ''draw call'' en une ou plusieurs '''commandes graphiques''', qui sont envoyées au ''driver'' du GPU. Les commandes en question sont assez diverses, mais elles sont spécifiques à chaque API graphique. Intuitivement, un ''draw call'' correspond à une commande graphique. Mais il peut y avoir d'autres types de commandes. Par exemple, copier une texture dans la mémoire vidéo demande d'exécuter une commande decopie, idem pour ce qui est de copier un objet/''mesh''. Pour comprendre en quoi un ''draw call'' peut se traduire en plusieurs commandes, prenons l'exemple suivant. On souhaite rendre un objet avec une texture bien précise, mais celle-ci n'a pas encore été chargée en mémoire vidéo. Dans ce cas, le ''draw call'' utilisera une commande pour copier la texture en mémoire vidéo, puis une seconde commande pour rendre l'objet dans le ''framebuffer''. Par contre, si la texture est déjà en mémoire vidéo, le ''draw call'' se traduira en une unique commande de rendu 3D. Il en est de même si le ''mesh'' n'est pas encore en mémoire vidéo : il faut exécuter une commande pour copier le ''mesh'' dans la mémoire vidéo. Il faut préciser que c'est la même chose si le ''draw call'' exécute un shader pour la première fois . Le ''driver'' doit compiler le shader pour la première fois, puis utiliser une commande pour mettre le résultat en mémoire vidéo, puis enfin effectuer le rendu. Cela explique le ''shader stuttering'' présent dans certains jeux récents, à savoir le petit ralentissement très énervant qui survient quand un ''shader'' est compilé en plein milieu d'une partie de jeu. Il est possible de limiter ce problème en compilant des shaders à l'avance, histoire de préparer le terrain pour les futurs ''draw calls'', dans une certaine mesure, mais cela demande du travail, qui n'est possible que le nombre de shaders à compiler reste faible. Les commandes graphiques sont envoyées au ''driver'' de la carte graphique. Il transforme alors ces commandes graphiques en '''commandes matérielles''', compréhensibles par le matériel, en quelque chose que le GPU peut exécuter. Le format des commandes matérielles est spécifique à chaque marquer de GPU, les GPU NVIDIA, Intel et AMD n'utilisent pas le même format de commande. Il est même possible que chaque GPU ait son propre format pour les commandes matérielles. Aussi, nous allons nous arrêter là pour le moment et laissons cela au chapitre sur le processeur de commande. ===Les optimisations liées aux ''draw calls''=== Il faut noter qu'un ''draw call'' demande d'utiliser un peu de puissance CPU : il faut traduire le ''draw call'' en commandes, les envoyer au ''driver'', qui fait du travail dessus, avant de les envoyer au GPU. Dans les premières versions d'OpenGL et DirectX, chaque ''draw call'' effectuait une commutation de contexte pour passer en espace noyau, afin de communiquer avec le ''driver''. Mais cette contrainte a depuis été relâchée, bien qu'elle marche dans les grandes lignes. Faire plein de ''draw calls'' aura donc un cout en CPU conséquent. Une première optimisation regroupe les objets avec le même ''render state'' ensemble. Sans cette optimisation, le moteur graphique met à jour le ''render state'' à chaque fois qu'il rend un objet. Avec cette optimisation, il met à jour le ''render state'' plus rarement. Par contre, le moteur graphique dépense du temps et de la puissance de calcul pour faire le tri. Il y a donc un compromis pas évident, qui ne vaudrait pas souvent le coup. Cependant, cette optimisation débloque d'autres optimisations très importantes, qui permettent de réduire le nombre de ''draw calls''. Plus haut, j'ai dit que le rendu se fait objet par objet, ''mesh'' par ''mesh''. Mais il s'agit là d'une simplification. En réalité, tout moteur graphique digne de ce nom incorpore des optimisations qui cassent cette règle. L'idée est d'éviter de faire plein de petits ''draw call'' : le GPU sera alors peu utilisé alors que le CPU fera beaucoup de travail. A l'inverse, faire peu de gros ''draw call'' entrainera une forte occupation du GPU au prix d'un cout CPU mineur. La première optimisation, appelée le '''''batching''''', regroupe plusieurs objets/''meshs'' en un seul ''draw call''. Par contre, cette optimisation ne marche que pour des objets ayant le même ''render state'', à l'exception de la géométrie. Les deux objets rendus ensemble doivent utiliser les mêmes shaders, les mêmes textures, etc. De plus, la fusion de deux objets doit se faire en mémoire RAM et est le fait du CPU, le GPU et la mémoire vidéo ne sont pas concernés. L'optimisation marche bien pour des objets statiques, ce qui permet de faire la fusion une fois pour toute, là où les objets dynamiques demandent de faire la fusion à chaque image. Diverses optimisations permettent de faciliter le ''batching''. L'idée est de rendre les différents ''render state'' plus similaires que la normale. Une optimisation de ce type est l'usage d''''atlas de textures'''. Un atlas de texture regroupe plusieurs textures en une seule texture. Deux objets avec les mêmes shaders et les mêmes options de configuration, peuvent ainsi partager le même ''render state'' quand ils adressent le même atlas de texture et non exactement les mêmes textures. Une seconde optimisation,appelée l''''''instancing''''', marche dans le cas où un objet dynamique est présent en plusieurs exemplaires à l'écran. L'idée est qu'au lieu d'utiliser un ''draw call'' par exemplaire, on utilise un seul ''draw call'' pour tous les exemplaires. L'avantage est que la carte n'a besoin de mémoriser qu'un seul exemplaire en mémoire vidéo, au lieu de mémoriser plusieurs copies du même ''mesh''. Il faut préciser que les différents exemplaires peuvent être placés à des endroits éloignés, être tournés différemment par rapport à la caméra, être dans des états d'animation différents, etc. Pour cela, le ''draw call'' précise, pour chaque exemplaire, comment l'orienter, le tourner et l'animer. Le ''render state'' contient pour cela une '''liste d'instances''' pour mémoriser ces informations pur chaque exemplaire. Le GPU peut consulter cette liste et la copier en mémoire vidéo. Une seule commande permet ainsi de rendre plusieurs exemplaires : le GPU lit la liste d'instance, le ''mesh'' et dessine automatiquement chaque exemplaire voulu de l'objet. Réduire le nombre de ''draw calls'' peut aussi se faire en évitant les objets peu détaillés, qui utilisent peu de polygones. Pour des objets trop peu détaillés, le GPU exécutera le ''draw call'' très vite et devra attendre que le CPU envoie le suivant. Le cout du ''draw call'' dominera le temps de calcul sur le GPU. Du temps de DirectX 9, l'idéal était d'avoir des objets d'au moins une centaine de triangles. De nos jours, les GPU les CPU sont plus puissant,ce qui fait que ce chiffre est à revoir, mais je n'en connais pas la valeur, même approximative. ==Le pipeline graphique== En plus de fournir des fonctions que les programmeurs peuvent utiliser, les API graphiques décrivent comment s'effectue le rendu d'une image. Elles spécifient comment doit être traité la géométrie, comment doit se faire la rastérisation, le filtrage de texture et bien d'autres choses. Pour le dire autrement, elles décrivent le pipeline graphique à utiliser. Pour rappel, le pipeline graphique comprend plusieurs étapes : plusieurs étapes de traitement de la géométrie, une phase de rastérisation, puis plusieurs étapes de traitement des pixels. Une API 3D comme DirectX ou OpenGl décrète quelles sont les étapes à faire, ce qu'elles font, et l'ordre dans lesquelles il faut les exécuter. Il n'existe pas un pipeline graphique unique et chaque API 3D fait à sa sauce, mais la plupart des API modernes ont des pipelines graphiques très similaires. Les seules différences majeures concernent la présence d'étapes facultatives, comme l'étape de tesselation, qui sont absentes des API anciennes. Pour donner un exemple, je vais prendre l'exemple d'OpenGL 1.0, une des premières version d'OpenGL, aujourd'hui totalement obsolète. Le pipeline d'OpenGL 1.0 est illustré ci-dessous. Il implémente le pipeline graphique de base, avec une phase de traitement de la géométrie (''per vertex operations'' et ''primitive assembly''), la rastérisation, et les traitements sur les pixels (''per fragment operations''). On y voit la présence du ''framebuffer'' et de la mémoire dédiée aux textures, les deux étant soit séparées, soit placée dans la même mémoire vidéo. La ''display list'' est une liste de commandes, de ''draw calls'', que la carte graphique doit traiter d'un seul bloc, chaque ''display list'' correspond au rendu d'une image, pour simplifier. Les étapes ''evaluator'' et ''pixel operations'' sont des étapes facultatives, qui ne sont pas dans le pipeline graphique de base, mais qui sont utiles pour implémenter certains effets graphiques. [[File:Pipeline OpenGL.svg|centre|vignette|upright=2|Pipeline d'OpenGL 1.0]] Le pipeline d'OpenGL 1.0 vu plus haut est très simple, comparé aux pipelines des API modernes. Pour comparaison, voici des schémas qui décrivent le pipeline de DirextX 10 et 11. Vous voyez que le nombre d'étapes n'est pas le même, que les étapes elles-mêmes sont légèrement différentes, etc. Toutes les API 3D modernes sont organisées plus ou moins de la même manière, ce qui fait que le pipeline des schémas ci-dessous colle assez bien avec les logiciels 3D anciens et modernes, ainsi qu'avec l'organisation des cartes graphiques (anciennes ou modernes). {| | style="vertical-align:top;" | [[File:D3D Pipeline.svg|vignette|D3D Pipeline]] |[[File:D3D11 Pipeline.svg|vignette|Pipeline de D3D 11]] |} ===L'implémentation peut être logicielle ou matérielle=== Une API graphique est avant tout quelque chose qui aide le programmeur. Il est d'ailleurs possible de les utiliser sans GPU, avec une simple carte d'affichage. Le rendu 3D se fait alors sur le processeur, et la carte d'affichage ne fait que recevoir l'image calculée et l'afficher. Et c'était le cas dans les années 90, avant l'invention des premières cartes accélératrices 3D. Le rôle des API 3D était de fournir des morceaux de code et un pipeline graphique, afin de simplifier le travail des développeurs, pas de déporter des calculs sur une carte accélératrice 3D. D'ailleurs, OpenGl et Direct X sont apparues avant que les premières cartes graphiques grand public soient inventées. Les premiers accélérateurs 3D sont arrivés sur le marché quelques mois après la toute première version de Direct X et Microsoft n'avait pas prévu le coup. OpenGL était lui encore plus ancien et ne servait pas initialement pour les jeux vidéos, mais pour la production d'images de synthèses et dans des applications industrielles (conception assistée par ordinateur, imagerie médicale, autres). OpenGL était l'API plébiscitée à l'époque, car elle était déjà bien implantée dans le domaine industriel, la compatibilité avec les différents OS de l'époque était très bonne, mais aussi car elle était assez simple à programmer. De nos jours, la grosse majorité du rendu 3D se fait sur le GPU. Les ''draw calls'' sont intégralement traités par le GPU, à quelques détails près. Mais les premières cartes accélératrices 3D ne le gérait que partiellement. Concrétement, les premières cartes de 3Dfx déléguaient le traitement de la géométrie au processeur, et ne s'occupaient que des étapes de rastérisation, de placage de texture et les étapes suivantes. Autant prévenir maintenant, nous verrons de nombreuses cartes graphiques de de genre dans le chapitre sur l'historique de l'accélération 3D. ===Les API imposent des contraintes sur le matériel=== Les API graphiques décrivent un pipeline, mais fournissent aussi d'autres contraintes. Par exemple, elles fournissent des régles sur la manière dont doit être faite la rastérisation. Elle disent plus ou moins quel doit être le résultat attendu par le programmeur. Et les GPU doivent respecter ces règles, ils doivent effectuer le rendu de manière à avoir un résultat identique à celui spécifié par l'API. Notez ma formulation quelque peu alambiquée, qui cache un point important : les GPU font comme si ! Je dis faire comme si, car il se peut que le matériel fasse autrement, mais pour un résultat identique. Tant que l'image finale est celle attendue par l'API 3D, le GPU a le droit de prendre des raccourcis, d'éliminer des calculs inutiles, d'utiliser un algorithme de rastérisation différent, etc. Par exemple, il arrive que la carte graphique fasse certaines opérations en avance, comparé au pipeline imposé par l'API, pour des raisons de performance. Typiquement, effectuer du ''culling'' ou les tests de profondeur plus tôt permet d'annuler de nombreux pixels invisibles à l'écran, et donc d'éliminer beaucoup de calculs inutiles. Mais la carte graphique doit cependant corriger le tout de manière à ce que pour le programmeur, tout se passe comme l'API 3D l'ordonne. De manière générale, sans même se limiter à l'ordonnancement des étapes du pipeline graphique, les règles imposées par les API 3D sont des contraintes fortes, qui contraignent les cartes graphiques dans ce qu'elles peuvent faire. De nombreuses optimisations sont rendues impossibles à cause des contraintes des API 3D. ==Le pilote de carte graphique== Le pilote de la carte graphique est un logiciel qui s'occupe de faire l'interface entre les API 3D et la carte graphique. 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é. Le pilote d'un périphérique sert justement à ajouter ce qui manque : ils ajoutent au système d'exploitation de quoi reconnaître le périphérique, de quoi l'utiliser au mieux. 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 très basique, juste assez pour afficher l’interface de base du système d'exploitation. Par exemple, certaines résolutions ne sont pas disponibles et 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. Le pilote de la carte graphique gère beaucoup de choses. Comme tout pilote de périphérique, il gère la communication entre procersseur et GPU, via des techniques communes comme les interruptions, le ''pooling'' ou le ''DMA''. Plus évident, il s'occupe de la gestion de la mémoire vidéo, à savoir que c'est lui qui place les textures ou les modèles 3D dedans, il place le ''framebuffer'', les ''render target'' et tout ce qui réside en mémoire vidéo. Il s'occupe aussi des fonctionnalités liées à l'affichage : initialiser la carte graphique, fixer la résolution, le taux de rafraichissement, gérer le curseur de souris matériel, etc. Mais surtout, le pilote de périphérique s'occupe de l'exécution des ''draw call'' et des changements de ''render state''. Dans ce qui suit, nous allons nous intéresser aux fonctionnalités spécifiques au rendu 3D. ===Les commandes matérielles, compréhensibles par le GPU=== Pour rappel, les API 3Denvoient des '''commandes graphiques''' au pilote de périphérique. Les commandes graphiques sont standardisées, spécifiques à chaque API, et surtout : indépendantes du matériel. Le matériel ne comprend pas ces commandes graphiques ! A la place, le GPU comprend des '''commandes matérielles''', spécifiques à chaque marque de GPU, si ce n'est à chaque GPU. Lors du passage à une nouvelle génération de GPU, des commandes matérielles peuvent apparaître, d'autres disparaître, d'autre voient leur fonctionnement légèrement altéré, etc. Le pilote de la carte graphique doit convertir les commandes graphiques de l'API 3D, en commandes matérielles que le GPU peut comprendre. : La traduction des commandes se fait dans le pilote en espace utilisateur, alors que leur envoi au GPU est le fait du pilote en espace noyau. L'envoi des commandes à la carte graphique ne se fait pas directement. La carte graphique n'est pas toujours libre pour accepter une nouvelle commande, soit parce qu'elle est occupée par une commande précédente, soit parce qu'elle fait autre chose. Il faut alors faire patienter les données dans une '''file de commandes''', où les commandes matérielles attendent leur tour, dans l'ordre d'arrivée. Elle est placée soit dans une portion de la mémoire vidéo, soit est dans la mémoire RAM. Si la file de commandes est plein, le driver n'accepte plus de demandes en provenance des applications. Une file de commandes pleine 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 la file 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. ===La compilation des ''shaders''=== Le pilote de carte graphique traduit les ''shaders'' en code machine que le GPU peut exécuter. 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, avant d'être pré-compilés vers un langage dit intermédiaire. 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. En clair, il y a deux passes de compilation : une passe de traduction du code source en langage intermédiaire, puis une passe de compilation du code intermédiaire vers le code machine. Notons que la première passe est réalisée par le programmeur des ''shaders'', alors que la seconde est le fait du pilote du GPU. L'avantage est que la compilation prend moins de temps, comparé à compiler directement du code HLSL/GLSL. Le gros du travail à été fait lors de la première passe de compilation et le pilote graphique ne fait que finir le travail. 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 plus vendus et/ou les plus populaires. 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. Les anciennes cartes graphiques avaient des circuits de T&L pour traiter la géométrie, mais elles ont disparues sur les machines récentes. Par souci de compatibilité, les circuits de T&L doivent être émulés sur les GPU récents. Sans cette émulation, les vieux jeux vidéo 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'' fournit par le pilote de carte graphique. {{NavChapitre | book=Les cartes graphiques | prev=La mémoire unifiée et la mémoire vidéo dédiée | prevText=La mémoire unifiée et la mémoire vidéo dédiée | next=Le processeur de commandes | nextText=Le processeur de commandes }}{{autocat}} csa0u9sxetj30r8648f6nvto4dssmrl 763043 763042 2026-04-06T15:09:55Z Mewtow 31375 /* La description des API 3D les plus communes */ 763043 wikitext text/x-wiki De nos jours, le développement de jeux vidéo, ou tout simplement de tout rendu 3D, utilise des API 3D. Les API 3D les plus connues sont DirectX, OpenGL, et Vulkan. L'enjeu des API est de ne pas avoir à recoder un moteur de jeu différent pour chaque carte graphique ou ordinateur existant. Elles fournissent des fonctions qui effectuent des calculs bien spécifiques de rendu 3D, mais pas que. L'application de rendu 3D utilise des fonctionnalités de ces API 3D, qui elles-mêmes utilisent les autres intermédiaires, les autres maillons de la chaîne. Typiquement, ces API communiquent avec le pilote de la carte graphique et le système d'exploitation. ==La description des API 3D les plus communes== Dans ce chapitre, nous n'allons pas faire de cours du DirextX, ulkan ou toute API précise. Toutes le API graphiques fonctionnent globalement sur les mêmes principes, que nous allons expliquer dans les grandes lignes. Les explications seront conçues pour que les personnes sans bagage de la programmation graphique puissent comprendre, seuls desbases très mineures en programmation seront nécessaires dans le pire des cas. ===Les ''draw calls''=== Une API 3D fournit un certain nombre de fonctions qu'un programmeur peut exécuter à loisir. La principale est la fonction qui dessine quelque chose dans le ''framebuffer''. Elle est appelée ''draw()'' dans la terminologie DirectX, gldraw pour OpenGL, vkcmddraw pour Vulkan. Une exécution de cette fonction est appelée un '''''draw call'''''. Un ''draw call''envoie des informations à la carte graphique, afin qu'elle affiche ce qui est demandé. Instinctivement, on pourrait croire que la fonction ''draw'' calcule tout l'image à afficher d'un seul coup, mais ce n'est pas le cas. En réalité, le moteur graphique d'un jeu effectue le rendu objet par objet, avec un ''draw call'' par objet. Plus il y a d'objets, plus le processeur exécutera de ''draw calls''. Diverses optimisations permettent d'économiser des ''draw calls'', mais cela ne change pas le fait que dessiner l'image finale demande plusieurs ''draw calls'', entre une centaine et plusieurs centaines de milliers suivant la complexité de la scène à rendre. Le fait de rendre une image objet par objet permet de nombreuses optimisations. Par exemple, il peut utiliser une première passe pour dessiner les objets opaques, puis une seconde pour les objets transparents. Tous les moteurs 3D font ainsi, car gérer la transparence est toujours compliqué, surtout avec un tampon de profondeur. Un autre avantage est que le moteur de jeu peut faciliter le travail de l'élimination des surfaces cachées. Par exemple, le moteur de jeu peut trier les objets selon leur profondeur, afin de les rendre du plus proche au plus lointain. Pour les objets opaques, cela permet d'éliminer les surfaces cachées à la perfection : aucun triangle/pixel caché par un autre ne sera rendu. Pour la transparence, cela permet un rendu idéal. Mais trier les objets selon leur profondeur prend alors du temps CPU, qu'il faut comparer à ce qui est gagné sur le GPU. Avant les années 2010 environ, le processeur faisait une bonne partie de l'élimination des surfaces cachées, dans le sens où il déterminait quels objets étaient cachés par d'autres. Il n'émettait pas de ''draw calls'' pour les objets complétement cachés par un autre objet opaque. Par contre, il travaillait au niveau des objets, alors que le GPU travaillait au niveau des triangles. Les objets partiellement cachés étaient gérés par le GPU, avec une élimination des surface cachées triangle par triangle. De nos jours, l'élimination des surfaces cachées est réalisée sur le GPU, dans sa totalité. L'idée est d'utiliser un ''shader'' séparé, un ''compute shader'', qui s'exécute avant toute autre opération de rendu. La scène 3D et tous les modèles sont dans la mémoire vidéo, et non en mémoire RAM. Le ''compute shader'' lit l'ensemble de la géométrie et élimine les surface cachées. On parle de '''''GPU driven rendering''''' pour désigner cette élimination des surfaces cachées réalisée sur le GPU (il faudrait aussi rajouter le choix du ''Level Of Detail'', mais passons. ===Les ''render target''=== Plus haut, j'ai dit qu'un ''draw call'' dessine une image dans le ''framebuffer''. Et il s'agit là du cas le plus important, mais certaines techniques de rendu demandent de dessiner des images intermédiaires, qui sont utilisées pour calculer l'image finale. Les images intermédiaires doivent alors être enregistrées ailleurs, par exemple dans une texture. L'idée générale d'enregistrer des images intermédiaires dans une texture, qui sont alors lues par un ''pixel shader'' pour des calculs d'éclairage, des filtres de post-traitement, ou autre. Autoriser d'enregistrer l'image finale dans une texture s'appelle du '''''render-to-texture'''''. Les techniques d'éclairage basées sur des ''shadowmap'' sont dans ce cas. Elles demandent de rendre la scène 3D deux fois : une fois du point de vue de la source de lumière, puis une seconde fois pour obtenir l'image finale. L'idée est que les pixels invisibles depuis la source de lumière, mais visibles depuis la caméra, sont dans l'ombre. La scène rendue depuis la caméra doit donc être mémorisée quelque part, de préférence dans une texture appelée une ''shadowmap''. Une autre utilisation est l'application de filtres de post-traitement, comme du bloom, de la profondeur de champ, etc. L'idée est de mémoriser l'image initiale, sans post-traitement, dans une texture. Puis, un ''shader'' lit cette texture, applique un filtre dessus, et mémorise le résultat dans une autre texture ou dans le ''framebuffer'' s'il calcule l'image finale. Pour cela, les API 3D modernes permettent de préciser où enregistrer l'image finale : dans le ''framebuffer'', dans une texture, dans une simple portion de mémoire, etc. Les endroits où l'image finale peut être rendue s'appellent des '''''render target'''''. Les API modernes supportent de nombreux ''render target'', avec au minimum un ''framebuffer''. Initialement, les API anciennes ne supportaient que le ''framebuffer''. Puis le ''render-to-texture'' est apparu, puis d'autres formes de ''render target''. Il faut noter que les API modernes permettent à un ''shader'' d'écrire dans plusieurs ''render-target''. On parle alors de '''''Multiple Render Targets''''', abrévié en MRT. Le MRT accélère fortement les techniques de rendu différé, qui enregistrent plusieurs images séparées, qui sont combinées par un pixel shader pour obtenir l'image finale. L'intérêt initial était d'accélérer le calcul de l'éclairage par pixel. Sans rendu différé, avec les anciennes API graphiques, il fallait utiliser un ''draw call'' par objet et par source de lumière. Un objet éclairé par N sources de lumière demandait N ''draw call'' pour être éclairé. Avec le rendu différé, pas besoin. De plus, on garantit que le calcul de l'éclairage n'est pas réalisé sur des pixels invisibles, à savoir des calculer l'éclairage pour des triangles cachés par un objet opaque. Le désavantage est que la transparence n'est pas prise en charge, de même que l’antialiasing de type MSAA. Le rendu différé demande deux passes de rendu. La première passe calcule tout, sauf le ''pixel shader'', il n'y a pas de calculs d'éclairage par pixel. Elle enregistre son résultat dans plusieurs textures :un avec la couleur non-éclairée de chaque pixel, un autre pour la profondeur de chaque pixel (le tampon de profondeur), une texture contenant les normales de la surface pour chaque pixel, et éventuellement d'autres informations (couleur spéculaire, autres). Les textures sont ensuite utilisées par un pixel shader pour calculer l'image finale avec éclairage. Il faut alors supporter des pseudo-''framebuffer'' pour chaque "texture", appelés des '''''G-buffer''''', pour gérer de telles techniques. De plus, le MRT optimise le rendu. Pas besoin de faire un ''draw call'' par ''G-buffer'', chacun recalculant la géométrie. Avec le MRT, les différents ''G-buffer'' sont calculés en une seule passe, la géométrie n'est calculée qu'une seule fois. {| |[[File:Deferred rendering pass col.jpg|thumb|''G-buffer'' pour la couleur.]] |[[File:Deferred rendering pass dep.jpg|thumb|''G-buffer'' pour la profondeur.]] |[[File:Deferred rendering pass nor.jpg|thumb|''G-buffer'' pour les normales.]] |[[File:Deferred rendering pass res.jpg|thumb|Image finale]] |} ===Les attributs de sommets et variables uniformes=== Lors d'un ''draw call'', certains paramètres vont rester constants, alors que d'autres vont varier d'un sommet à l'autre. Les paramètres qui varient d'un sommet à l'autre sont des '''attributs de sommet'''. Par exemple, prenons un sommet : sa position, sa couleur et ses coordonnées de texture sont des attributs du sommet. Les paramètres constants sont appelées des '''variables uniformes''', ou encore des ''uniforms''. Elles restent les mêmes pour un objet, mais varient d'un objet à l'autre. Un exemple est les matrices utilisées par les étapes de transformation et de projection. : Il y a la même chose avec les pixels, avec des attributs de pixels et des ''pixel uniforms'', la différence étant que les attributs de pixels sont calculés par la rastérisation. Les deux sont stockés différemment : les variables uniformes sont simplement intégrées dans les shaders, alors que les attributs sont placés dans le tampon de sommets. Il faut noter que les processeurs de shaders avaient autrefois des registres séparés pour les deux, et c'est toujours un peu le cas à l'heure actuelle. ===Les ''render states'' et les ''Pipeline State Object''=== Pour rendre un objet avec un ''draw call'', il faut préciser toutes informations nécessaires pour son rendu : la géométrie de l'objet représentée par une liste de triangles, les textures de l'objet, les ''shaders'' à exécuter (vertex ou pixel shaders), etc. Pour simplifier, nous allons regrouper ces informations en deux : un ''mesh'' qui représente la géométrie de l'objet, et le reste. La géométrie de l'objet est juste une liste de triangles. Le reste est regroupé dans un '''''render state''''', qui liste les textures, les shaders, quel ''render starget'' utiliser, et surtout : diverses options de configuration. Il n'y a qu'un seul ''render state'' actif, qui est mémorisé dans une portion de la RAM qui est toujours fixe. Pour les programmeurs, le ''render state'' est dans une variable globale, qui est lue directement par la fonction ''draw''. Si on veut rendre un objet, on doit mettre à jour le ''render state'' avant de lancer un ''draw call''. Un moteur graphique fait donc le travail suivant : * Pour chaque image : ** Mettre à jour la position de la caméra et autres ** Pour chaque objet, scène 3D inclue : *** 1 - Mettre à jour le ''render state'' *** 2 - Exécuter le ''draw call'' L'API 3D fournit des fonctions pour modifier le ''render state'', en plus de la fonction''draw''. A ce niveau, les anciennes API fonctionne différemment des API plus récentes comme DirectX 12, Vulkan et consort. Les anciennes API fournissaient plusieurs fonctions très spécialisées : certaines pour modifier les textures, d'autres pour changer les shaders, et un paquet d’autres pour modifier telle ou telle option de configuration. Par exemple, il y a probablement une fonction pour changer l'antialiasing. Les API modernes, comme DirectX 12 et Vulkan, permettent de mettre à jour le ''render state'' assez simplement. L'idée est de pré-calculer un ''render state'', qui est alors appelé un ''Pipeline State Object'' (PSO). Mettre à jour le ''render state'' demande alors juste de copier un PSO dans le render state, au lieu d'exécuter une dizaine ou centaine de fonctions pour obtenir le ''render state'' voulu. ===Les commandes graphiques=== L'API 3D traduit chaque ''draw call'' en une ou plusieurs '''commandes graphiques''', qui sont envoyées au ''driver'' du GPU. Les commandes en question sont assez diverses, mais elles sont spécifiques à chaque API graphique. Intuitivement, un ''draw call'' correspond à une commande graphique. Mais il peut y avoir d'autres types de commandes. Par exemple, copier une texture dans la mémoire vidéo demande d'exécuter une commande decopie, idem pour ce qui est de copier un objet/''mesh''. Pour comprendre en quoi un ''draw call'' peut se traduire en plusieurs commandes, prenons l'exemple suivant. On souhaite rendre un objet avec une texture bien précise, mais celle-ci n'a pas encore été chargée en mémoire vidéo. Dans ce cas, le ''draw call'' utilisera une commande pour copier la texture en mémoire vidéo, puis une seconde commande pour rendre l'objet dans le ''framebuffer''. Par contre, si la texture est déjà en mémoire vidéo, le ''draw call'' se traduira en une unique commande de rendu 3D. Il en est de même si le ''mesh'' n'est pas encore en mémoire vidéo : il faut exécuter une commande pour copier le ''mesh'' dans la mémoire vidéo. Il faut préciser que c'est la même chose si le ''draw call'' exécute un shader pour la première fois . Le ''driver'' doit compiler le shader pour la première fois, puis utiliser une commande pour mettre le résultat en mémoire vidéo, puis enfin effectuer le rendu. Cela explique le ''shader stuttering'' présent dans certains jeux récents, à savoir le petit ralentissement très énervant qui survient quand un ''shader'' est compilé en plein milieu d'une partie de jeu. Il est possible de limiter ce problème en compilant des shaders à l'avance, histoire de préparer le terrain pour les futurs ''draw calls'', dans une certaine mesure, mais cela demande du travail, qui n'est possible que le nombre de shaders à compiler reste faible. Les commandes graphiques sont envoyées au ''driver'' de la carte graphique. Il transforme alors ces commandes graphiques en '''commandes matérielles''', compréhensibles par le matériel, en quelque chose que le GPU peut exécuter. Le format des commandes matérielles est spécifique à chaque marquer de GPU, les GPU NVIDIA, Intel et AMD n'utilisent pas le même format de commande. Il est même possible que chaque GPU ait son propre format pour les commandes matérielles. Aussi, nous allons nous arrêter là pour le moment et laissons cela au chapitre sur le processeur de commande. ===Les optimisations liées aux ''draw calls''=== Il faut noter qu'un ''draw call'' demande d'utiliser un peu de puissance CPU : il faut traduire le ''draw call'' en commandes, les envoyer au ''driver'', qui fait du travail dessus, avant de les envoyer au GPU. Dans les premières versions d'OpenGL et DirectX, chaque ''draw call'' effectuait une commutation de contexte pour passer en espace noyau, afin de communiquer avec le ''driver''. Mais cette contrainte a depuis été relâchée, bien qu'elle marche dans les grandes lignes. Faire plein de ''draw calls'' aura donc un cout en CPU conséquent. Une première optimisation regroupe les objets avec le même ''render state'' ensemble. Sans cette optimisation, le moteur graphique met à jour le ''render state'' à chaque fois qu'il rend un objet. Avec cette optimisation, il met à jour le ''render state'' plus rarement. Par contre, le moteur graphique dépense du temps et de la puissance de calcul pour faire le tri. Il y a donc un compromis pas évident, qui ne vaudrait pas souvent le coup. Cependant, cette optimisation débloque d'autres optimisations très importantes, qui permettent de réduire le nombre de ''draw calls''. Plus haut, j'ai dit que le rendu se fait objet par objet, ''mesh'' par ''mesh''. Mais il s'agit là d'une simplification. En réalité, tout moteur graphique digne de ce nom incorpore des optimisations qui cassent cette règle. L'idée est d'éviter de faire plein de petits ''draw call'' : le GPU sera alors peu utilisé alors que le CPU fera beaucoup de travail. A l'inverse, faire peu de gros ''draw call'' entrainera une forte occupation du GPU au prix d'un cout CPU mineur. La première optimisation, appelée le '''''batching''''', regroupe plusieurs objets/''meshs'' en un seul ''draw call''. Par contre, cette optimisation ne marche que pour des objets ayant le même ''render state'', à l'exception de la géométrie. Les deux objets rendus ensemble doivent utiliser les mêmes shaders, les mêmes textures, etc. De plus, la fusion de deux objets doit se faire en mémoire RAM et est le fait du CPU, le GPU et la mémoire vidéo ne sont pas concernés. L'optimisation marche bien pour des objets statiques, ce qui permet de faire la fusion une fois pour toute, là où les objets dynamiques demandent de faire la fusion à chaque image. Diverses optimisations permettent de faciliter le ''batching''. L'idée est de rendre les différents ''render state'' plus similaires que la normale. Une optimisation de ce type est l'usage d''''atlas de textures'''. Un atlas de texture regroupe plusieurs textures en une seule texture. Deux objets avec les mêmes shaders et les mêmes options de configuration, peuvent ainsi partager le même ''render state'' quand ils adressent le même atlas de texture et non exactement les mêmes textures. Une seconde optimisation,appelée l''''''instancing''''', marche dans le cas où un objet dynamique est présent en plusieurs exemplaires à l'écran. L'idée est qu'au lieu d'utiliser un ''draw call'' par exemplaire, on utilise un seul ''draw call'' pour tous les exemplaires. L'avantage est que la carte n'a besoin de mémoriser qu'un seul exemplaire en mémoire vidéo, au lieu de mémoriser plusieurs copies du même ''mesh''. Il faut préciser que les différents exemplaires peuvent être placés à des endroits éloignés, être tournés différemment par rapport à la caméra, être dans des états d'animation différents, etc. Pour cela, le ''draw call'' précise, pour chaque exemplaire, comment l'orienter, le tourner et l'animer. Le ''render state'' contient pour cela une '''liste d'instances''' pour mémoriser ces informations pur chaque exemplaire. Le GPU peut consulter cette liste et la copier en mémoire vidéo. Une seule commande permet ainsi de rendre plusieurs exemplaires : le GPU lit la liste d'instance, le ''mesh'' et dessine automatiquement chaque exemplaire voulu de l'objet. Réduire le nombre de ''draw calls'' peut aussi se faire en évitant les objets peu détaillés, qui utilisent peu de polygones. Pour des objets trop peu détaillés, le GPU exécutera le ''draw call'' très vite et devra attendre que le CPU envoie le suivant. Le cout du ''draw call'' dominera le temps de calcul sur le GPU. Du temps de DirectX 9, l'idéal était d'avoir des objets d'au moins une centaine de triangles. De nos jours, les GPU les CPU sont plus puissant,ce qui fait que ce chiffre est à revoir, mais je n'en connais pas la valeur, même approximative. ==Le pipeline graphique== En plus de fournir des fonctions que les programmeurs peuvent utiliser, les API graphiques décrivent comment s'effectue le rendu d'une image. Elles spécifient comment doit être traité la géométrie, comment doit se faire la rastérisation, le filtrage de texture et bien d'autres choses. Pour le dire autrement, elles décrivent le pipeline graphique à utiliser. Pour rappel, le pipeline graphique comprend plusieurs étapes : plusieurs étapes de traitement de la géométrie, une phase de rastérisation, puis plusieurs étapes de traitement des pixels. Une API 3D comme DirectX ou OpenGl décrète quelles sont les étapes à faire, ce qu'elles font, et l'ordre dans lesquelles il faut les exécuter. Il n'existe pas un pipeline graphique unique et chaque API 3D fait à sa sauce, mais la plupart des API modernes ont des pipelines graphiques très similaires. Les seules différences majeures concernent la présence d'étapes facultatives, comme l'étape de tesselation, qui sont absentes des API anciennes. Pour donner un exemple, je vais prendre l'exemple d'OpenGL 1.0, une des premières version d'OpenGL, aujourd'hui totalement obsolète. Le pipeline d'OpenGL 1.0 est illustré ci-dessous. Il implémente le pipeline graphique de base, avec une phase de traitement de la géométrie (''per vertex operations'' et ''primitive assembly''), la rastérisation, et les traitements sur les pixels (''per fragment operations''). On y voit la présence du ''framebuffer'' et de la mémoire dédiée aux textures, les deux étant soit séparées, soit placée dans la même mémoire vidéo. La ''display list'' est une liste de commandes, de ''draw calls'', que la carte graphique doit traiter d'un seul bloc, chaque ''display list'' correspond au rendu d'une image, pour simplifier. Les étapes ''evaluator'' et ''pixel operations'' sont des étapes facultatives, qui ne sont pas dans le pipeline graphique de base, mais qui sont utiles pour implémenter certains effets graphiques. [[File:Pipeline OpenGL.svg|centre|vignette|upright=2|Pipeline d'OpenGL 1.0]] Le pipeline d'OpenGL 1.0 vu plus haut est très simple, comparé aux pipelines des API modernes. Pour comparaison, voici des schémas qui décrivent le pipeline de DirextX 10 et 11. Vous voyez que le nombre d'étapes n'est pas le même, que les étapes elles-mêmes sont légèrement différentes, etc. Toutes les API 3D modernes sont organisées plus ou moins de la même manière, ce qui fait que le pipeline des schémas ci-dessous colle assez bien avec les logiciels 3D anciens et modernes, ainsi qu'avec l'organisation des cartes graphiques (anciennes ou modernes). {| | style="vertical-align:top;" | [[File:D3D Pipeline.svg|vignette|D3D Pipeline]] |[[File:D3D11 Pipeline.svg|vignette|Pipeline de D3D 11]] |} ===L'implémentation peut être logicielle ou matérielle=== Une API graphique est avant tout quelque chose qui aide le programmeur. Il est d'ailleurs possible de les utiliser sans GPU, avec une simple carte d'affichage. Le rendu 3D se fait alors sur le processeur, et la carte d'affichage ne fait que recevoir l'image calculée et l'afficher. Et c'était le cas dans les années 90, avant l'invention des premières cartes accélératrices 3D. Le rôle des API 3D était de fournir des morceaux de code et un pipeline graphique, afin de simplifier le travail des développeurs, pas de déporter des calculs sur une carte accélératrice 3D. D'ailleurs, OpenGl et Direct X sont apparues avant que les premières cartes graphiques grand public soient inventées. Les premiers accélérateurs 3D sont arrivés sur le marché quelques mois après la toute première version de Direct X et Microsoft n'avait pas prévu le coup. OpenGL était lui encore plus ancien et ne servait pas initialement pour les jeux vidéos, mais pour la production d'images de synthèses et dans des applications industrielles (conception assistée par ordinateur, imagerie médicale, autres). OpenGL était l'API plébiscitée à l'époque, car elle était déjà bien implantée dans le domaine industriel, la compatibilité avec les différents OS de l'époque était très bonne, mais aussi car elle était assez simple à programmer. De nos jours, la grosse majorité du rendu 3D se fait sur le GPU. Les ''draw calls'' sont intégralement traités par le GPU, à quelques détails près. Mais les premières cartes accélératrices 3D ne le gérait que partiellement. Concrétement, les premières cartes de 3Dfx déléguaient le traitement de la géométrie au processeur, et ne s'occupaient que des étapes de rastérisation, de placage de texture et les étapes suivantes. Autant prévenir maintenant, nous verrons de nombreuses cartes graphiques de de genre dans le chapitre sur l'historique de l'accélération 3D. ===Les API imposent des contraintes sur le matériel=== Les API graphiques décrivent un pipeline, mais fournissent aussi d'autres contraintes. Par exemple, elles fournissent des régles sur la manière dont doit être faite la rastérisation. Elle disent plus ou moins quel doit être le résultat attendu par le programmeur. Et les GPU doivent respecter ces règles, ils doivent effectuer le rendu de manière à avoir un résultat identique à celui spécifié par l'API. Notez ma formulation quelque peu alambiquée, qui cache un point important : les GPU font comme si ! Je dis faire comme si, car il se peut que le matériel fasse autrement, mais pour un résultat identique. Tant que l'image finale est celle attendue par l'API 3D, le GPU a le droit de prendre des raccourcis, d'éliminer des calculs inutiles, d'utiliser un algorithme de rastérisation différent, etc. Par exemple, il arrive que la carte graphique fasse certaines opérations en avance, comparé au pipeline imposé par l'API, pour des raisons de performance. Typiquement, effectuer du ''culling'' ou les tests de profondeur plus tôt permet d'annuler de nombreux pixels invisibles à l'écran, et donc d'éliminer beaucoup de calculs inutiles. Mais la carte graphique doit cependant corriger le tout de manière à ce que pour le programmeur, tout se passe comme l'API 3D l'ordonne. De manière générale, sans même se limiter à l'ordonnancement des étapes du pipeline graphique, les règles imposées par les API 3D sont des contraintes fortes, qui contraignent les cartes graphiques dans ce qu'elles peuvent faire. De nombreuses optimisations sont rendues impossibles à cause des contraintes des API 3D. ==Le pilote de carte graphique== Le pilote de la carte graphique est un logiciel qui s'occupe de faire l'interface entre les API 3D et la carte graphique. 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é. Le pilote d'un périphérique sert justement à ajouter ce qui manque : ils ajoutent au système d'exploitation de quoi reconnaître le périphérique, de quoi l'utiliser au mieux. 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 très basique, juste assez pour afficher l’interface de base du système d'exploitation. Par exemple, certaines résolutions ne sont pas disponibles et 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. Le pilote de la carte graphique gère beaucoup de choses. Comme tout pilote de périphérique, il gère la communication entre procersseur et GPU, via des techniques communes comme les interruptions, le ''pooling'' ou le ''DMA''. Plus évident, il s'occupe de la gestion de la mémoire vidéo, à savoir que c'est lui qui place les textures ou les modèles 3D dedans, il place le ''framebuffer'', les ''render target'' et tout ce qui réside en mémoire vidéo. Il s'occupe aussi des fonctionnalités liées à l'affichage : initialiser la carte graphique, fixer la résolution, le taux de rafraichissement, gérer le curseur de souris matériel, etc. Mais surtout, le pilote de périphérique s'occupe de l'exécution des ''draw call'' et des changements de ''render state''. Dans ce qui suit, nous allons nous intéresser aux fonctionnalités spécifiques au rendu 3D. ===Les commandes matérielles, compréhensibles par le GPU=== Pour rappel, les API 3Denvoient des '''commandes graphiques''' au pilote de périphérique. Les commandes graphiques sont standardisées, spécifiques à chaque API, et surtout : indépendantes du matériel. Le matériel ne comprend pas ces commandes graphiques ! A la place, le GPU comprend des '''commandes matérielles''', spécifiques à chaque marque de GPU, si ce n'est à chaque GPU. Lors du passage à une nouvelle génération de GPU, des commandes matérielles peuvent apparaître, d'autres disparaître, d'autre voient leur fonctionnement légèrement altéré, etc. Le pilote de la carte graphique doit convertir les commandes graphiques de l'API 3D, en commandes matérielles que le GPU peut comprendre. : La traduction des commandes se fait dans le pilote en espace utilisateur, alors que leur envoi au GPU est le fait du pilote en espace noyau. L'envoi des commandes à la carte graphique ne se fait pas directement. La carte graphique n'est pas toujours libre pour accepter une nouvelle commande, soit parce qu'elle est occupée par une commande précédente, soit parce qu'elle fait autre chose. Il faut alors faire patienter les données dans une '''file de commandes''', où les commandes matérielles attendent leur tour, dans l'ordre d'arrivée. Elle est placée soit dans une portion de la mémoire vidéo, soit est dans la mémoire RAM. Si la file de commandes est plein, le driver n'accepte plus de demandes en provenance des applications. Une file de commandes pleine 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 la file 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. ===La compilation des ''shaders''=== Le pilote de carte graphique traduit les ''shaders'' en code machine que le GPU peut exécuter. 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, avant d'être pré-compilés vers un langage dit intermédiaire. 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. En clair, il y a deux passes de compilation : une passe de traduction du code source en langage intermédiaire, puis une passe de compilation du code intermédiaire vers le code machine. Notons que la première passe est réalisée par le programmeur des ''shaders'', alors que la seconde est le fait du pilote du GPU. L'avantage est que la compilation prend moins de temps, comparé à compiler directement du code HLSL/GLSL. Le gros du travail à été fait lors de la première passe de compilation et le pilote graphique ne fait que finir le travail. 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 plus vendus et/ou les plus populaires. 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. Les anciennes cartes graphiques avaient des circuits de T&L pour traiter la géométrie, mais elles ont disparues sur les machines récentes. Par souci de compatibilité, les circuits de T&L doivent être émulés sur les GPU récents. Sans cette émulation, les vieux jeux vidéo 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'' fournit par le pilote de carte graphique. {{NavChapitre | book=Les cartes graphiques | prev=La mémoire unifiée et la mémoire vidéo dédiée | prevText=La mémoire unifiée et la mémoire vidéo dédiée | next=Le processeur de commandes | nextText=Le processeur de commandes }}{{autocat}} 7q8nyi41rm9l0tcro77dx42o63hojcp Wikilivres:Le Bistro/2026 4 83439 763039 762184 2026-04-06T14:04:57Z Xhungab 23827 763039 wikitext text/x-wiki <noinclude>{{Wikilivres:Le Bistro/En-tête}}</noinclude> == Le meilleur à Wikilivres pour 2026 ! == Je crée la page du bistrot 2026 en transmettant mes vœux. Bonne année éditoriale à tous ! [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 16 janvier 2026 à 06:05 (CET) :Merci, meilleurs vœux ! [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 16 janvier 2026 à 09:53 (CET) ::Meilleurs vœux pour 2026 ! ::--&nbsp;◄&nbsp;[[Utilisateur:DavidL|'''D'''avid&nbsp;'''L''']]&nbsp;•&nbsp;[[Discussion Utilisateur:DavidL|discuter]]&nbsp;► 16 janvier 2026 à 20:13 (CET) :::Meilleurs vœux pour 2026 :::[[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 17 janvier 2026 à 11:56 (CET) ::::Bonne année et bonnes lectures et écritures ! ::::[[Utilisateur:Matthius|Matthius]] == Pages orphelines : == Livre de cuisine/Amlou ... En cherchant mes pages orphelines, j'ai vu par exemple qu'une bonne centaine de pages du livre de cuisine étaient référencées, alors qu'elles semblent correctes. Il me semble qu'il y a le même problème avec d'autres livres (photos...). Des pages qui semblent correctes mais orphelines. Comment peut-on corriger ce problème? Merci [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 17 janvier 2026 à 12:05 (CET) :Bonjour Xhungab, :Comme indiqué sur la page [[Spécial:Pages orphelines]], ces pages n'ont pas de lien. Il suffit de créer des liens, depuis un sommaire, ou une page thématique (groupe de recettes, ...) ; les liens depuis une catégorie ne comptent que s'ils sont ajoutés explicitement (pas automatiquement par ajout de catégorie aux pages), par exemple [[:Catégorie:Desserts]]. :--&nbsp;◄&nbsp;[[Utilisateur:DavidL|'''D'''avid&nbsp;'''L''']]&nbsp;•&nbsp;[[Discussion Utilisateur:DavidL|discuter]]&nbsp;► 18 janvier 2026 à 09:56 (CET) ::Bonjour David, ::J'ai vue qu'il existe un livre Recettes_de_la_Provence. ::https://fr.wikibooks.org/wiki/Livre_de_cuisine/Recettes_de_la_Provence ::Il y a ici des recettes de_la_Provence qui ne sont pas dans le livre. ::https://fr.wikibooks.org/wiki/Cat%C3%A9gorie:Cuisine_proven%C3%A7ale ::Je vais essayer d'introduire toutes les recettes dans le livre. ::Comme c'est la première fois que je fais ce genre de travail. Je reviendrai le présenter une fois que j'aurai terminé. ::Merci [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 18 janvier 2026 à 13:56 (CET) :::Merci pour ton aide @[[Utilisateur:Xhungab|Xhungab]] ! [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 21 janvier 2026 à 04:44 (CET) ::::Merci, Lionel Scheepmans ::::Voilà j'ai fait ma première correction : ::::https://fr.wikibooks.org/wiki/Livre_de_cuisine/Recettes_de_la_Provence ::::Si cela est correct je vais voir si je peux trouver d'autres livres régionaux ::::Merci [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 21 janvier 2026 à 12:43 (CET) :::::C'est correct @[[Utilisateur:Xhungab|Xhungab]] et utile pour les personnes en recherche de recettes provençales. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 21 janvier 2026 à 14:29 (CET) ::::::Merci ::::::De temps en temps je vais essayer de travail sur d'autres livres régionaux. ::::::[[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 21 janvier 2026 à 15:13 (CET) == Page orpheline du livre Climat et écocitoyens == Bonjour, Le livre "Climat et écocitoyens" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. Merci * Travail terminé. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 2 février 2026 à 00:41 (CET) :Salut Xhungab :{{Fait}} Pages fusionnées pour conserver les historiques. :--&nbsp;◄&nbsp;[[Utilisateur:DavidL|'''D'''avid&nbsp;'''L''']]&nbsp;•&nbsp;[[Discussion Utilisateur:DavidL|discuter]]&nbsp;► 2 février 2026 à 20:07 (CET) ::Merci David L pour ton aide [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 3 février 2026 à 00:04 (CET) == Page orpheline du livre : Les Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais == Le livre "Les Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais" a laissé quelques pages orphelines. Ces pages orphelines ont été neutralisées. Merci * Travail terminé. Merci [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 7 février 2026 à 11:52 (CET) :{{fait}} Pages vides supprimées. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 10 février 2026 à 08:10 (CET) ::Merci pour ton aide [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 10 février 2026 à 12:09 (CET) == Page orpheline du livre : États généraux du multilinguisme dans les outre-mer == Bonjour, Le livre "États généraux du multilinguisme dans les outre-mer" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. Je montre dans ce livre [[feuilles dupliquées orphelines|feuilles dupliquées orphelines]] cette suite de pages orphelines accompagnée par la page où elles ont été recopiées. Merci [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 10 février 2026 à 19:57 (CET) :@[[Utilisateur:Xhungab|Xhungab]], @[[Utilisateur:JackPotte|JackPotte]], @[[Utilisateur:DavidL|DavidL]], bonjour. Je vois que l'on est en campagne de gestion de pages orphelines et je me pose la question de savoir si on doit les supprimer une fois réintégrées ailleurs ou les supprimer ? Je vois que les deux cas de figure ont été mis en œuvre précédemment. L'avantage de la fusion est de conserver l'historique des modifications, mais je ne suis pas habitué à le faire. Si quelqu'un d'entre vous pouvait m'indiquer une page d'explication, cela m'aiderait à m'y mettre. Ensuite, je me demande si la fusion a vraiment un sens lorsque c'est la même personne qui a créé la page orpheline et la nouvelle au contenu copié collé pour insertion ? Qu'en pensez-vous ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 13 février 2026 à 16:02 (CET) ::Petit complément. J'ai créé ce livre ::* [[feuilles volantes orphelines|Feuilles Volantes Orphelines]] ::dans lequel je met ::* les feuilles volantes qui sont dans les pages orphelines. ::* les mini livres qui sont dans les pages orphelines. ::* les livres abandonnés sur une feuille. qui sont dans les pages orphelines. ::Mon intention est d'indiquer si c'est un mini livre, si c'est une feuille sans contenue, et rien si c'est une simple feuille volante. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 13 février 2026 à 16:16 (CET) :::Ça fait plaisir de voir une personne s'investir dans la maintenance de Wikilivres =). Soit le ou la bienvenu.e. J'attends les avis des autres administrateurs pour te donner mon soutien @[[Utilisateur:Xhungab|Xhungab]]. @ bientôt. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 13 février 2026 à 16:27 (CET) :::: @[[Utilisateur:Lionel Scheepmans|Lionel Scheepmans]] bonjour, je n'ai pas compris en quoi "deux cas de figure ont été mis en œuvre précédemment" : si la page est vide on la supprime et si elle a au moins une phrase valable on la fusionne. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 13 février 2026 à 16:26 (CET) ::::Concernant l'auteur pour moi cela importe peu, car on souhaite conserver chaque élément permettant de prouver une primauté du droit d'auteur (par exemple pour ne pas accuser un contributeur d'avoir copié un site miroir de sa page supprimée). [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 13 février 2026 à 16:30 (CET) :::::Désolé [[Utilisateur:JackPotte|JackPotte]], je n'avais pas vu que les pages que tu as supprimées étaient vides. La règle est donc supprimer si vide et fusionner si non. Tu peux me conseiller une page d'aide pour oppérer les fusions ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 13 février 2026 à 16:34 (CET) ::::::Salut, ::::::Pour la fusion d'historique : ::::::* soit [[Wikilivres:Le guide de l'administrateur#Fusionner deux pages|Utiliser la page spéciale pour cela]] (cependant ne fonctionne pas toujours), ::::::* soit [https://fr.wikibooks.org/w/index.php?title=Wikilivres:Le_guide_de_l%27administrateur&oldid=588957#Fusionner_deux_pages utiliser l'ancienne méthode] qui reste toujours valable quand la première ne fonctionne pas. ::::::--&nbsp;◄&nbsp;[[Utilisateur:DavidL|'''D'''avid&nbsp;'''L''']]&nbsp;•&nbsp;[[Discussion Utilisateur:DavidL|discuter]]&nbsp;► 13 février 2026 à 19:34 (CET) :::::::Salut @[[Utilisateur:DavidL|DavidL]]. Je viens de tester l'outil fusionner avec les deux pages suivantes :::::::[[États généraux du multilinguisme dans les outre-mer/Annexes/Contexte]] :::::::vers :::::::[[États généraux du multilinguisme dans les outre-mer/Annexes]] :::::::Et j'ai le message suivant : :::::::« La période spécifiée chevauche les versions préexistantes de la page de destination. » :::::::Tu peux m'expliquer si tu as le temps ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 16 février 2026 à 00:19 (CET) ::::::::Salut @[[Utilisateur:Lionel Scheepmans|Lionel Scheepmans]] ::::::::Dans ce cas, j'utilise [https://fr.wikibooks.org/w/index.php?title=Wikilivres:Le_guide_de_l%27administrateur&oldid=588957#Fusionner_deux_pages l'ancienne méthode]. ::::::::# Renommer A (page qui n'existera plus) vers B, sans laisser de redirection, et en cochant la case de suppression de B (case qui apparait après 1er clic sur le bouton Renommer) (&rarr; l'historique de A est sur B, celui de B est supprimé) ::::::::# Supprimer B (&rarr; l'historique de A et B sont supprimés) ::::::::# Restaurer B et cocher toutes les cases des versions (bouton "Inverser la sélection") (&rarr; restaurer l'historique de A et B) ::::::::# Effectuer une modification de la version finale de la page à afficher, trouvée dans l'historique. ::::::::--&nbsp;◄&nbsp;[[Utilisateur:DavidL|'''D'''avid&nbsp;'''L''']]&nbsp;•&nbsp;[[Discussion Utilisateur:DavidL|discuter]]&nbsp;► 16 février 2026 à 08:29 (CET) :::::::::Merci @[[Utilisateur:DavidL|DavidL]]. Ça m'a l'air bien compliqué tout ça. Et avec toute les pages qu'il faut faire, ça risque de prendre un certain temps. Je manque de courage en pensant que c'est juste pour ajouter une ligne dans l'historique des versions. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 16 février 2026 à 14:51 (CET) == Page orpheline du livre : Programmation PHP == Bonjour, il semble qu'il existe deux versions imprimables de ce livre. Une version en un seul bloc qui est sur la page du livre, et une version en quatre parties qui est dans les pages orphelines. Peut être que l'on pourrait enregistrer ces versions dans le livre, mais je ne sais pas ou. * [[Programmation PHP/Version imprimable1|Version imprimable1]] * [[Programmation PHP/Version imprimable2|Version imprimable2]] * [[Programmation PHP/Version imprimable3|Version imprimable3]] * [[Programmation PHP/Version imprimable4|Version imprimable4]] Merci [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 16 février 2026 à 15:24 (CET) :Salut Xhungab :Les livres de programmation utilisent beaucoup d'extrait de code avec <code><nowiki><syntaxhighlight></nowiki></code> dépassant les limites (trop basses) de MediaWiki empêchant l'affichage de la version imprimable en un bloc. :La version imprimable en 4 parties permet de contourner la limite de MediaWiki. :--&nbsp;◄&nbsp;[[Utilisateur:DavidL|'''D'''avid&nbsp;'''L''']]&nbsp;•&nbsp;[[Discussion Utilisateur:DavidL|discuter]]&nbsp;► 17 février 2026 à 11:50 (CET) ::Merci Pour les info [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 17 février 2026 à 12:17 (CET) == Page orpheline: abandonnée sans contenue == Bonjour, * Travail terminé. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 17 février 2026 à 11:36 (CET) :{{Fait}}Pages supprimées--&nbsp;◄&nbsp;[[Utilisateur:DavidL|'''D'''avid&nbsp;'''L''']]&nbsp;•&nbsp;[[Discussion Utilisateur:DavidL|discuter]]&nbsp;► 17 février 2026 à 11:45 (CET) :Merci [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 17 février 2026 à 11:46 (CET) == Proposition de page à supprimer. == Proposition de page à supprimer : Page orpheline: abandonnée sans contenue * Travail terminé. * Merci [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 21 février 2026 à 13:04 (CET) == archive.today == ''Voir [[:w:Wikipédia:Le Bistro/21 février 2026#archive.today]].'' Ce site pose apparemment des problèmes de sécurité, il faudrait le remplacer au plus vite si possible, et sinon désactiver les liens. [[Utilisateur:SyntaxTerror|SyntaxTerror]] ([[Discussion utilisateur:SyntaxTerror|discussion]]) 21 février 2026 à 13:07 (CET) :Salut SyntaxTerror, :* 0 liens vers archive.today : [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.today] :* <s>18</s> 0 liens vers archive.is [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.is] :* 0 liens vers archive.li [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.li] :* 0 liens vers archive.ec [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.ec] :* 0 liens vers archive.ph [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.ph] :* 0 liens vers archive.fo [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.fo] :* 0 liens vers archive.md [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.md] :* <s>1</s> 0 liens vers archive.vn [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.vn] :* 0 liens vers archive.closed.social [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.closed.social] :--&nbsp;◄&nbsp;[[Utilisateur:DavidL|'''D'''avid&nbsp;'''L''']]&nbsp;•&nbsp;[[Discussion Utilisateur:DavidL|discuter]]&nbsp;► 21 février 2026 à 16:14 (CET) == demande aide pour sortir du mode "ébauche" == je viens de terminer mon livre ... ci dessous. J'ai compris qu'il fallait le signaler comme n'étant plus au stade "ébauche" commente faire Merci https://fr.wikibooks.org/w/index.php?title=Essai_pour_un_mod%C3%A8le_de_psychisme_objectif&action=info#mw-pageinfo-header-basic [[Utilisateur:Clopeau|Clopeau]] ([[Discussion utilisateur:Clopeau|discussion]]) 17 mars 2026 à 08:20 (CET) :Bonjour, quand je regarde ''[[Essai pour un modèle de psychisme objectif]]'', sa complétude permettrait en effet de le sortir des ébauches, mais il semble plutôt d'agir d'un travail de recherche que d'un livre pédagogique sur un sujet sourcé comme reconnu. :Donc en vertu de [[Wikilivres:Principes fondateurs|notre charte]], je propose de le déplacer sur {{WV|Recherche:Accueil}}. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 18 mars 2026 à 09:06 (CET) cbjxu50x2c8z3v8qk228ndnpdwrqe80 Mathc initiation/005h 0 83698 763105 761820 2026-04-07T11:23:47Z Xhungab 23827 763105 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/Fichiers h : c44a4| Sommaire]] {{Partie{{{type|}}}|'''L'intégrale de surface : '''}} {{Partie{{{type|}}}|[[Mathc initiation/Fichiers h : c50a1|* L'intégrale de surface ]]}} {{Partie{{{type|}}}|[[Mathc initiation/Fichiers h : c53a5|* L'intégrale de surface (en coordonnée polaire)]]}} {{Partie{{{type|}}}|[[Mathc initiation/Fichiers h : c60|* L'intégrale de surface (forme explicite) ]]}} {{Partie{{{type|}}}|[[Mathc initiation/a450|* L'intégrale de surface définie paramétriquement ]]}} {{AutoCat}} fhfxbmn9slm6mt8xkc7d54i6pms3e8h Mathc initiation/005s 0 83788 763104 2026-04-07T11:23:14Z Xhungab 23827 news 763104 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/Fichiers h : c44a4| Sommaire]] {{Partie{{{type|}}}|''' L'intégrale de flux de surface : '''}} {{Partie{{{type|}}}|[[Mathc initiation/Fichiers h : c61|* L'intégrale de flux de surface]]}} {{Partie{{{type|}}}|[[Mathc initiation/Fichiers h : c62|* L'intégrale de flux de surface, simplifiée]]}} {{Partie{{{type|}}}|[[Mathc initiation/a458|* L'intégrale de flux de surface définie paramétriquement, simplifiée]]}} {{AutoCat}} e7s6ls22939t1ihr0am0rsggdh6ptwc